T
Sign in

STACK.md — how each piece is wired

STACK.md — how each piece is wired

This is the org default stack: Bun → SvelteKit (@sveltejs/adapter-bun) → daisyUI (Tailwind v4) → Drizzle → bun:sqlite, plus Better Auth on top. Every file referenced below lives in this repo; keep this document updated when the wiring changes.

SvelteKit 3 + @sveltejs/adapter-bun

  • Framework: SvelteKit 3 (runes; svelte ^5.57). Server load + form actions in src/routes/+page.server.ts; UI in src/routes/+page.svelte. The $lib alias is gone — imports use the #lib subpath from package.json ("imports": { "#lib": ..., "#lib/*": "./src/lib/*" }) with .js extensions (Vite/TS map them to the .ts sources).
  • Config lives in vite.config.ts (sveltekit({ preprocess, adapter })): SvelteKit 3 no longer reads svelte.config.js. The production build is build/ and is run with bun run build/index.js — no Node needed.
  • Adapter: @sveltejs/adapter-bun. It serves client/prerendered assets itself as native Bun routes; the app is hosted at the root of its own host (trading.tynolabs.com), so there is no paths.base. Keep it at root — a sub-path deployment (e.g. dsh.tynolabs.com/trading) 404s every asset. The first iteration of this project hit that exact bug.
  • ORIGIN=https://trading.tynolabs.com in .env is the app's own value (Better Auth baseURL, read via src/env.ts → $app/env/private). SvelteKit 3's CSRF check uses paths.origin, else the adapter derives the origin from the Host header assuming https; a missing/mismatched origin ⇒ every form POST → 403.
  • Build with bun run build (the one true command — it is the bun-runtime invocation): the npm-run wrapper trips Node's ESM loader on bun:sqlite; the direct binary under bun is the supported path. vite.config.ts marks bun:sqlite as an SSR-external.

Drizzle ORM + libSQL (Turso)

  • One database: Turso. src/lib/server/db.ts — createClient() from @libsql/client, wrapped with drizzle() — reads TURSO_DATABASE_URL + TURSO_AUTH_TOKEN (.env locally; the wrangler.jsonc var + the Pages secret in production). The hosted app, the dev server and every script share that ONE database. There is no local app database — the file was removed on 2026-11-04.

  • Tests never touch it. tests/setup.ts (preloaded via bunfig.toml) clears TURSO_*, so the suite runs against a disposable local SQLite fixture created on demand — the only file: database left, and never a project DB.

  • libSQL is ASYNC: every Drizzle terminal call (.get() / .all() / .run()) and every db.transaction(async (tx) => …) callback must be await-ed. Raw SQL goes through the exported client.execute({ sql, args }). There is no synchronous bun:sqlite handle in the app any more.

  • Schema: src/lib/server/schema.ts (imported by drizzle-kit). App tables + Better Auth's user / session / account / verification tables. Never import schema from browser code (#lib/server stays server-side).

  • Migrations (always the Turso DB named in .env):

    bunx drizzle-kit generate   # write SQL into drizzle/
    bunx drizzle-kit push       # apply to the Turso DB
    

    drizzle/ IS committed; a stray test fixture is gitignored. After a push, re-create the evt dedupe unique index (drizzle-kit drops it — Gotcha 14).

  • Gotcha: drizzle's SQLite integer(..., { mode: 'timestamp' }) stores epoch seconds, not ms (that's what mode: 'timestamp_ms' is for). The schema intentionally uses the plain timestamp mode everywhere and the generated SQL matches — don't "fix" it to ms.

  • Scripts (scripts/seed-demo.ts, live-check.ts, …) resolve their target the way the app does — Turso when TURSO_* is set — and a mis-pointed target fails loudly rather than seeding a stray local file.

daisyUI 5 + Tailwind v4

  • Two lines, all of it: vite.config.ts has the @tailwindcss/vite plugin; src/app.css has @import "tailwindcss"; @plugin "daisyui";.
  • House rules (org): build UI from daisyUI classes (btn, input, card, navbar, badge, table, alert, toggle, tabs, …); theme tokens (bg-base-200, badge-primary, …) instead of hardcoded colors.
  • daisyUI for everything — MUST (Repo rule 4, PLANNING §46): every control/card/table/form/modal is built from daisyUI components; raw utility classes are allowed only where daisyUI has no fitting component (app-shell sidebar/drawer scaffolding in +layout.svelte, icon rows, custom grids). Utility-styled UI that daisyUI can express is a defect — convert it in the same change it is found.
  • App shell + theme reference — GrayFabric ERP (owner directive, github.com/skpawar1305/gray_fabric_erp; PLANNING §45): +layout.svelte is a grouped light sidebar (lucide-svelte icons, blue active states) with a mobile drawer and a user chip + sign-out; per-page duplicate chrome (home navbar identity/links/sign-out) lives in the shell now. src/app.css layers the re-skin on top of the pinned light theme: primary = blue-600, slate neutrals (white cards on slate-100), radii selector/field/box 0.5/0.375/0.5rem, --text-xs 13px / --text-sm 15px, and the reference's tbody tr:hover inset-primary-bar row highlight. The daisyUI plugin block (themes: light --default) is untouched — never move the re-skin into a second theme.
  • Light mode only (Repo rule 1 — README "The rules of this repo"): daisyUI v5's default theme list is light --default, dark --prefersdark, so a bare @plugin "daisyui"; auto-ships dark mode (the site goes dark whenever the OS prefers dark). src/app.css therefore must pin themes: light --default; inside the plugin block — never add a second theme, never add the --prefersdark flag, no data-theme switching. Dark-mode-related markup/config is cruft — remove it wherever you find it. (Regression-guarded by tests/ui.test.ts.)

Type checks (svelte-fast-check)

  • Runner: svelte-fast-check (svelte2tsx + tsgo) — drop-in replacement for svelte-check, up to ~24x faster with --incremental. Run with bun run check (or bun --bun svelte-fast-check --incremental).
  • It type-checks .ts + .svelte (TS errors) and reports Svelte compiler warnings (unused CSS, a11y hints, state_referenced_locally, …). CSS language-service diagnostics are not included (use eslint-plugin-svelte / Biome if needed) and the tsgo diagnostics set is still being completed.
  • scripts/deploy.sh runs bun test (not the type check) — the type check is the manual pre-commit gate. Per Repo rule 3, tests matter more.

Tests (bun:test, zero deps)

  • Runner: Bun's built-in test runner — run with npm run test (bun test); one file with bun test tests/<file>.test.ts. Nothing to install.
  • Suite lives in tests/, named after and mirroring src/lib/server/*.ts (pure logic: yarnMath, tippanSolver, intake) plus config guards (tests/ui.test.ts pins the light-mode-only Rule 1).
  • Two repo rules make this non-optional: every solved edge case gets a regression test (same commit, named after the case); passing tests is as important as commit+push — scripts/deploy.sh runs bun test first and aborts on red. Full conventions: docs/TESTS.md.

Better Auth (email + password)

  • Server instance: src/lib/server/auth.ts — betterAuth({ database: drizzleAdapter(db, { provider: 'sqlite', schema: authSchema }), emailAndPassword: { enabled: true }, baseURL: ORIGIN, secret: BETTER_AUTH_SECRET, trustedOrigins: [...] }).
  • Routing: src/hooks.server.ts returns svelteKitHandler({...}) from better-auth/svelte-kit, so /api/auth/* is answered by better-auth and everything else falls through to SvelteKit. Works at root path defaults because the app is at the root of its host (see the vite.config.ts note).
  • Browser client: src/lib/auth-client.ts — createAuthClient() defaults; the page calls authClient.signUp.email / signIn.email / signOut.
  • Session on the server: auth.api.getSession({ headers }) in the page server load.
  • DB: the four Better Auth tables are in schema.ts. When Better Auth upgrades and its core schema gains a field (e.g. account.issuer was required by 1.7.x), Drizzle migration + a docs note are mandatory.
  • BETTER_AUTH_SECRET is a per-deployment secret in .env (openssl rand -base64 32), never committed.
  • Cookie: __Secure-better-auth.session_token, Secure; HttpOnly; SameSite=Lax, path /. Browsers only send it over HTTPS — fine for https://trading.tynolabs.com (curl against plain HTTP can't send it, so smoke-test with -H "Cookie: ..." or expect null).
  • trustedOrigins includes the LAN/HTTP doors (192.168.1.102:4104/4106) so the auth API also accepts those origins if the app is ever exposed there; the primary origin is https://trading.tynolabs.com.