# System Abstraction

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.ts` wires together Layers 1–3: it boots background modules, runs a 14-stage `sequence()` of `Handle` middleware, and hands a fully populated `event.locals` to 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](#security-headers-set)) |
| 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:

1. **No framework imports in domain modules.** No `@sveltejs/kit` or `$app/` imports inside `$lib/server/[domain]/`. These bind logic to the SvelteKit request cycle, preventing reuse by AI tools and jobs.
2. **Date serialization happens in the adapter layer.** Domain modules return `Date` objects as-is. The route or tool `execute` converts them to ISO strings.
3. **SvelteKit response helpers (`redirect`, `error`, `fail`, `message`) only in adapters.** Never in domain modules.
4. **Domains call down, not across.** Cross-domain reads go through the other domain's `index.ts` barrel 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:

```typescript
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`)

```typescript
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 (`:root` light, `.dark` dark). All color tokens live here.
- `src/lib/styles/tokens.ts` — build-time token values (breakpoints, fontSize, spacing, borderRadius, zIndex, iconSize) read by `uno.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](./blueprint/auth.md#passkeys--step-up-totp).

**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](./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](./blueprint/ai/layered-rag.md#docs-corpus-search_project_docs).

**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:

1. **Request lifecycle** — HTTP edge → hooks pipeline → route adapter → domain → DB → response
2. **Multi-client core** (notifications) — same `getNotifications` / `markAsRead` called from UI, REST, AI tool, and job
3. **AI chat + tool-calling** — `orchestrateChat` in `ai/chat-orchestrator.ts`; provider resolution → `streamText` → tool loop → persistence
4. **RAG retrieval** — `retrieve` in `rawrag/index.ts` fans out across `tiers/` (tier-1 `searchContextual` = pgvector + BM25 fused via reciprocal-rank fusion, tier-2 `searchParentChild`, tier-3 `searchGraph` — Postgres seeds → Neo4j expand → Postgres hydrate)
5. **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 atomic `FOR UPDATE SKIP LOCKED` claim, terminal writes are fenced on `attempts`, and a lapsed lease is reclaimed inside the same drain. See [blueprint/architecture/workers.md](./blueprint/architecture/workers.md)
6. **Background jobs** — `runJob()` + scheduler (`setInterval`, persistent container) vs cron dispatcher (`/api/cron/[job]`, serverless); same runner, different trigger
7. **Visual identity** — `loadStyle` in hooks resolves cookie → custom palette DB lookup (`CP_` ids only) → `generateRandomStyle` fallback; Paraglide `transformPageChunk` injects palette CSS into every HTML response. There is no site-wide brand override: every visitor's style is their own
8. **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 in `auth/index.ts`: a `before` step-up gate and an `after` chokepoint 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](./blueprint/auth.md#passkeys--step-up-totp)
9. **Personal-data access** (privacy) — `collectUserData` in `privacy/report.ts` is the one aggregator behind four adapters: the `/account/data` page load (streamed), `GET /api/me/data`, `GET /api/me/data/export`, and `DELETE /api/me` (via `deleteUserData`). 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-effort `deleteUserGraph` sweep (Neo4j has no FKs). See [stack/capabilities/gdpr.md](./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.

1. **No notification AI tool implemented.** `multi-client-core.md` uses `createNotificationTools` / `markNotificationRead` as 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.

2. **Daily token budget is enforced.** `chargeTokens` records daily AI spend to Redis in `onFinish`; the entry-gate `checkUserBudget` (`ai/budget.ts`) now runs in the shared `guardAiRequest` (`ai/guard.ts`) before `orchestrateChat`, rejecting once the day's spend exceeds the cap. (It was previously called nowhere — recorded but unenforced.)

3. **`notification-delivery` drains on every platform.** It is registered in the jobs registry (`jobs/index.ts`) and scheduled in `vercel.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-second `delivery-scheduler` `setInterval` still owns it; the cron never fires there. Web push bypasses the outbox entirely — it is partitioned out before `createDeliveries` runs and sent synchronously inside `NotificationService.send()`, alongside in-app SSE. (It previously had no serverless trigger and was absent from the registry, so deliveries queued as `pending` and never drained on Vercel.)

4. **Per-surface chat routes, one orchestrator.** Three thin routes — `/api/ai/chatbot`, `/api/ai/deskbot`, `/api/ai/showcase/rag` — set an explicit `surface` and share one entry guard (`guardAiRequest`), then call the single `orchestrateChat` (routing, retrieval, tools, persistence). They replace the former single `/api/ai/chat`; the bare `/api/ai/chat/stream` is also gone. See [blueprint/ai/surfaces.md](./blueprint/ai/surfaces.md).

5. **Blueprint examples lag the SDK.** `multi-client-core.md` shows 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`](./codebase-organization.md) | The spatial map: source-tree layout, canonical-home rules, import direction |
| [`blueprint/architecture/multi-client-core.md`](./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`](./blueprint/middleware.md) | Hooks pipeline detail: CORS, security headers, session strategy |
| [`blueprint/ai/`](./blueprint/ai/) | AI assistant architecture, RAG pipeline, TOON format, tool design |
| [`blueprint/db/polyglot-freshness.md`](./blueprint/db/polyglot-freshness.md) | When to use each data store; freshness and consistency tradeoffs |
| [`foundation/architecture.md`](./foundation/architecture.md) | SvelteKit route structure patterns |
| [`foundation/principles.md`](./foundation/principles.md) | The seven decision constraints that drive every stack choice |
