Skip to content
Tikab's Toolkit

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

WhatWhere
Building blocks (generic logic)packages/<block>/src/
The app's server functionsexample/src/lib/*-server.ts
Routes (file-based, flat dotted names)example/src/routes/_authed.<area>.<page>.tsx
Site-admin areaexample/src/routes/_authed.admin.* + example/src/components/site-admin/
Sandbox harnessexample/src/routes/_authed.sandbox.* + example/src/lib/sandbox-server.ts
Journey tests / block testsexample/e2e/*.spec.ts / example/e2e/sandbox/*.spec.ts
Schema: foundation / apppackages/db/src/schema/foundation.ts / app.ts
Migrationspackages/db/drizzle/
These docsdocs/src/pages/

Rules

  1. 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.
  2. Sandbox-first. New building block = package → sandbox page → block spec → only then app UI. Definition of done.
  3. 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.*() from example/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.
  4. Packages are generic. No app knowledge (projects, tasks) in packages/. Domain logic lives in example/src/lib/*-server.ts.
  5. Server packages ship a browser stub — a browser condition in exports pointing at a throwing stub.
  6. Graceful degradation for optional infra. Missing env ⇒ log and skip, never a boot crash. Patterns: jobs, mailer, ai.
  7. Access control in the server. Every server function opens with requireSession() or requireSiteAdmin(). Gates throw Response — do not catch them.
  8. Mutations write audit rows via recordAudit() with a human-readable summary.
  9. Wire types are serializable. Dates as ISO strings; unknown is rejected by the shape check — use any with a justifying comment for free-form JSON.
  10. Schema fragments: the foundation never references app tables; the dependency arrow always points app → foundation.
  11. 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.
  12. 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:seed truncates everything.
  • Better Auth's server API sets cookies inside a request context — auth.api.signUpEmail in a server function hijacks the caller's session. Create users with direct user + account inserts (hashPassword from better-auth/crypto); see adminImportUsersCsv.
  • 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 @source lines. Tailwind only generates classes it finds in scanned sources: example/src/styles.css and docs/src/_components/demo.css both 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 geometry columns — hand-edit the SQL to geometry(Point, 4326) before db:migrate (see migrations 0008/0009).
  • A thrown non-redirect Response from 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 — see deleteMyAccount. Reserve throw for auth (the 302 from requireSession).
  • /sandbox is 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 with docker exec example-valkey valkey-cli FLUSHALL between prod-build runs (dev has rate limiting off).
  • Self-hosted infra pins, and latest drifts. hatchet-lite once moved its admin binary, config dir and required a new env var on a latest bump, and regenerated its keys (invalidating tokens) when its /config volume 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.