For AI agents
The contract for agents working in this repository. Humans: these are the same rules
you follow, compressed. (This site also ships llms.txt and llms-full.txt — the
whole documentation as plain text.)
Orientation
Tikab's Toolkit = the reusable foundation in packages/ (consumed as @repo/*
inside this repo; published name will be @tikab-interactive/toolkit, not yet
published). example/ = one consumer of it, also the reference implementation.
Generic capability → a package. Domain behavior → the example.
Definition of done
For every change: bun run typecheck, bun run lint, bun run fmt green from the
repo root, plus the affected e2e specs green. Schema change ⇒ migration generated
(bun run db:generate), the SQL reviewed, and applied. See
Commands for everything runnable.
The map
| What | Where |
|---|---|
| Building blocks (generic logic) | packages/<block>/src/ |
| The app's server functions | example/src/lib/*-server.ts |
| Routes (file-based, flat dotted names) | example/src/routes/_authed.<area>.<page>.tsx |
| Site-admin area | example/src/routes/_authed.admin.* + example/src/components/site-admin/ |
| Sandbox harness | example/src/routes/_authed.sandbox.* + example/src/lib/sandbox-server.ts |
| Journey tests / block tests | example/e2e/*.spec.ts / example/e2e/sandbox/*.spec.ts |
| Schema: foundation / app | packages/db/src/schema/foundation.ts / app.ts |
| Migrations | packages/db/drizzle/ |
| These docs | docs/src/pages/ |
Rules
- Break things into small, separate packages. A reusable capability belongs in
packages/<block>, standing on its own legs — not inlined in the app. When app code starts looking generic, extract it. - Sandbox-first. New building block = package → sandbox page → block spec → only then app UI. Definition of done.
- UI decomposes to its smallest parts, and a UI component does ONE of two
things: handle layout, or render one specific component. UI packages never
contain logic and never contain user-facing strings — not even as default prop
values. Every label, empty state, aria text and date locale is a prop; the
consumer (the example app) supplies them all through Paraglide JS
(
m.*()fromexample/messages/sv.json+en.json, Swedish base locale). New user-facing text ⇒ a new message key in BOTH locales, never a literal in a component. - Packages are generic. No app knowledge (projects, tasks) in
packages/. Domain logic lives inexample/src/lib/*-server.ts. - Server packages ship a browser stub — a
browsercondition inexportspointing at a throwing stub. - Graceful degradation for optional infra. Missing env ⇒ log and skip, never a boot crash. Patterns: jobs, mailer, ai.
- Access control in the server. Every server function opens with
requireSession()orrequireSiteAdmin(). Gates throwResponse— do not catch them. - Mutations write audit rows via
recordAudit()with a human-readable summary. - Wire types are serializable. Dates as ISO strings;
unknownis rejected by the shape check — useanywith a justifying comment for free-form JSON. - Schema fragments: the foundation never references app tables; the dependency arrow always points app → foundation.
- Reuse the seams. Files via the storage block, email via the mailer, jobs via the app's enqueue helpers, LLM calls via the ai factory. Never hard-code a provider.
- Docs excerpts are physical includes driven by
[!region]markers in source files — moving a marker silently changes the docs; keep them where they are or update the page.
Known traps
bun run db:seedtruncates everything.- Better Auth's server API sets cookies inside a request context —
auth.api.signUpEmailin a server function hijacks the caller's session. Create users with directuser+accountinserts (hashPasswordfrombetter-auth/crypto); seeadminImportUsersCsv. - Playwright reuses a dev server already on
:3000— check which process owns the port before debugging "impossible" failures. - Shared dev databases drift under repeated journey runs — a red journey spec after many runs can mean "re-seed", not "bug".
- A new UI package needs
@sourcelines. Tailwind only generates classes it finds in scanned sources:example/src/styles.cssanddocs/src/_components/demo.cssboth list the workspace packages explicitly. Skip it and any class used only inside the package is silently missing from the production CSS (the admin shell's full-height sidebar was the symptom) while dev pages with tall content hide it. - drizzle-kit drops the SRID when generating migrations for
geometrycolumns — hand-edit the SQL togeometry(Point, 4326)beforedb:migrate(see migrations 0008/0009). - A thrown non-redirect
Responsefrom a server function isn't reliably a client rejection.throw new Response("…", { status: 400 })can surface as a resolved value, so the client proceeds as if it succeeded. For an expected "no" (validation, a refused delete), return a typed result ({ ok: false, reason }) and branch on it — seedeleteMyAccount. Reservethrowfor auth (the 302 fromrequireSession). /sandboxis site-admin-only in a production build, open in dev. A block spec that must act as a non-admin (rules, GDPR signup) runs against the dev server; one that only needs any signed-in user runs against either. When a prod-build sandbox spec 404s, that's the gate, not a bug.- Prod-build e2e hits the auth rate limit. Rate limiting is on only when
NODE_ENV=production, backed by the shared Valkey store; repeated sign-ins across runs trip it and sign-in silently stays on/login. Clear it withdocker exec example-valkey valkey-cli FLUSHALLbetween prod-build runs (dev has rate limiting off). - Self-hosted infra pins, and
latestdrifts. hatchet-lite once moved its admin binary, config dir and required a new env var on alatestbump, and regenerated its keys (invalidating tokens) when its/configvolume wasn't persisted. Every compose image is pinned; keep it that way and persist state volumes. - A new package must be wired into four places, or it's invisible: the
consuming
package.json,example/src/styles.css(@source, if it ships classes), the docs sidebar (docs/vocs.config.ts) and the overview graph + blocks table. Adding the stone is half the job.