Where code lives, and why it lives there.
This document is deliberately short. The repository is meant to answer "where does X go?"
by itself — through directory names, barrels, and the checks in
src/lib/architecture.gate.test.ts. What remains
here is the part a tree cannot show: the reasoning, and the constraints that come from
outside the codebase.
For the runtime view — request flow, the hooks pipeline, the layer hierarchy — see
system-abstraction.md.
The one decision that matters
Adapter or domain? Getting this wrong couples business logic to the framework, and it is the only structural mistake here that is expensive to undo.
A domain (src/lib/server/[domain]/) is framework-free. It returns values and null;
it never returns a Response, never throws redirect(), and never imports
@sveltejs/kit or $app/*. That is what lets one function serve a page load, a REST
endpoint, an AI tool, and a cron job without modification.
An adapter translates a domain to one transport. There are exactly three kinds, and each announces itself:
| Kind | Where | Example |
|---|---|---|
| Route adapter | +page.server.ts, +server.ts |
handles fail/redirect/error, converts Date → ISO |
| Shared HTTP toolkit | src/lib/server/http/ |
response envelopes, pagination, bounded body reads, rate limiters, route guards |
| Domain-local | *.adapter.ts, *.hook.ts |
mcp/http.adapter.ts, analytics/collector.hook.ts |
The gate enforces this. There is no exception list to keep in sync: if a module under
server/[domain]/ needs the framework, it is an adapter and belongs in one of the three
places above.
Product, demonstration, plumbing
This repo is a pattern library, so the most important thing a reader needs to know about any file is which of these it is.
src/lib/server/[domain]/— product behaviour.src/lib/server/showcases/[name]/— exists to demonstrate a pattern. Written to the same standard (these are the reference implementations people copy), but owns no product behaviour. Deleting a showcase page must never break anything outside it.pattern-library/— the registry that is the product: pattern records, their schema, and the drift guard.mcp/(stdio, bare Bun) andsrc/lib/server/mcp/(hosted HTTP) are two transports over it; the generated pages underdocs/pattern-library/are a third reader. Reachable from the app as$patterns.
A showcase spans three directories, one per concern, and they are named alike:
$lib/showcases/[name]/ (shared vocabulary) · components/showcases/[name]/ (UI) ·
server/showcases/[name]/ (server logic).
Data below the components
Some things are neither UI nor server logic — they are catalogues that several UI features
read. They live directly under src/lib/ so that no feature directory has to import
another:
| Owns | Read by | |
|---|---|---|
$lib/3d/ |
scene, part, and customization registries | the 3D viewer and blog scene embeds |
$lib/desk/ |
persisted layout shape, panel catalogue, help copy, the spreadsheet formula evaluator and cell contract | the Desk UI, the app shell's command palette, the desk DB schema, and the desk mutations (every stored sheet is re-derived at the write door) |
$lib/showcases/catalog/ |
the showcase card tree and section anchors | the hub, nav, both search lanes, the sitemap, the Neo4j projection |
$lib/desk/panels.ts lists the panel types; the directories under
components/desk/panels/ are named to match, one per entry. The list and the tree are the
same fact.
db/ — two trees, on purpose
schema/[namespace]/ groups tables by storage. db/[domain]/ groups access by call
site. They are not 1
schema/personalization is read
by four different domains, and schema/auth is mostly Better Auth's.
Reads and writes split into queries.ts / mutations.ts, in one of two places:
- Default —
db/[domain]/, for incidental CRUD. - Co-located in
[domain]/when the query is the domain logic and cannot be meaningfully separated from it — retrieval ranking (retrieval/), post rendering (blog/).
The test is not "is it SQL" but "would someone reading this domain expect to find it here".
Push-only. There is no drizzle/ migrations directory; db:push syncs directly. Every
pgSchema() must be exported through schema/index.ts and listed in
drizzle.config.ts's schemaFilter, or push silently omits it.
db/ is the sink: it imports no sibling domain, so the import graph stays acyclic. Its
root holds the primitives every table's values are made of and no domain owns — id.ts,
content-hash.ts, regconfig.ts, errors.ts — and db/[domain]/ holds the ones only one
schema needs (analytics/inet.ts, the shape checks before an ::inet cast). A domain
that finds db/ reaching up into it has found one of these in the wrong place. The one
thing beneath the sink is server/errors/: the base class every domain's errors extend,
with no imports of its own.
Components — the barrel is a bundle boundary
Layer order, leaf → root: primitives/ ← composites/ ← layout/ ← shell/ and the
feature directories. Feature directories never import each other; when two need the same
component, it moves down a layer rather than sideways (that is how
composites/citation/ and primitives/color-input/ came to exist).
The default $lib/components barrel is the cheap surface. Anything pulling a heavy or
optional dependency — viz engines, Three.js, the markdown sanitiser — or app-specific
chrome is deep-import-only. Adding a heavy dependency to a barreled component is a
bundle-size regression, not a style preference.
Which directories are excluded is asserted in the gate, not listed here: the prose version of that list drifted to naming two of fourteen.
Routes
src/routes/[[locale=locale]]/ is the localized tree; src/routes/api/ is the parallel
un-localized REST/SSE tree. Auth gates live in +layout.server.ts files, not route groups.
Route-local private folders use a leading underscore (_components/, _sections/).
Promote to $lib/components/[layer]/ only when a second route needs the same thing.
Global CSS belongs to the root layout, not the locale layout. uno.css, src/app.css,
and the fonts are imported once in src/routes/+layout.svelte. This is load-bearing: a
+page@.svelte breakout sheds the locale layer, so anything it needs globally must live
above it. The full-screen 3D viewer rendered token-less until these were hoisted.
Naming
- Server
.ts: kebab-case. Components: PascalCase.sveltein a kebab-case folder. - Barrels are always
index.ts. Tests are co-located*.test.ts— no__tests__/. - Runes state files must use
.svelte.ts. App-wide:src/lib/state/[concern].svelte.ts. Component-local: co-located as[component].state.svelte.ts. - Internal/special files take a leading underscore (
_better-auth.ts).
Policy belongs to its domain
Every domain keeps its own constants in server/[domain]/config.ts. There is no shared
constants module — re-introducing one is the regression this replaced. A policy leaf is
deep-imported by design (the gate exempts */config.ts and *-config.ts): taking a constant
through the domain's barrel would drag that domain's whole implementation graph with it.
Retention is one schedule, not fourteen constants. server/retention/schedule.ts names every
dataset that ages out, its window, what the sweep does and which job enforces it. The cron
sweeps read it, the public privacy page renders it, and retention/schedule.gate.test.ts
fails if a sweep hard-codes a window or names a job that does not exist.
Where does a new file go?
- Thin adapter? (handles
fail/redirect/error, converts types, no business logic) → the route file, orserver/http/if several adapters share it. - Business logic? →
server/[domain]/[feature].ts, exposed via the domain'sindex.ts. - Only there to demonstrate a pattern? →
server/showcases/[name]/. - A Postgres query? →
db/[domain]/queries.ts, unless the query is the domain logic. - A table? →
db/schema/[namespace]/, exported throughschema/index.tsand listed indrizzle.config.ts. - A Valibot schema? → server-only:
server/schemas/. Client-importable:src/lib/schemas/. - A component? → the lowest layer that all its consumers can reach. Route-local until a second route needs it.
- Reactive state? → app-wide
src/lib/state/, or co-located with its component. - A policy constant? →
server/[domain]/config.ts. A retention window →server/retention/schedule.ts. - A catalogue two features share? → directly under
src/lib/, below the component layer (see$lib/3d,$lib/desk).
Constraints that come from outside
These are the reasons behind placements that otherwise look arbitrary.
pattern-library/sits outsidesrc/becausemcp/server.tsruns under bare Bun in an ephemeral, network-less container with no Vite — it must reachregistry.jsonby relative path. The app gets the same file through the$patternsalias.pattern-library/schema.tsis split fromload.tsso the app can import the pattern types without pullingimport.meta.dir, a Bun API that breakssvelte-check. One type declaration, two runtimes.server/retrieval-shared/embed-config.tsis dependency-free so standalone Bun ingest scripts can import it by relative path. Never re-declare those constants;retrieval/config.tsre-exports them so retrieval code has one place to look.$lib/showcases/catalog/registry.tsuses relative imports, not$lib, so it stays resolvable fromscripts/db/catalog-sync.tsunder bare Bun.ai/connections.ts,db/ai/provider-connections.tsandsecurity/aes-gcm.tsare alias-free for the same reason: the ingest and seed scripts read the administrator's saved Google connection through them by relative path, so the app and the scripts share one reading of "is Google usable" and one ciphertext format.ai/connections.test.tswalks the import closure;security/encryption-key.tsis where the app, not the scripts, readsENCRYPTION_KEY.auth/admin-ids.tsis deep-imported on purpose: it is a framework-free leaf that exists precisely so callers can avoid constructing the Better Auth instance thatauth/index.tsbuilds.server/mcp/keepshttp.adapter.tsDB-free so the protocol tests need no mocks;telemetry/writer.tsis the only module there that touches the database.
Related
| Document | Covers |
|---|---|
system-abstraction.md |
Runtime view: layers, request flow, hooks pipeline |
blueprint/architecture/multi-client-core.md |
The hexagonal core in detail |
src/lib/architecture.gate.test.ts |
The invariants, executable |
foundation/self-expressive-project.md |
The principle this map serves: structure expresses architecture, and the gates that make it testable |
CLAUDE.md |
Agent instructions and house rules |