> For the complete documentation index, see [llms.txt](https://developers-apps-in-toss.toss.im/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers-apps-in-toss.toss.im/documentation/api-and-sdk-en/unity/build/build-customization.md).

# Build Customization

Explains how to modify the web layer that wraps the mini app (HTML, TypeScript, npm dependencies, Vite settings) in a way that survives SDK updates.

### What to touch

The build is split into the step where Unity creates the WebGL output and the step where that output is wrapped into a web project and packaged. The internal behavior is [Build pipeline](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-process)in, and here **the location the user edits**only.

| Step              | Output                           | Edit point                                                                                                                      |
| ----------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Unity WebGL build | `webgl/` (intermediate output)   | Do not edit. The settings are [Build profile](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-profiles) |
| Granite packaging | `ait-build/` → `ait-build/dist/` | `Assets/WebGLTemplates/AITTemplate/` subfolder — this document                                                                  |

> **Caution**: `webgl/`and `ait-build/`files should not be edited directly. `webgl/`is intermediate output that Unity recreates on every build, and packaging operates based on `Assets/WebGLTemplates/AITTemplate/`the template, not this folder. Even if you modify both folders, the final package will not reflect those changes and they will disappear on the next build.

> **Note**: The final package used by QR testing and actual deployment is `ait-build/dist/`. When you open the build result directly, look in this folder.

### User area markers

The SDK template is merged with the latest SDK version every time a build starts. At that time **only the content between the markers is preserved**and anything outside the markers is updated with SDK values. Where and when merging happens for each file is described in [Build pipeline](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-process)the template merge timing section.

#### HTML markers

`index.html`provides two areas.

```html
<!-- USER_HEAD_START - Add your custom scripts/styles in this area -->
<!-- USER_HEAD_END -->

<!-- USER_BODY_END_START - Add your custom scripts in this area -->
<!-- USER_BODY_END_END -->
```

`USER_HEAD`is `inside <head>,` and `USER_BODY_END`is `just before </body>.` is inserted.

#### TypeScript config file markers

`vite.config.ts`, `granite.config.ts`, `apps-in-toss.config.ts`use the same pair of markers.

```typescript
//// SDK_GENERATED_START - DO NOT EDIT THIS SECTION ////
// Code managed by the SDK. Anything written here will disappear when the SDK is updated.
//// SDK_GENERATED_END ////

//// USER_CONFIG_START ////
// Custom user code. Preserved when the SDK is updated.
//// USER_CONFIG_END ////
```

> **Important**: `USER_CONFIG`If SDK-managed settings (app name, brand, permissions, `webViewProps` etc.) are redeclared here, the SDK values win during merge, so it has no effect. The build is fine, but the following warning appears — remove the relevant key from `USER_CONFIG`.
>
> ```
> [AIT]   ⚠ SDK-managed settings remain in USER_CONFIG of apps-in-toss.config.ts.
> ```

Conversely `SDK_GENERATED` If an unreplaced placeholder remains in the area, the build stops with a hard error. In that case, recreate the template with Clean Build.

### Customizable files

**All files are under `Assets/WebGLTemplates/AITTemplate/` .**

| File                                  | Role                               | Merge method                                                                                                                       |
| ------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `index.html`                          | HTML entry point                   | `USER_HEAD` / `USER_BODY_END` Preserve marker area                                                                                 |
| `BuildConfig~/package.json`           | npm dependencies                   | Merge dependencies / devDependencies (SDK takes precedence on conflicts)                                                           |
| `BuildConfig~/vite.config.ts`         | Vite build settings                | `USER_CONFIG` Preserve marker area                                                                                                 |
| `BuildConfig~/granite.config.ts`      | Granite packaging settings (2.x)   | `USER_CONFIG` Preserve marker area                                                                                                 |
| `BuildConfig~/apps-in-toss.config.ts` | Apps in Toss settings (3.x)        | `USER_CONFIG` Preserve marker area. If empty, `granite.config.ts`of `USER_CONFIG`automatically migrates                            |
| `BuildConfig~/tsconfig.json`          | TypeScript compiler settings       | SDK-required options(`moduleResolution`, `esModuleInterop`) are forced to SDK values; otherwise the project values take precedence |
| `BuildConfig~/pnpm-workspace.yaml`    | pnpm workspace settings            | If a project file exists, use it; otherwise copy the SDK file                                                                      |
| `BuildConfig~/src/`                   | TypeScript entry point and modules | Preserve the entire folder (recursive copy)                                                                                        |
| `BuildConfig~/` Other files           | `.env`, static assets, etc.        | Copy all root files and subfolders except those in the exclusion list below                                                        |

Things excluded from copying other files — root files `package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`, `vite.config.ts`, `tsconfig.json`, `unity-bridge.ts`, `granite.config.ts`, `apps-in-toss.config.ts` (each has its own merge path) and folders `node_modules/`, `.npm-cache/`, `dist/`.

> **Dependency conflict handling**: If you add a package already declared by the SDK(`@apps-in-toss/web-framework`, `@apps-in-toss/web-analytics`, `vite`, `typescript` etc.) at a different version, the SDK version takes precedence. Packages not declared by the SDK (for example: `firebase`, `canvas-confetti`) are added as-is.

> **Note**: `pnpm-workspace.yaml`exists to exempt`minimumReleaseAge`from pnpm's supply-chain protection ( `@apps-in-toss/*`). Since pnpm reads this setting only from `pnpm-workspace.yaml`, it must be copied to the build directory. Unless you have a special reason, keep the SDK default.

### index.html customization

The file to edit is `Assets/WebGLTemplates/AITTemplate/index.html`. **It must be added `_START`and `_END` between the markers**to be preserved.

`USER_HEAD`is used to declare static resources such as meta tags, fonts, preload hints, and external stylesheets.

```html
<!-- USER_HEAD_START - Add your custom scripts/styles in this area -->
<meta name="theme-color" content="#3182f6">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR&display=swap">
<!-- USER_HEAD_END -->
```

`USER_BODY_END`is used to reference the entry point of user code. The recommended pattern is to load the TypeScript entry point as a module — everything imported in the entry point is bundled into a single bundle through Vite's tree shaking and minification.

```html
<!-- USER_BODY_END_START - Add your custom scripts in this area -->
<script type="module" src="./src/main.ts"></script>
<!-- USER_BODY_END_END -->
```

After the build finishes, `ait-build/index.html`you can open it and check whether the code you wrote was included. If the following appears in the Unity Console, the merge worked correctly.

```
[AIT] index.html USER_HEAD section merged
[AIT] index.html USER_BODY_END section merged
```

### TypeScript entry point

User code is written with `BuildConfig~/src/main.ts`as the entry point. Since Vite bundles this file, npm package imports, tree shaking, and type checking all apply.

```
Assets/WebGLTemplates/AITTemplate/
├── index.html                    ← references main.ts from USER_BODY_END
└── BuildConfig~/
    ├── package.json              ← dependencies
    ├── tsconfig.json             ← TypeScript options (optional)
    └── src/
        └── main.ts               ← entry point
```

`BuildConfig~/src/main.ts`:

```ts
window.addEventListener('load', () => {
    console.log('User entry loaded');
});
```

`BuildConfig~/tsconfig.json`If you put it here, you can customize compiler options. SDK-required options (`moduleResolution`, `esModuleInterop`) are forced to SDK values.

```json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "paths": {
      "@/*": ["./src/*"]
    },
    "baseUrl": "."
  },
  "include": ["src", "*.ts", "*.tsx"]
}
```

### Add external libraries

We recommend installing them as npm packages and importing them from the entry point. Versions are pinned, ensuring build reproducibility; you are not affected by CDN outages or network blocking; and tree shaking and minification are applied.

The procedure is the same regardless of the library — `package.json`add dependency to `main.ts`→ import from `index.html`→ reference the entry point in. See the **tutorial** below for a concrete example.

#### Alternative: load directly from a CDN

If you just want to try it quickly without build tools, `USER_HEAD`to `<script src="...">`you can load it directly with . However, if the CDN fails the app cannot load, versions are baked into the URL so reproducibility is worse, and you do not get tree shaking or type checking. It is not recommended for everyday use.

```html
<!-- USER_HEAD_START -->
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/dist/confetti.browser.min.js"></script>
<!-- USER_HEAD_END -->
```

```html
<!-- USER_BODY_END_START -->
<script>
    window.addEventListener('load', () => {
        confetti({ particleCount: 100, spread: 70, origin: { y: 0.6 } });
    });
</script>
<!-- USER_BODY_END_END -->
```

### Customize Vite settings

`BuildConfig~/vite.config.ts`of `USER_CONFIG` Add plugins or build options in the section.

```typescript
//// USER_CONFIG_START ////
const userConfig = defineConfig({
  plugins: [
    // Add user plugins
  ],
  define: {
    __CUSTOM_FLAG__: JSON.stringify(true),
  },
});
//// USER_CONFIG_END ////
```

`granite.config.ts`and `apps-in-toss.config.ts`also provides the same `USER_CONFIG` section.

### Using React components

To implement a UI overlay with React, add React dependencies and a Vite plugin to the external library addition and TypeScript entry point flow.

`BuildConfig~/package.json`:

```json
{
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.0.0"
  }
}
```

`BuildConfig~/tsconfig.json`:

```json
{
  "compilerOptions": {
    "jsx": "react-jsx"
  },
  "include": ["src"]
}
```

`BuildConfig~/vite.config.ts`:

```typescript
//// USER_CONFIG_START ////
import react from '@vitejs/plugin-react';

const userConfig = defineConfig({
  plugins: [react()],
});
//// USER_CONFIG_END ////
```

`BuildConfig~/src/main.tsx`:

```tsx
import React from 'react';
import { createRoot } from 'react-dom/client';

function GameUI() {
  return <div id="game-ui">Game UI</div>;
}

const container = document.getElementById('ui-root');
if (container) {
  createRoot(container).render(<GameUI />);
}
```

`index.html`:

```html
<!-- USER_BODY_END_START -->
<script type="module" src="./src/main.tsx"></script>
<!-- USER_BODY_END_END -->
```

### Build output structure

After packaging finishes, `ait-build/`the following structure is created in

```
ait-build/
├── index.html              ← Unity placeholder substitution + USER_HEAD/USER_BODY_END merge
├── public/
│   ├── Build/              ← Unity WebGL build files
│   ├── TemplateData/       ← styles, images
│   ├── Runtime/            ← additional scripts such as debug console
│   └── StreamingAssets/    ← StreamingAssets (if present)
├── src/                    ← user TypeScript code (if present)
├── .env                    ← user environment variables (if present)
├── package.json            ← SDK + user dependencies merged (SDK takes precedence on conflicts)
├── vite.config.ts          ← latest SDK version + USER_CONFIG preserved
├── granite.config.ts       ← app metadata placeholder substitution + USER_CONFIG preserved
├── apps-in-toss.config.ts  ← 3.x settings (only when present in the SDK template)
├── tsconfig.json           ← SDK-required options + user options merged
├── pnpm-workspace.yaml     ← project file takes precedence, otherwise SDK file
├── pnpm-lock.yaml          ← project lockfile (consistency check) or SDK fallback
└── dist/                   ← final deployment package (granite build result, QR test target)
```

`node_modules`and `pnpm-lock.yaml`is preserved even during rebuilds, improving build speed.

### Behavior on SDK update

Even if you update the SDK, your customizations are automatically preserved.

| Situation                      | Behavior                                                                     |
| ------------------------------ | ---------------------------------------------------------------------------- |
| Template with markers          | Preserve user area, update only SDK area                                     |
| Older template without markers | Replace the entire file with the new SDK template + manual migration warning |

Existing without markers `index.html`is fully replaced and the following warning is shown. Move the custom parts of the backed-up old file into the marker area of the new template.

```
[AIT] Template updated: replacing the old version template with the new marker-based template.
[AIT] ⚠️ If the existing index.html had custom modifications, manually reapply them to the USER_* marker area.
```

When merged successfully, logs like the following are left.

```
[AIT] ✓ index.html template updated (user custom area preserved)
[AIT]   ✓ vite.config.ts (latest SDK version + USER_CONFIG preserved)
[AIT]   ✓ granite.config.ts (latest SDK version + USER_CONFIG preserved)
```

### tutorial

The two tutorials below (#1 canvas-confetti, #2 Firebase Analytics) are actually built by E2E tests and verified in a browser. The code blocks are exactly in the form the tests expect, so it is safer to follow them as-is first and change them afterward.

#### Add screen effects with canvas-confetti

[canvas-confetti](https://github.com/catdad/canvas-confetti)The simplest example of bundling it to show a confetti effect on page load. You can learn the whole external library addition flow at once.

**1. `BuildConfig~/package.json`Add dependency to**

```json
{
  "dependencies": {
    "canvas-confetti": "^1.9.3"
  },
  "devDependencies": {
    "@types/canvas-confetti": "^1.6.4"
  }
}
```

**2. `BuildConfig~/src/main.ts` Write**

```ts
import confetti from 'canvas-confetti';

window.addEventListener('load', () => {
    confetti({ particleCount: 100, spread: 70, origin: { y: 0.6 } });
});
```

**3. `index.html`reference the entry point in**

```html
<!-- USER_BODY_END_START -->
<script type="module" src="./src/main.ts"></script>
<!-- USER_BODY_END_END -->
```

**4. Check after build**

If you run the build and open the output in the browser, confetti bursts onto the screen immediately after the page loads. If you see `confetti is not defined`in the console, recheck the entry point reference or `package.json` dependency addition step.

#### Firebase Analytics integration

Bundle the Firebase Web SDK ([Modular SDK](https://firebase.google.com/docs/web/modular-upgrade)) to connect app initialization and Analytics. Inject the API key via `.env`— this keeps the key out of code commits and lets you use different values per environment.

**1. `BuildConfig~/package.json`Add dependency to**

```json
{
  "dependencies": {
    "firebase": "^10.7.0"
  }
}
```

**2. `Assets/WebGLTemplates/AITTemplate/BuildConfig~/.env` Write**

```bash
VITE_FIREBASE_API_KEY=your-api-key
VITE_FIREBASE_PROJECT_ID=your-project-id
VITE_FIREBASE_APP_ID=your-app-id
VITE_FIREBASE_MEASUREMENT_ID=your-measurement-id
```

This file is automatically copied to `ait-build/.env`during the build and used by Vite.

> Vite `VITE_` only exposes environment variables with this prefix to the client bundle. If you use a different prefix, `import.meta.env`cannot read them.
>
> **`.gitignore` Configuration**: `.env`contains secret keys, so add both of the following paths to ignore. A common place for shared team defaults is `.env.example`.
>
> ```gitignore
> # Original written by the user (Unity project)
> Assets/WebGLTemplates/AITTemplate/BuildConfig~/.env
>
> # Build output (no separate addition needed if the entire ait-build/ is already ignored)
> ait-build/.env
> ```

**3. `BuildConfig~/src/main.ts` Write**

```ts
import { initializeApp } from 'firebase/app';
import { getAnalytics } from 'firebase/analytics';

const app = initializeApp({
    apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
    projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
    appId: import.meta.env.VITE_FIREBASE_APP_ID,
    measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID,
});
getAnalytics(app);
```

**4. `index.html`reference the entry point in**

```html
<!-- USER_BODY_END_START -->
<script type="module" src="./src/main.ts"></script>
<!-- USER_BODY_END_END -->
```

**5. Check after build**

In the browser developer tools console, you can check the following.

```js
> getApp().options.projectId
"your-project-id"
```

You can also verify real-time event reception in Firebase Console's Analytics > DebugView (debug mode must be enabled — [official documentation](https://firebase.google.com/docs/analytics/debugview) See).

> **To apply the two tutorials together**: `package.json`add both dependencies to `main.ts`place the two import blocks in order in. One entry point (`src/main.ts`) is sufficient.

### Related documents

* [Build pipeline](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-process) — where merging and replacement actually happen
* [Build profile](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-profiles) — Unity WebGL build settings
* [Loading screen customization](https://developers-apps-in-toss.toss.im/documentation/unity/build/loading-screen-customization) — replacing the loading screen
* [Get started](https://developers-apps-in-toss.toss.im/documentation/unity/first-steps/getting-started) — installation and basic setup
* [Troubleshooting](https://developers-apps-in-toss.toss.im/documentation/unity/first-steps/faq) — when the build gets stuck


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://developers-apps-in-toss.toss.im/documentation/api-and-sdk-en/unity/build/build-customization.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
