6.8 KiB
Architecture
webavatar-core is a base SvelteKit project meant to be branched off for future webavatar client projects. Its job is to provide the core infrastructure those projects would otherwise reimplement from scratch — session management, auth tokens, and the realtime model connection — as a clean, extensible foundation rather than a finished product.
Module layering
Code that's part of this core infrastructure lives under src/lib/ in three places, split by where it's allowed to run:
src/lib/core/— shared, isomorphic contracts (types, protocols, constants). Importable by both browser and server code. Must stay free of SvelteKit-specific magic (no$env/*, no server-only imports) so it can also be compiled standalone outside Vite when needed (see REALTIME_SESSION.md for why that matters).src/lib/server/core/— server-only implementations: session orchestration, provider adapters, token minting/verification, raw HTTP upgrade handling. SvelteKit enforces this boundary at build time — browser code cannot import from here, even transitively.src/lib/client/core/— thin browser-side wrappers around the shared contracts. No business logic; just transport.
This repo is a base template meant to be branched per client project, so there's a second, parallel set of directories for code that's specific to whichever project branches from it — src/lib/project/, src/lib/server/project/, src/lib/client/project/, mirroring the three-way split above. Core never imports from project, only the reverse — that one-way dependency is what lets a branched project pull template updates without merge conflicts in core files. Where a project needs something from both (e.g. merging core session defaults with a project's tool registry), that merge happens at a composition root — a repo-root file like server.js or vite.config.ts, not under src/lib and not bound by the core/project layering — never inside a core file. See TOOLS.md for a concrete example of this split in practice.
Optional packages
A third kind of top-level directory, alongside core and project: a package — a named directory mirrored across only the scopes it actually needs (src/lib/<name>/ for isomorphic code, src/lib/server/<name>/, src/lib/client/<name>/). Where core is infrastructure every branched project needs and project is that project's own content, a package is infrastructure only some projects need — one a 2D avatar project, say, has no reason to even look at.
Same dependency rule as core/project, extended: a package may import from core; core and project never import from a package; a project opts in by importing directly from the package — no registration step, no runtime flag, no manifest. If nothing imports it, it's never bundled — that's what makes it free for a project that doesn't use it.
First (and, as of this writing, only) package: humanoid (src/lib/humanoid/, src/lib/client/humanoid/) — a renderer-agnostic avatar-state and animation-channel-composition contract for 3D humanoid avatars rigged with ARKit blend shapes. See HUMANOID.md. Shared animation libraries built as consumers of that contract live as sub-packages nested inside it ($lib/humanoid/<name>, $lib/client/humanoid/<name>) rather than as further top-level packages — eye (gaze animation) is the first; blink, lip-sync, and body/gesture are expected to follow the same way, not to be folded into core.
Routes (src/routes/) stay thin and delegate into these — a route file is glue, not where logic lives.
src/routes/demo/ is the one exception worth calling out: it's core-owned verification scaffolding (the /demo/realtime and /demo/humanoid smoke-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 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.
Foundational infrastructure decisions
- This app runs as a persistent Node process, not serverless. The adapter is
@sveltejs/adapter-node— configured inline invite.config.ts'ssveltekit({ adapter: ... })option, since this project 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 base inherits that constraint — plan hosting accordingly. - Auth tokens are generic infrastructure, not tied to any one feature.
src/lib/server/core/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.
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 |
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.