> 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 wrapping the mini app (HTML, TypeScript, npm dependencies, Vite configuration) so that it survives SDK updates.

### What to touch

The build is divided into the step where Unity creates the WebGL output and the step where that output is wrapped and packaged as a web project. The internal behavior is [the build pipeline](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-process)in **locations that users can edit**only.

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

> **Note**: `webgl/`and `ait-build/`files. Do not edit them directly. `webgl/`is an intermediate output that Unity recreates on every build, and packaging works based on the template in `Assets/WebGLTemplates/AITTemplate/`rather than this folder. Even if you modify both folders, the changes will not be reflected in the final package and will disappear on the next build.

> **Note**: The final package used by QR tests and real deployment is `ait-build/dist/`. When you want to inspect the build result directly, look in this folder.

### User area markers

The SDK template is merged with the latest SDK version every time the build is entered. At that time, **only the content between the markers is preserved**and everything outside the markers is updated with SDK values. Where and when merging happens for each file is described in [the 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>,` is inserted into `USER_BODY_END`is `</body>` right before.

#### TypeScript config file markers

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

```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 ////
// User custom code. Preserved when the SDK is updated.
//// USER_CONFIG_END ////
```

> **Important**: `USER_CONFIG`If you redeclare SDK-managed settings (app name, brand, permissions, `webViewProps` etc.) here, the SDK values win during merging, so it has no effect. The build is fine, but the following warning appears — remove the relevant keys from `USER_CONFIG`.
>
> ```
> [AIT]   ⚠ SDK-managed settings remain in USER_CONFIG in 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 a 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 in case of 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`the `USER_CONFIG`automatically migrates                |
| `BuildConfig~/tsconfig.json`          | TypeScript compiler settings       | SDK-required options(`moduleResolution`, `esModuleInterop`) are forced to SDK values; the rest use project values first |
| `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 as-is                                       |

Items 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.) in a different version, the SDK version takes precedence. Packages not declared by the SDK (e.g. `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. If you have no special reason, leave the SDK default as-is.

### Customizing index.html

The file to modify is `Assets/WebGLTemplates/AITTemplate/index.html`. **Be sure to add it `_START`and `_END` between the markers**for it 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 — all imports written in the entry point are 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 your code 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 `BuildConfig~/src/main.ts`written with this as the entry point. Since Vite bundles this file, npm package imports, tree shaking, and type checking are all applied.

```
Assets/WebGLTemplates/AITTemplate/
├── index.html                    ← main.ts referenced 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 place this, 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"]
}
```

### Adding external libraries

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

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

#### Alternative: load directly from a CDN

If you just want to quickly try something without build tools, you can load it directly with `USER_HEAD`are set to `<script src="...">`. However, if the CDN fails, app loading fails; the version is embedded in the URL, so reproducibility is poor; and you do not get tree shaking or type checking. 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 -->
```

### Customizing Vite settings

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

```typescript
//// USER_CONFIG_START ////
const userConfig = defineConfig({
  plugins: [
    // Add user plugin
  ],
  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 flow and the 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 replacement + 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 any)
├── src/                    ← user TypeScript code (if any)
├── .env                    ← user environment variables (if any)
├── package.json            ← SDK + user dependency merge (SDK takes precedence in case of conflicts)
├── vite.config.ts          ← latest SDK version + USER_CONFIG preserved
├── granite.config.ts       ← app metadata placeholder replacement + 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 first; if absent, 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 when rebuilding, improving build speed.

### Behavior when updating the SDK

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

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

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

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

If merged successfully, the following logs are left behind.

```
[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 and run in the browser by E2E tests for verification. The code blocks are exactly in the form the tests expect, so it is safer to follow them as-is first and then modify them later.

#### Add screen effects with canvas-confetti

[canvas-confetti](https://github.com/catdad/canvas-confetti)This is the simplest example of bundling it to show a confetti effect when the page loads. You can learn the entire flow for adding an external library 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 result in a browser, confetti bursts onto the screen right after the page loads. If you see `confetti is not defined`in the console, check the entry point reference or `package.json` the dependency addition step again.

#### Integrating Firebase Analytics

Firebase Web SDK([Modular SDK](https://firebase.google.com/docs/web/modular-upgrade)) is bundled to connect app initialization and Analytics. The API key is injected via `.env`— this prevents committing keys to the repository 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`at build time and used by Vite.

> Vite exposes only environment variables with the `VITE_` prefix to the client bundle. If you use another prefix, `import.meta.env`cannot read it.
>
> **`.gitignore` Settings**: `.env`contains secret keys, so add both of the following paths to ignore. It is common to put defaults shared by the team in `.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**

You can check the following in the browser developer tools console.

```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 both tutorials together**: `package.json`add both dependencies to, and `main.ts`place the two import blocks in order. A single entry point (`src/main.ts`) is sufficient.

### Related documents

* [the build pipeline](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-process) — where merging and substitution actually happen
* [Build Profiles](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
* [Getting 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 is 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.
