# Architecture webavatar is an npm-workspaces monorepo providing the core infrastructure future webavatar client projects would otherwise reimplement from scratch — session management, auth tokens, and the realtime model connection — as independently-versioned packages, plus a SvelteKit template to branch client projects from. See `docs/MONOREPO_MIGRATION_BRIEF.md` for the rationale and history behind this structure (converted from a single-app template where the same code lived under the app's own `src/lib/`). ## Module layering Three top-level directories, each a separate npm workspace: - **`packages/core/`** (`@webavatar/webavatar-core`) — infrastructure every branched client project needs: session orchestration, provider adapters, token minting/verification, the WS relay, and the Vite dev-server integration. Published as a normal (privately-distributed) npm package with curated entry points: - **`.`** (root) — shared, isomorphic contracts (types, protocols, constants: `SessionPersona`, the `ClientEvent`/`ServerEvent` protocol, `Logger`, `ToolDefinition`). Free of SvelteKit-specific magic (no `$env/*`, no server-only imports) and no Node-only APIs, so it's safe for both browser and server code to import. - **`./client`** — thin browser-side transports and wrappers (`RealtimeClient`, `ResilientRealtimeClient`, client logging, tool dispatch). No business logic; just transport. - **`./server`** — server-only implementations (session orchestration, provider adapters, token handling, `handleRealtimeUpgrade`) plus the `createRealtimeRuntime` composition-root factory (see "Composition roots" below). Maps to a throwing stub for any consumer's browser bundle (the package's `browser` export condition) — see "Server-only safety" below. - **`./vite`** — the dev-server integration (`realtimeDevWsPlugin`), kept separate from `./server` so importing the package root never pulls in Vite. - **`packages/humanoid/`** (`@webavatar/webavatar-humanoid`) — an **optional package**: infrastructure only _some_ client projects need (one building a 2D avatar has no reason to even look at it). A renderer-agnostic avatar-state and animation-channel-composition contract for 3D humanoid avatars rigged with ARKit blend shapes. See [HUMANOID.md](HUMANOID.md). May depend on core's public contracts, but **only as types** (`import type`) — never a runtime value, never core's internal file layout — so its built JavaScript contains no reference to core at all; core is declared as a peer dependency purely so npm can report a version-compatibility error at install time. Entry points: **`.`** (state/animation contracts), **`./client`** (the browser wrapper deriving live state), **`./eye`** (the first animation library built as a consumer of the root contract — gaze animation), **`./eye/client`** (its browser wrapper). Future animation libraries (blink, lip-sync, body/gesture) are expected to nest the same way, as `./` / `.//client` sub-packages, rather than becoming further top-level packages. - **`apps/template/`** — the SvelteKit application template client projects branch from. Depends on both packages; nothing in either package imports from it. Within it, a further split mirrors what used to be the whole app: - `src/lib/project/`, `src/lib/server/project/`, `src/lib/client/project/` — code specific to whichever client project branches from the template (tool implementations, custom instructions, persona content). **Neither package imports from project code, only the reverse** — that one-way dependency is what let this code stay branch-friendly even before the package split, and still does. Where the app needs to merge something from a package with project content (e.g. core session defaults with a project's tool registry), that merge happens at a composition root (`server.js`, `vite.config.ts`) — never inside a package. See [TOOLS.md](TOOLS.md) for a concrete example of this split in practice. - `src/lib/server/webavatar-core.ts` — the server-only wrapper (`export * from '@webavatar/webavatar-core/server'`) that SvelteKit routes and server modules import through instead of the package directly, so SvelteKit's own `$lib/server` build-time guard (which is path-based and doesn't reach into `node_modules`) still covers them. See "Server-only safety" below for the full picture. - `src/routes/` — thin route glue; delegates into the packages and project code rather than holding logic itself. - `src/routes/demo/` — verification scaffolding (the `/demo/realtime` and `/demo/humanoid` smoke-test surfaces referenced throughout these docs and in [AGENTS.md](../AGENTS.md)'s "Verifying changes"), not a client project's own UI, and a branched project has no reason to touch it. Rather than needing to be deleted per fork, it's gated to dev-only via `src/routes/demo/+layout.server.ts` (404s when `!dev`) — it stays available for `npm run dev` verification but never ships in a production build. - Three.js, the bundled test avatar (`static/models/avatar.glb`, tracked via Git LFS), and all rig/morph-target binding stay entirely app-owned — neither package has any renderer knowledge. ## Server-only safety Now that core's server code is a package rather than a path SvelteKit's own build-time guard can see, that protection is rebuilt in layers, all required together: (1) `packages/core`'s `browser` export condition maps `./server` and `./vite` to a stub that throws when evaluated, for every consumer, in any project, automatically; (2) `apps/template/src/lib/server/webavatar-core.ts`, so SvelteKit's own `$lib/server` guard covers all app code that goes through it; (3) an ESLint `@typescript-eslint/no-restricted-imports` rule forbidding `@webavatar/webavatar-core/server`/`/vite` everywhere in the app except `src/lib/server/**`, `server.js`, and `vite.config.ts`; (4) a build-time `serverOnlyGuard()` Vite plugin (`apps/template/server-only-guard.ts`) that fails a client-side import at build time rather than only at runtime in the browser. ## Composition roots `server.js` (production) and the `./vite` plugin (dev) both need the same wiring: load config, select the provider, choose the analytics recorder, resolve session defaults from the persona and tool registry, then hand everything to the WS upgrade handler. Both call `createRealtimeRuntime` (exported from `@webavatar/webavatar-core/server`) — a factory, not a service locator: it returns a fresh instance per call, keeps no module state of its own, and is called once per process by whichever composition root is running. This is what keeps `server.js` and the Vite plugin thin: a core change to how the relay is assembled ships inside a core package release, not as a shell edit every client project has to apply by hand. ## Foundational infrastructure decisions - **This app runs as a persistent Node process, not serverless.** The adapter is `@sveltejs/adapter-node` — configured inline in [`apps/template/vite.config.ts`](../apps/template/vite.config.ts)'s `sveltekit({ adapter: ... })` option, since the template has no separate `svelte.config.js` — started via a custom [`server.js`](../apps/template/server.js) rather than adapter-node's default generated entrypoint. This is a hard requirement, not a preference: the realtime session relay holds a WebSocket connection to a browser and a second WebSocket connection to the model provider simultaneously, for the lifetime of a conversation. No serverless/edge adapter can hold connections like that. Any client project branching from this template inherits that constraint — plan hosting accordingly. - **Auth tokens are generic infrastructure, not tied to any one feature.** `packages/core/src/server/tokens.ts` mints/verifies short-lived HMAC-signed tokens as a reusable primitive, currently used to gate the realtime WebSocket handshake but intentionally not coupled to it. - **Artifact parity.** Vite externalizes an SSR dependency only if it resolves inside `node_modules` — a workspace package is a symlink whose real path is `packages/core/...`, so without `ssr.external` in `apps/template/vite.config.ts` (production builds only), the monorepo would bundle core's server code into the SvelteKit server build while a client project that installed the published package gets it externalized, running a different artifact than this template does. Not applied in `dev`, since externalizing there would load the package through Node's module cache and lose hot reload of package changes for SSR routes. ## Subsystems | Subsystem | Doc | | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | | Realtime session management (OpenAI Realtime API today, extensible to other models/pipelines; client resilience/reconnect; the mock provider for offline dev) | [REALTIME_SESSION.md](REALTIME_SESSION.md) | | Tool calling (server- and client-executed) and the core/project ownership boundary | [TOOLS.md](TOOLS.md) | | Analytics recording (sessions, messages, tool calls, errors) to a local SQLite database | [ANALYTICS.md](ANALYTICS.md) | | Configuration (env vars, validation, the core/project split for persona content) | [CONFIGURATION.md](CONFIGURATION.md) | | `humanoid` package — avatar-state derivation and animation-channel composition for humanoid avatars | [HUMANOID.md](HUMANOID.md) | | Ephemeral, in-memory debug/logging system (open categories, level filtering, session-scoped WS forwarding to a client debug panel) — distinct from analytics | [LOGGING.md](LOGGING.md) | Add a row here (and a corresponding doc) for each new subsystem as it's built — see the "Documentation" section in [AGENTS.md](../AGENTS.md) for when and how.