Перейти к основному содержимому

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 rootsrc/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 coresrc/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:

  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:

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 (: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.

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: ServerErrorDbError / 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 manualdb: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:

  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-callingorchestrateChat in ai/chat-orchestrator.ts; provider resolution → streamText → tool loop → persistence
  4. RAG retrievalretrieve 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 deliveryNotificationService.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
  6. Background jobsrunJob() + scheduler (setInterval, persistent container) vs cron dispatcher (/api/cron/[job], serverless); same runner, different trigger
  7. Visual identityloadStyle 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 + analyticsauthHandler (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
  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

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.

  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.


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
← Back to Docs

Думаете, этот паттерн можно сделать лучше? Расскажите как.

Оставить отзыв