The v10r system is organized as a seven-layer abstraction hierarchy where each layer is built from the one above it. Data flows down from HTTP edge to infrastructure; events and responses bubble back up. This document is the entry-point map for understanding how the whole system fits together — read it before diving into any single layer.
Two physical spines hold all seven layers together: a composition root (src/hooks.server.ts) that owns cross-cutting concerns, and a hexagonal core (src/lib/server/[domain]/) where all business logic lives, insulated from the framework.
The Hierarchy at a Glance
Layer Canonical Home
─────────────────────────────────────────────────────────────────────────────
1 Tech Stack root config (svelte.config.js, vite.config.ts,
uno.config.ts, drizzle.config.ts, package.json)
+ Containerfile.dev / compose.yaml
2 Architecture src/hooks.server.ts (composition root)
+ multi-client-core pattern
server/client boundary = the $lib/server/ path
3 Services/Applications src/routes/[[locale=locale]]/ (app/, admin/, desk/,
auth/, (public)/showcases/)
+ parallel src/routes/api/ tree
4 Modules src/lib/server/[domain]/
+ src/lib/[client-domain]/
public surface = each module's index.ts barrel
5 Components src/lib/components/[layer]/
primitives → composites → layout/shell → feature dirs
6 Classes/Functions files inside a module/component folder;
private unless re-exported from folder's index.ts
7 Code leaf .ts / .svelte / .css;
design tokens at src/lib/styles/tokens.ts + src/app.css
─────────────────────────────────────────────────────────────────────────────
The seven layers collapse into two physical spines:
- Composition root —
src/hooks.server.tswires together Layers 1–3: it boots background modules, runs a 14-stagesequence()ofHandlemiddleware, and hands a fully populatedevent.localsto every route adapter. - Hexagonal core —
src/lib/server/[domain]/houses Layers 4–6: framework-free domain modules that any adapter (UI, REST, AI tool, job) can call without modification.
Layer 1 — Tech Stack
The foundation is declared in root config files and Containerfile.dev; nothing above Layer 1 should care which specific vendors implement it.
Runtime environment
| Concern | Technology |
|---|---|
| Container | Podman (Containerfile.dev, compose.yaml) |
| Runtime | Bun |
| Framework | SvelteKit 2 + Svelte 5 (runes) |
| Host | Vercel (@sveltejs/adapter-vercel, nodejs22.x) |
| Code quality | Biome |
Data stores (polyglot persistence)
| Store | Technology | Role |
|---|---|---|
| Relational | PostgreSQL via Neon serverless (@neondatabase/serverless) |
Universal floor — all CRUD, analytics, jobs |
| ORM | Drizzle (drizzle-orm, drizzle-kit) |
Type-safe SQL |
| Graph | Neo4j Aura (Bolt) | RAG tier-3 graph expansion only |
| Cache / rate-limit | Upstash Redis (@upstash/ratelimit, @upstash/redis) |
Rate limits, AI daily budget, Better Auth secondary storage |
| Object storage | Cloudflare R2 via S3 API (@aws-sdk/client-s3) |
Blog media, avatars |
Application concerns
| Concern | Technology |
|---|---|
| Auth | Better Auth (session-based) |
| Validation | Valibot + Superforms |
| Styling | UnoCSS + Bits UI |
| i18n | Paraglide JS (en / de / ru) |
| AI | Vercel AI SDK v6 (ai@^6, @ai-sdk/{google,groq,openai,svelte}) |
| 3D | Three.js + Threlte |
| Abuse prevention | ALTCHA (altcha, altcha-lib) |
| Visualization | d3 (force/dag/hierarchy/sankey/zoom), chart.js, @xyflow/svelte, maplibre-gl |
| Markdown | unified / remark / rehype / shiki |
Design principles (docs/foundation/principles.md): libraries-over-services, lightweight, standard protocols, free-tier-friendly, svelte-native-first, no-codegen, speed-is-a-feature. The no-codegen constraint is consequential: Better Auth schema is hand-written Drizzle tables rather than auto-generated.
Connects down to Layer 2 by providing the runtime these patterns run in. No Layer 2 code imports the vendor names directly; it imports from wrappers in $lib/server/[domain]/.
Layer 2 — Architecture
Two architectural spines impose order on every request.
Spine A — Composition Root
src/hooks.server.ts is the composition root. Three module-load side effects at the top of the file boot background work once at process start:
import '$lib/server/agents'
import '$lib/server/jobs/scheduler'
import '$lib/server/jobs/delivery-scheduler'
The main export is a sequence() of fourteen Handle middlewares that mutate the shared event.locals bus in order. Each handler writes named fields and, in some cases, short-circuits the chain with a response before downstream handlers run.
| # | Handler | Writes to event.locals |
Short-circuits? | Why this order |
|---|---|---|---|---|
| 1 | securityHeaders |
clientIp; sets x-client-ip |
No | Must be first — auth pins ipAddressHeaders: ['x-client-ip']; attacker-mutable headers are fixed here. Emits the full security-header set (see Security Headers) |
| 2 | bodySizeFloor |
— | 413 when a mutating request declares > MAX_REQUEST_BYTES |
Advisory floor before authHandler consumes /api/auth/* bodies; the enforceable bound is readJsonBounded at the endpoints |
| 3 | stripBaseLocalePrefix |
— | 308 on /en/* paths |
Canonical URL before Paraglide resolves locale |
| 4 | docsMarkdown |
— | 200 markdown on /docs/**.md, 303 on negotiated clean URLs, 308 on locale-prefixed .md |
The agent-facing .md layer. Above loadStyle/i18n so no Set-Cookie ever lands on a cacheable markdown response, and short-circuited responses skip analytics |
| 5 | loadStyle |
style, customPaletteColors, customPaletteAccentOffset |
No | Before i18n, which injects the palette <style> block. Custom-palette DB lookups go through an in-memory TTL cache |
| 6 | i18n (Paraglide) |
locale |
No | Wraps resolve with transformPageChunk to fill %lang%/%palette%/%typography%/%radius% and inject custom-palette CSS |
| 7 | authCaptchaGate |
— | Decision response on captcha/rate-limit fail | Before authHandler — gate must run before Better Auth consumes the request body |
| 8 | authHandler |
— | 429 on rate-limit exceed | Better Auth svelteKitHandler + Upstash rate-limit on /api/auth/* keyed by clientIp |
| 9 | csrfProtection |
— | 403 on mutating /api/* without X-Requested-With or mismatched origin |
Exempt: /api/auth/, /api/cron/, /api/webhooks/, /api/analytics/journey, /api/mcp/ |
| 10 | sessionPopulate |
user, session, grants, authDegraded? |
No | Must run AFTER authHandler (Better Auth #2188: svelteKitHandler does not populate locals). Fast-path skips DB if session cookie absent. A DB failure degrades the request to anonymous (authDegraded: true) instead of 500ing every session-carrying page |
| 11 | consentLoader |
consentTier (default 'necessary') |
No | Before route handlers need consent tier |
| 12 | debugOwnerLoader |
debugOwnerId |
No | Verifies v10r_debug_owner HMAC cookie; fail-closed; independent of Better Auth |
| 13 | devRouteGuard |
— | 404 on (dev) routes outside DEV |
— |
| 14 | analyticsCollector |
— | No | Last; consumes consentTier + debugOwnerId; fire-and-forget post-resolve. The _v10r_sid cookie is consent-gated (TDDDG §25 / ePrivacy Art 5(3)): set only at analytics+ tier; at necessary it deletes any stale cookie and falls back to a cookieless daily session id |
The terminating error handler handleError mints an errorId (crypto.randomUUID()), emits one structured JSON log line, and returns { message, errorId } to the client — never raw error details.
Security Headers set
securityHeaders (handler #1) writes these on every response (wrapped in try/catch — redirect responses have immutable headers):
| Header | Value | Notes |
|---|---|---|
X-Frame-Options |
DENY |
|
X-Content-Type-Options |
nosniff |
|
Referrer-Policy |
strict-origin-when-cross-origin |
|
Permissions-Policy |
camera=(), microphone=(), geolocation=() |
|
Strict-Transport-Security |
max-age=…; includeSubDomains; preload |
|
Cross-Origin-Opener-Policy |
same-origin-allow-popups |
Popups (OAuth) still work |
Cross-Origin-Resource-Policy |
same-site |
|
X-DNS-Prefetch-Control |
off |
|
Cache-Control |
no-store, private |
Conditional — only on authed responses or any /api/ path, and only via !response.headers.has('Cache-Control') so an explicit per-route setter wins |
Clear-Site-Data |
"cache","cookies","storage" |
Conditional — only on a successful /api/auth/sign-out |
CSP itself is configured in svelte.config.js (nonce/hash mode), not here.
Extracted handler helpers
Several handler predicates were lifted into framework-free modules the hook imports — behavior unchanged, now unit-testable in isolation:
| Module | Exports | Used by |
|---|---|---|
$lib/server/security/csrf.ts |
needsCsrf(method, path), isSameHost(), CSRF_EXEMPT_PREFIXES |
csrfProtection (handler #9) |
$lib/styles/random/palette-sanitize.ts |
VALID_TOKEN_KEYS, OKLCH_RE, safeEntries |
loadStyle / i18n palette CSS injection (#3–4) |
$lib/server/auth/step-up.ts |
twoFactorVerifyLimitKey() |
per-account 2FA verify limiter in authHandler (#8) |
Spine B — Hexagonal Multi-Client Core
All business logic lives in $lib/server/[domain]/. Thin adapters wrap it for each client type. The domain modules know nothing about the framework.
┌──────────────────────────────────────────────────────────────┐
│ ADAPTERS │
│ │
│ +page.server.ts +server.ts AI tools jobs/ │
│ (form actions, (REST API, (tool (cron, │
│ load fns) SSE) wrappers) sched.) │
└──────┬───────────────┬───────────┬──────────────┬───────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ DOMAIN MODULES │
│ $lib/server/[domain]/ │
│ │
│ notifications/ auth/ rawrag/ llmwiki/ │
│ ├── index.ts ├── index.ts ├── index.ts ├── search.ts │
│ ├── service.ts └── guards.ts └── ... └── ... │
│ └── ... │
│ │
│ db/[domain]/ │
│ ├── queries.ts (reads — no side effects) │
│ └── mutations.ts (writes — explicit intent) │
└──────────────────────────┬───────────────┬───────────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌────────────────────────────────────┐
│ PostgreSQL │ │ Neo4j │ Redis │ R2 │
│ (Drizzle ORM) │ │ (graph)│ (cache) │ (storage) │
└──────────────────────┘ └────────────────────────────────────┘
The four invariants — violations break cross-client reuse:
- No framework imports in domain modules. No
@sveltejs/kitor$app/imports inside$lib/server/[domain]/. These bind logic to the SvelteKit request cycle, preventing reuse by AI tools and jobs. - Date serialization happens in the adapter layer. Domain modules return
Dateobjects as-is. The route or toolexecuteconverts them to ISO strings. - SvelteKit response helpers (
redirect,error,fail,message) only in adapters. Never in domain modules. - Domains call down, not across. Cross-domain reads go through the other domain's
index.tsbarrel only — never into its internals.
Server/client boundary: the $lib/server/ path itself. SvelteKit refuses to bundle it client-side. No runtime guard is needed; the path is the boundary.
Error spine: ServerError base class (src/lib/server/errors/index.ts) with kind / toStatus() / toJSON(). Subclasses: DbError (maps PG SQLSTATE → safe message + HTTP status), AIError, Neo4jError, LlmwikiError. Each adapter translates: REST endpoints return apiError(status, kind, safeMessage); AI stream tools return structured error objects (never throw); form actions use fail(); jobs capture into JobResult. Safe messages only — no PG codes, constraint names, or API-key prefixes reach the client.
Layer 3 — Services / Applications
Route areas under src/routes/[[locale=locale]]/ and the parallel src/routes/api/ tree define the bounded contexts. Each context owns a route area, an API group, and primary server domains.
| Application | Route area | API group | Primary domain | Gate |
|---|---|---|---|---|
| Public + Showcases | (public)/ (blog, docs, showcases, feedback) |
— | — | None; self-documenting layer |
| Auth | auth/ (login, verify) |
/api/auth/* |
auth/ |
ALTCHA-gated, rate-limited |
| App (member) | app/ (dashboard, account, account/data, notifications, settings) |
/api/preferences/*, /api/notifications/*, /api/consent, /api/me/* |
preferences/, notifications/, privacy/ |
app/+layout.server.ts |
| Admin | admin/ (access, ai, analytics, audit, cache, content, db, feedback, flags, jobs, notifications, rag, users — ~13 areas) |
/api/admin/* |
various | admin/+layout.server.ts |
| Desk (AI workspace) | desk/ |
/api/desk/* (files, folders, spreadsheets, theme, workspaces) |
store/, branding/ |
desk/+layout.server.ts |
| Blog | (public)/blog/ |
/api/blog/* (posts, comments, tags, assets, domains, folders, feed.xml) |
blog/, content/ |
Capability-gated authoring |
| AI Assistant | — | /api/ai/* (chat, conversations, proposals, providers) |
ai/ |
Session-gated |
| RAG / Retrieval | — | /api/retrieval/* (documents, graph, ingest, search, stats) |
rawrag/, llmwiki/, graph/ |
Admin-gated |
| Notifications | — | /api/notifications/* (stream SSE, telegram, discord, read-all) |
notifications/ |
Session-gated |
| Analytics | — | /api/analytics/* (journey beacon, stream) |
analytics/ |
Consent-tiered |
| Privacy (GDPR) | app/account/data (transparency mirror) |
/api/me/* (data, data/export, DELETE) |
privacy/ |
Session-gated; per-endpoint rate limits (10/5/3 per min) |
| Pairing | pair/[code] |
/api/pair/* |
pairing/ |
HMAC cookie; independent of Better Auth |
| Jobs | — | /api/cron/[job] dispatcher |
jobs/ |
Bearer token (Vercel cron) |
| Abuse | — | — | abuse/ |
Cross-cutting; wired in hooks |
| Visual identity | (public)/showcases/shell/style |
/api/style/*, /api/desk/theme |
branding/, styles/random/ |
Picking public; palette CRUD session-gated |
Self-documenting showcases: pages under (public)/showcases/ serve simultaneously as documentation, feature tests, and copy templates. If the showcase works, the feature is proven. This is the repo's primary test strategy for UI patterns.
Layer 4 — Modules
src/lib/server/[domain]/ holds ~40 server-side domain modules. src/lib/[client-domain]/ holds client-side state, styles, i18n, schemas, and config. The public surface of every module is its index.ts barrel export — external callers import from the barrel only, never from internal files.
Server-side domains (selected by file count)
| Domain | Files | Role |
|---|---|---|
db/ |
135 | Drizzle client + schema + all read/write query files |
ai/ |
35 | Provider registry, orchestrator, tools, errors, budget |
rawrag/ |
18 | Three-tier retrieval pipeline (tiers/, ingest/) |
notifications/ |
17 | Send, stream (SSE), route, outbox, channel providers |
blog/ |
16 | Posts, comments, tags, assets, feed |
llmwiki/ |
14 | Hybrid vector+BM25 wiki search, compile, lint |
jobs/ |
13 | Runner, scheduler, delivery-scheduler, 8 registered jobs |
store/ |
12 | Desk workspace and file data |
cache/ |
9 | Upstash Redis wrappers |
abuse/ |
9 | ALTCHA, honeypot, rate-limits, AI budget |
Repeating module template
[domain]/
index.ts ← barrel / public API
service.ts ← multi-step orchestration (only when justified by ≥2 concrete consumers)
queries.ts ← reads; no side effects
mutations.ts ← writes; explicit intent
types.ts
config.ts
errors.ts
[feature].ts ← one file per capability
*.test.ts ← co-located unit tests
[sub-pipeline]/ ← e.g. rawrag/tiers/, rawrag/ingest/, ai/tools/, ai/loop/
Deviations: auth/index.ts is the Better Auth instance (construction site, not re-export). ai/index.ts exposes provider-resolver functions. Tiny modules (utils/, errors/, schemas/, feedback/) skip the full structure.
The db/ module — parallel-tree structure
db/ is the most structurally important domain. It contains two co-located trees:
src/lib/server/db/
index.ts ← Drizzle client over Neon serverless Pool
errors.ts ← DbError, classifyDbError
schema/
[domain]/ ← what data IS (Drizzle table definitions)
index.ts ← re-exports every domain schema
relations.ts ← Drizzle relation definitions
[domain]/
queries.ts ← how you read data
mutations.ts ← how you write data
db/index.ts exports the singleton db instance:
neonConfig.poolQueryViaFetch = true; // Bun WebSocket workaround
const pool = new Pool({ connectionString: env.NEON_DATABASE_URL_PROD });
export const db = drizzle(pool, { schema: { ...schema, ...relations } });
Push-only workflow: only db:push is used; no migrations directory exists. All pgSchema() and pgEnum() objects must be exported through schema/index.ts AND listed in drizzle.config.ts schemaFilter (14 namespaces: admin, showcase, image, auth, ai, rag, jobs, notifications, analytics, app, blog, dbops, desk, feedback) or db:push silently omits them.
Client-side modules
| Module | Role |
|---|---|
state/ |
Svelte 5 runes stores (*.svelte.ts) |
styles/ |
tokens.ts design tokens + random/ procedural style engine |
i18n/ |
Runtime locale wrapper |
paraglide/ |
Generated message functions — do not edit |
schemas/ |
Valibot schemas, foldered to mirror routes |
showcases/ |
Showcase registry + sections — the catalog hubs, nav, and search read |
pwa/ |
Service-worker policy (pure) + push / session-refresh clients |
workers/ |
Web Worker payloads — pure compute, browser-API half, postMessage shell |
nav/, shortcuts/, config/ |
App navigation, keyboard shortcuts, app config |
Import direction
The import graph is a verified DAG. Cross-domain references always target barrel roots — never internals. The db/ module imports no sibling domains; it is the sink. Framework coupling is confined to adapter-purpose files (auth/guards.ts, api/response.ts, api/rate-limit.ts, abuse/decision.ts) and scheduler-boot code that reads the building / dev flags. Pure domain modules — db/, rawrag/, blog/, store/, llmwiki/, ai/providers — have zero framework imports.
Layer 5 — Components
src/lib/components/[layer]/ organizes 22 categories in a dependency-direction hierarchy.
Layer order (leaf → root)
primitives/ wrap Bits UI: button, dialog, table, slider, combobox,
tabs, typography, switch, popover, calendar, …
composites/ compose primitives: card, form-field, command-palette,
dropdown-menu, pagination, toast, confirm-dialog, dock,
chatbot, page-header, empty-state, …
layout/ Stack, Cluster, PageContainer
shell/ app chrome: AppShell, Sidebar*, Nav*, UserMenu,
ConsentBanner — depend on composites/primitives/layout
feature dirs/ blog, chat, editor, explorer, spreadsheet, viz, cycle,
io-log, branding, 3d, docs, ui, preview, admin —
depend DOWNWARD on composites/primitives/layout; never
on each other
Public barrel (src/lib/components/index.ts)
export * from './composites';
export * from './layout';
export * from './primitives';
// viz/ excluded — import from '$lib/components/viz' directly
// (avoids bundling Chart.js/Three.js into the default surface)
// shell/ excluded — app-specific chrome, import from '$lib/components/shell'
viz/ and shell/ are intentionally excluded from the default barrel. This is a bundle-size boundary: Chart.js, Three.js, and app-specific chrome should not load on every page.
Design tokens
src/app.css— runtime CSS custom properties (:rootlight,.darkdark). All color tokens live here.src/lib/styles/tokens.ts— build-time token values (breakpoints, fontSize, spacing, borderRadius, zIndex, iconSize) read byuno.config.ts.
Component-First Rule: never use a raw HTML element (<button>, <input>, <select>, <textarea>) when a project component exists. Raw elements bypass the design system. Exceptions: <input type="hidden">, <input type="checkbox"> in table rows for native indeterminate support, <select> binding numeric values, and custom interactive regions needing specialized styling.
Layer 6 — Classes / Functions
Individual files inside a module or component folder. Private by default; public only when re-exported from the folder's index.ts.
Recurring kinds
Guards (auth/guards.ts):
| Function | Failure |
|---|---|
requireAuth(locals) |
redirect(303, '/auth/login') |
guardApiUser(locals) |
returns apiError(401) (never throws — a thrown Response becomes a 500) |
requireAdmin(locals) |
error(404, 'Not Found') — non-admins get a 404, not a 403, so the admin surface is not disclosed |
Step-up gate (auth/step-up.ts): requireStepUp / isStepUpFresh / stampStepUp over a Redis stepup:<userId> key (600s). Reads Redis, never the session (freshness must not ride the cookie cache); fail-closed in prod. Never imports the auth instance — auth/index.ts imports from it via its hooks. factor-changes.ts:onFactorChanged is the single chokepoint for passkey/TOTP side effects (audit + sibling-revoke + email). See blueprint/auth.md.
Services — multi-step orchestration warranting extraction: NotificationService.send() (DB insert → SSE push → async channel routing).
Domain functions — the shared call site for all adapters: getNotifications, markAsRead, retrieve (rawrag), searchLlmwiki, getCustomPaletteById.
Read/write seam: queries.ts contains reads with no side effects; mutations.ts contains writes with explicit intent. This split is a naming convention, not a CQRS infrastructure.
Error classes and classifiers: ServerError → DbError / AIError / Neo4jError / LlmwikiError; classifyDbError / classifyAIError / classifyNeo4jError; safeDbMessage / safeAIMessage.
Provider resolution (ai/providers.ts): getActiveProvider / getToolProvider resolves in order: request override → user preference → env → first configured. Circuit breaker: markCooldown / isCooledDown (60-second window), Redis-backed (ai:cooldown:{id}) so it is cross-instance and async.
AI tools (ai/tools/): desk-read, desk-write, propose-plan, get-rawrag-chunks, get-llmwiki-pages, resolve-ref, search-catalog, search-docs. All are thin wrappers that return structured data and never throw — tools return error objects; the LLM reads them.
Catalog grounding (ai/catalog-citations.ts, ai/tool-leak-guard.ts): post-stream surface-citation verifier and Groq/llama textual-tool-call leak guard. See blueprint/ai/provider-routing.md.
Catalog graph (search/catalog-map.ts, search/catalog-projection.ts, graph/catalog.ts): formatCatalogMap injects a path-free shape hint into the system prompt; deriveCatalogGraph projects the showcase registry to typed :Resource nodes + PART_OF edges; seedCatalogResources writes them to Neo4j idempotently (run via db:catalog-sync, chained into db:setup).
Docs corpus (rawrag/markdown-split.ts, ai/tools/search-docs.ts, scripts/db/ingest-docs.ts): the project's own docs/**/*.md is ingested into rag.document/rag.chunk (owned by SYSTEM_DOCS_USER_ID) so the search_project_docs tool can ground "how does X work" answers in the real docs. Ingestion is manual — db:ingest-docs (not chained into db:setup). See blueprint/ai/layered-rag.md.
CVA variant definitions (e.g. button.ts): class-variance-authority variants double as DOM markers for scoped CSS selectors (UnoCSS cannot extract complex classes from .ts files reliably; Button.svelte scoped CSS does the actual styling, targeting CVA class names as :global() selectors).
Layer 7 — Code
Leaf .ts, .svelte, and .css files. The execution substrate.
Languages and type discipline: TypeScript strict mode throughout. Svelte 5 runes ($state, $derived, $effect, $props) for all reactive state — no Svelte 4 stores. CSS custom properties for all theming.
Recurring idioms:
| Idiom | Pattern |
|---|---|
| Framework-free domains | Domain functions take plain arguments; return plain values or null |
| Return-null-not-throw at domain boundaries | return row ?? null rather than error(404) inside domain logic |
| Structured tool returns | return { data } or return { error: 'safe message' } — never throw from a tool |
| Fire-and-forget side effects | someAsyncWork().catch(logger) — not awaited, not blocking |
| OKLCH color math | culori library for accent token derivation |
| Graph queries | Cypher via Neo4j driver (Bolt) |
| Type-safe SQL | Drizzle ORM; schema-typed queries |
| Form validation | Valibot safeParse; Superforms lifecycle stays in the adapter |
Data Flow & Wiring (Runtime)
Descent diagram
HTTP edge
│
▼
hooks.server.ts sequence() ──────────────────────────────────────────┐
│ (14 stages, mutate event.locals) │
│ [securityHeaders → bodySizeFloor → stripBaseLocalePrefix → │
│ docsMarkdown → loadStyle → i18n → authCaptchaGate → │
│ authHandler → csrfProtection → sessionPopulate → │
│ consentLoader → debugOwnerLoader → devRouteGuard → │
│ analyticsCollector] │
│ │
▼ │
Route adapter (event.locals fully populated) │
│ +page.server.ts | +server.ts | ai/tools/ | jobs/ │
│ │
▼ │
Domain module $lib/server/[domain]/ (no framework imports) │
│ Pure TypeScript; calls db/, calls infra via injected clients │
│ │
▼ │
db/[domain]/{queries,mutations}.ts │
│ │
▼ │
Infra boundary ───────────────────────────────────────────────────────┘
Postgres/Neon · Neo4j/Aura · Upstash Redis · R2 · AI providers · channels
Data flows down. Responses bubble up the same chain. The event.locals bus is the handoff between the middleware column and the route-adapter column.
Infra boundary crossings
| Infrastructure | Used by |
|---|---|
| PostgreSQL / Neon | All CRUD; RAG tiers 1–2; llmwiki search; analytics; job logs; grants; conversations |
| Neo4j / Aura (Bolt) | RAG tier-3 graph expansion only |
| Upstash Redis | Rate-limit; AI daily budget; Better Auth secondary storage |
| Cloudflare R2 (S3 API) | Blog media, avatars |
| AI providers (HTTPS) | Embeddings + streamText |
| Notification channels | Telegram, Discord, SMTP (external delivery) |
| In-process SSE Map | In-app push — server→client; lives in persistent process heap; does not survive restart or cross instances |
Multi-client adapter table
| Client | Adapter | Domain call | Infrastructure |
|---|---|---|---|
| Human UI | +page.server.ts load / action |
getNotifications() |
PostgreSQL |
| REST API | +server.ts GET / POST |
markAsRead() |
PostgreSQL |
| AI tool | tool execute callback |
getNotifications() |
PostgreSQL |
| Background job | runJob() → job function |
Direct DB call | PostgreSQL |
Authentication per client: session cookie for UI and REST (populated by sessionPopulate in hooks); closure capture of userId for AI tools (auth happens once at the chat endpoint, user.id flows into createTools(user.id)); none for background jobs (trusted server context).
Traced flows (wiring inventory)
Nine end-to-end flows have been traced through the system:
- Request lifecycle — HTTP edge → hooks pipeline → route adapter → domain → DB → response
- Multi-client core (notifications) — same
getNotifications/markAsReadcalled from UI, REST, AI tool, and job - AI chat + tool-calling —
orchestrateChatinai/chat-orchestrator.ts; provider resolution →streamText→ tool loop → persistence - RAG retrieval —
retrieveinrawrag/index.tsfans out acrosstiers/(tier-1searchContextual= pgvector + BM25 fused via reciprocal-rank fusion, tier-2searchParentChild, tier-3searchGraph— Postgres seeds → Neo4j expand → Postgres hydrate) - Notification delivery —
NotificationService.send()→ DB insert + SSE push + web push (both synchronous) + channel routing (async via outbox: Telegram / Discord / email). The outbox drain is a claim-based queue worker: rows are taken with an atomicFOR UPDATE SKIP LOCKEDclaim, terminal writes are fenced onattempts, and a lapsed lease is reclaimed inside the same drain. See blueprint/architecture/workers.md - Background jobs —
runJob()+ scheduler (setInterval, persistent container) vs cron dispatcher (/api/cron/[job], serverless); same runner, different trigger - Visual identity —
loadStylein hooks resolves cookie → custom palette DB lookup (CP_ids only) →generateRandomStylefallback; ParaglidetransformPageChunkinjects palette CSS into every HTML response. There is no site-wide brand override: every visitor's style is their own - Auth + session + grants + analytics —
authHandler(Better Auth) →sessionPopulate(locals.user/session/grants) →analyticsCollector(consent-tiered, fire-and-forget). Factor mutations (passkey/TOTP) route through two global Better Auth hooks inauth/index.ts: abeforestep-up gate and anafterchokepoint that audits, revokes sibling sessions, emails, and stamps step-up freshness. Passkeys are a phishing-resistant first factor; TOTP is a step-up factor only (passwordless sign-ins are never challenged). See blueprint/auth.md - Personal-data access (privacy) —
collectUserDatainprivacy/report.tsis the one aggregator behind four adapters: the/account/datapage load (streamed),GET /api/me/data,GET /api/me/data/export, andDELETE /api/me(viadeleteUserData). One definition of "all my data"; secrets projected out at the query, prior-session IPs masked. Erasure is the Postgres FK cascade plus a best-effortdeleteUserGraphsweep (Neo4j has no FKs). See stack/capabilities/gdpr.md
Drift from Blueprint & Known Gaps
These gaps make the blueprint-to-code mapping imperfect. They are recorded here, not concealed, so the doc remains trustworthy.
-
No notification AI tool implemented.
multi-client-core.mdusescreateNotificationTools/markNotificationReadas its flagship example.ai/tools/currently holds desk, llmwiki, rawrag, propose-plan, and resolve-ref tools. Multi-client reuse for notifications is real for UI, REST, and jobs — not yet for AI. -
Daily token budget is enforced.
chargeTokensrecords daily AI spend to Redis inonFinish; the entry-gatecheckUserBudget(ai/budget.ts) now runs in the sharedguardAiRequest(ai/guard.ts) beforeorchestrateChat, rejecting once the day's spend exceeds the cap. (It was previously called nowhere — recorded but unenforced.) -
notification-deliverydrains on every platform. It is registered in the jobs registry (jobs/index.ts) and scheduled invercel.json(/api/cron/notification-delivery, daily — Vercel Hobby rejects sub-daily crons at deploy time) — on Vercel the cron sweep is what drains pending Telegram / Discord / email deliveries. On persistent platforms the 15-seconddelivery-schedulersetIntervalstill owns it; the cron never fires there. Web push bypasses the outbox entirely — it is partitioned out beforecreateDeliveriesruns and sent synchronously insideNotificationService.send(), alongside in-app SSE. (It previously had no serverless trigger and was absent from the registry, so deliveries queued aspendingand never drained on Vercel.) -
Per-surface chat routes, one orchestrator. Three thin routes —
/api/ai/chatbot,/api/ai/deskbot,/api/ai/showcase/rag— set an explicitsurfaceand share one entry guard (guardAiRequest), then call the singleorchestrateChat(routing, retrieval, tools, persistence). They replace the former single/api/ai/chat; the bare/api/ai/chat/streamis also gone. See blueprint/ai/surfaces.md. -
Blueprint examples lag the SDK.
multi-client-core.mdshows AI SDK v4/v5 spellings (parameters,maxSteps,maxTokens). The running code is v6 (inputSchema,stopWhen: stepCountIs,maxOutputTokens). The code is correct; the blueprint doc has not been updated.
Related Docs
| Document | What it elaborates |
|---|---|
codebase-organization.md |
The spatial map: source-tree layout, canonical-home rules, import direction |
blueprint/architecture/multi-client-core.md |
Full hexagonal core pattern: the four invariants, adapter patterns, auth per client type, error handling, extraction rules |
blueprint/middleware.md |
Hooks pipeline detail: CORS, security headers, session strategy |
blueprint/ai/ |
AI assistant architecture, RAG pipeline, TOON format, tool design |
blueprint/db/polyglot-freshness.md |
When to use each data store; freshness and consistency tradeoffs |
foundation/architecture.md |
SvelteKit route structure patterns |
foundation/principles.md |
The seven decision constraints that drive every stack choice |