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 insrc/routes/+page.server.ts; UI insrc/routes/+page.svelte. The$libalias is gone — imports use the#libsubpath frompackage.json("imports": { "#lib": ..., "#lib/*": "./src/lib/*" }) with.jsextensions (Vite/TS map them to the.tssources). - Config lives in
vite.config.ts(sveltekit({ preprocess, adapter })): SvelteKit 3 no longer readssvelte.config.js. The production build isbuild/and is run withbun 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 nopaths.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.comin.envis the app's own value (Better AuthbaseURL, read viasrc/env.ts→$app/env/private). SvelteKit 3's CSRF check usespaths.origin, else the adapter derives the origin from theHostheader assuminghttps; 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 onbun:sqlite; the direct binary under bun is the supported path.vite.config.tsmarksbun:sqliteas an SSR-external.
Drizzle ORM + libSQL (Turso)
One database: Turso.
src/lib/server/db.ts—createClient()from@libsql/client, wrapped withdrizzle()— readsTURSO_DATABASE_URL+TURSO_AUTH_TOKEN(.envlocally; thewrangler.jsoncvar + 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 viabunfig.toml) clearsTURSO_*, so the suite runs against a disposable local SQLite fixture created on demand — the onlyfile:database left, and never a project DB.libSQL is ASYNC: every Drizzle terminal call (
.get()/.all()/.run()) and everydb.transaction(async (tx) => …)callback must beawait-ed. Raw SQL goes through the exportedclient.execute({ sql, args }). There is no synchronousbun:sqlitehandle in the app any more.Schema:
src/lib/server/schema.ts(imported bydrizzle-kit). App tables + Better Auth'suser/session/account/verificationtables. Never import schema from browser code (#lib/serverstays 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 DBdrizzle/IS committed; a stray test fixture is gitignored. After a push, re-create theevtdedupe unique index (drizzle-kit drops it — Gotcha 14).Gotcha: drizzle's SQLite
integer(..., { mode: 'timestamp' })stores epoch seconds, not ms (that's whatmode: 'timestamp_ms'is for). The schema intentionally uses the plaintimestampmode 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 whenTURSO_*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.tshas the@tailwindcss/viteplugin;src/app.csshas@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.svelteis 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.csslayers 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-xs13px /--text-sm15px, and the reference'stbody tr:hoverinset-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.csstherefore must pinthemes: light --default;inside the plugin block — never add a second theme, never add the--prefersdarkflag, nodata-themeswitching. Dark-mode-related markup/config is cruft — remove it wherever you find it. (Regression-guarded bytests/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 withbun run check(orbun --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.shrunsbun 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 withbun test tests/<file>.test.ts. Nothing to install. - Suite lives in
tests/, named after and mirroringsrc/lib/server/*.ts(pure logic: yarnMath, tippanSolver, intake) plus config guards (tests/ui.test.tspins 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.shrunsbun testfirst 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.tsreturnssvelteKitHandler({...})frombetter-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 callsauthClient.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.issuerwas required by 1.7.x), Drizzle migration + a docs note are mandatory. BETTER_AUTH_SECRETis 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 expectnull). trustedOriginsincludes 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 ishttps://trading.tynolabs.com.