10 KiB
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, theClientEvent/ServerEventprotocol,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 thecreateRealtimeRuntimecomposition-root factory (see "Composition roots" below). Maps to a throwing stub for any consumer's browser bundle (the package'sbrowserexport condition) — see "Server-only safety" below../vite— the dev-server integration (realtimeDevWsPlugin), kept separate from./serverso 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. 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./<name>/./<name>/clientsub-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 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/serverbuild-time guard (which is path-based and doesn't reach intonode_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/realtimeand/demo/humanoidsmoke-test surfaces referenced throughout these docs and in 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 viasrc/routes/demo/+layout.server.ts(404s when!dev) — it stays available fornpm run devverification 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 inapps/template/vite.config.ts'ssveltekit({ adapter: ... })option, since the template has no separatesvelte.config.js— started via a customserver.jsrather 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.tsmints/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 ispackages/core/..., so withoutssr.externalinapps/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 indev, 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 |
| Tool calling (server- and client-executed) and the core/project ownership boundary | TOOLS.md |
| Analytics recording (sessions, messages, tool calls, errors) to a local SQLite database | ANALYTICS.md |
| Configuration (env vars, validation, the core/project split for persona content) | CONFIGURATION.md |
humanoid package — avatar-state derivation and animation-channel composition for humanoid avatars |
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 |
Add a row here (and a corresponding doc) for each new subsystem as it's built — see the "Documentation" section in AGENTS.md for when and how.