Skip to content

Architecture

The one rule: a single source of truth over HTTP

Section titled “The one rule: a single source of truth over HTTP”

Every surface is a thin client of one HTTP API. The SvelteKit app, the Tauri desktop and mobile shells, the Astro sites’ dynamic bits, CLIs and the Python service all consume the same apps/api — the same auth, the same procedures, the same OpenAPI document. No Tauri IPC, no per-platform data layer, no duplicated business logic.

Public (Astro, static) Application (SvelteKit, one codebase) Native shells (Tauri 2, no IPC)
┌─────────────────────┐ ┌──────────────────────────────────┐ ┌────────────────────────────┐
│ apps/site │ │ apps/web · SSR (Node/Cloudflare) │ │ Desktop · Win, macOS, Linux│
│ apps/docs │ │ apps/web · static SPA ───────────┼───▶│ Mobile · Android, iOS │
└──────────┬──────────┘ │ PWA install │ └─────────────┬──────────────┘
│ optional └────────────────┬─────────────────┘ │ HTTPS + bearer
▼ ▼ HTTPS (cookie) ▼
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ apps/api — Hono on Bun │
│ Better Auth (/api/auth/*) · oRPC: RPC (/rpc/*) + REST/OpenAPI (/api/v1/*) · Stripe webhooks │
└──────────────────────┬──────────────────────────────────────────────┬──────────────────────────┘
│ Drizzle (Bun.SQL) │ X-Service-Token
▼ ▼
┌──────────────────┐ ┌────────────────────────────┐
│ Postgres 17 │ │ services/ai — FastAPI │
│ + pgvector │ │ AI · data · documents │
└────────────────────────────┘

The same diagram as Mermaid, for pasting into tools that render it:

flowchart LR
subgraph Public["Public (Astro, static)"]
SITE[apps/site<br/>marketing · blog · SEO]
DOCS[apps/docs<br/>Starlight]
end
subgraph App["Application (SvelteKit, one codebase)"]
WEB[apps/web · SSR<br/>Node or Cloudflare]
SPA[apps/web · static SPA]
PWA[PWA install]
end
subgraph Shells["Native shells (Tauri 2, no IPC)"]
DESK[Desktop<br/>Win · macOS · Linux]
MOB[Mobile<br/>Android · iOS]
end
SPA --> DESK
SPA --> MOB
WEB --> PWA
API["apps/api — Hono on Bun<br/>Better Auth · oRPC (RPC + REST/OpenAPI) · webhooks"]
DB[(Postgres + pgvector<br/>Drizzle)]
AI["services/ai — FastAPI<br/>AI · data · documents"]
WEB -- HTTPS --> API
SPA -- HTTPS + bearer --> API
SITE -. optional .-> API
API --> DB
API -- X-Service-Token --> AI
  • Add a feature once. Declare it in packages/api-contract, implement it in apps/api, call it from any client with full types (api.workspaces.create({...})). REST + OpenAPI come for free.
  • Ship the frontend anywhere. apps/web builds for Node, Cloudflare Workers or as a static SPA with one env var: ADAPTER=….
  • Auth is transport-aware, not platform-aware. Browsers use an httpOnly cookie; the Tauri shells use a bearer token. The UI code is identical.
  • The database has exactly one client. Only the API touches Postgres (Bun’s native Bun.SQL through Drizzle). Frontends cannot reach it even by accident.
  1. A Svelte component calls api.workspaces.list({ organizationId }) (packages/api-client).
  2. The oRPC link POSTs to ${PUBLIC_API_URL}/rpc/workspaces/list. In the browser the session cookie rides along (credentials: include); in a Tauri shell Authorization: Bearer … is attached from localStorage. During SSR, hooks.server.ts rewrites the origin to the internal API_URL and forwards the browser’s cookies.
  3. Hono routes to the oRPC handler; requireAuth resolves the session via Better Auth (auth.api.getSession) and the procedure runs against Drizzle.
  4. The same procedure is reachable as GET /api/v1/workspaces?organizationId=… and documented at /api/v1/openapi.json — that is what the Python service, curl and third parties use.

These are the rules the codebase is built around. Breaking one usually means a second source of truth is being created.

  1. Frontends never import server packages. @repo/db, @repo/auth/server, @repo/env and @repo/billing are never imported by apps/web or apps/site. The browser-safe surfaces are @repo/auth/client, @repo/auth/permissions, @repo/billing/catalog, @repo/api-client and @repo/ui.
  2. Contract first. No endpoint without a contract entry; no client without the typed client. A new feature is: contract in packages/api-contract → implementation in apps/api/src/rpc/router.ts → the UI calls the typed client. REST and OpenAPI follow.
  3. No Tauri IPC. The shells have no commands and no data layer; they use the same HTTP client as the browser. Native capabilities are added as Tauri plugins.
  4. SSR is rendering, not a backend. apps/web has no server-side business logic; load functions call the API. No form actions — forms validate the contract’s schema and submit to the API, on every target.
  5. The session guard is universal. Route protection is a universal load that works in SSR and in the static SPA, never a server-only layout load.
  6. The UI learns capabilities from the API (system.authConfig, system.flags, billing.status) instead of mirroring server configuration into frontend env.
  7. Tenancy is enforced in the API from one shared role definition; the UI only hides what a role cannot do.
  8. Env is validated once (packages/env); SKIP_ENV_VALIDATION=1 only for steps that never boot the app.
  9. Generated code is regenerated, not edited (packages/db/src/schema/auth.ts, packages/db/drizzle/).
  10. One formatter and linter — Biome; tabs, single quotes, 100 columns.
  11. services/ai is not a public gateway. It is reached only by the API with a service token; the API owns identity, limits and metering.
apps/
web/ SvelteKit application — auth, orgs/teams, workspaces, billing, admin (thin client)
api/ Hono on Bun — Better Auth, oRPC router (RPC + REST/OpenAPI), webhooks, job worker
site/ Astro — marketing, pricing, blog, SEO
docs/ Astro Starlight — product documentation
native/ Tauri 2 — desktop + mobile shells around apps/web's static build (no IPC)
packages/
api-contract/ oRPC + Zod contract: the API's single source of truth
api-client/ typed client for the contract (browser, SSR, Tauri, Bun scripts)
auth/ Better Auth server instance (API) and Svelte client factory (frontends)
db/ Drizzle schema (generated auth tables + ours, incl. pgvector document_chunk), migrations, Bun.SQL client
billing/ plan catalog + Stripe client
email/ provider abstraction (Resend / console) + templates
env/ validated server environment (t3-env + Zod)
storage/ object storage for uploads (S3/R2 via Bun.S3Client, or local disk + signed URLs)
ui/ shared Svelte 5 components (shadcn-svelte/bits-ui + thin form wrappers) + Tailwind tokens
tsconfig/ shared TypeScript configs
services/
ai/ FastAPI service (uv) — internal, called by the API only: extract (+ OCR) · summarize · embed
infra/
compose.yml self-hosted stack: postgres (pgvector) · migrate · api · web · ai · caddy
(+ profiles: worker, backup, monitoring, observability)
compose.dev.yml Postgres for local development
caddy/ subdomains.Caddyfile (public) · single-origin.Caddyfile (tailnet/LAN)
docker/ Dockerfile.static (Astro sites → Caddy)
backup/ pg_dump + uploads, S3 copy, restore drill (`@repo/backup`)
scripts/ provision.sh · deploy.sh
env/ production .env, encrypted (SOPS + age)
loadtest/k6/ smoke.js
scripts/ licenses.ts (dependency-licence audit) · rename.ts (rename the kit)
docs/DECISIONS.md architecture decision records (D1–D30)
LICENSE.md · THIRD-PARTY.md · CHANGELOG.md · UPGRADING.md · SECURITY.md · CONTRIBUTING.md

Internal packages are consumed from source (no build step): Bun runs TypeScript natively and Vite compiles Svelte from the workspace. Versions of shared dependencies are pinned once in the root package.json catalog and referenced with catalog:.

Layer Choice Why (short)
Runtime · package manager · test runner Bun One tool for installs, scripts, the API runtime and tests
Task orchestration Turborepo Cached, graph-aware build/check/test
Lint · format Biome One config for TS/JSON/CSS + Svelte/Astro
Application UI SvelteKit 2 + Svelte 5 Every route works as SSR and SPA
Public sites Astro 7 (+ Starlight) Content, SEO, docs; static output
Styling Tailwind v4 Shared tokens in packages/ui/theme.css as light-dark() pairs
UI kit shadcn-svelte on bits-ui Generated into packages/ui, one copy for every surface
Forms · client cache sveltekit-superforms (SPA mode, Zod 4) · TanStack Query Schemas shared with the contract; keys and fetchers derived from it
API Hono + oRPC on Bun Contract-first, end-to-end types, native OpenAPI 3.1
Database Postgres 17 + pgvector + Drizzle SQL as the source of truth; Bun-native driver; embeddings in the same database
Auth · orgs · teams · admin Better Auth Organization (+teams), admin, bearer, OpenAPI plugins; Drizzle adapter
Billing Stripe via @better-auth/stripe Organization as customer, per-seat billing, webhooks handled
Desktop · mobile shells Tauri 2 System webview, small binaries, Android/iOS first-class
Compute services Python 3.13 + FastAPI, uv AI, data, document processing — behind the API
Edge · static hosting Cloudflare Workers static assets Astro sites + optional SvelteKit SSR
Server hosting Docker Compose + Caddy, Tailscale One container host or your VPS; private/public access

Full reasoning, alternatives considered and the trade-offs live in docs/DECISIONS.md in the repository.