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 --> AIConsequences you feel every day
Section titled “Consequences you feel every day”- Add a feature once. Declare it in
packages/api-contract, implement it inapps/api, call it from any client with full types (api.workspaces.create({...})). REST + OpenAPI come for free. - Ship the frontend anywhere.
apps/webbuilds 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.SQLthrough Drizzle). Frontends cannot reach it even by accident.
How a request flows
Section titled “How a request flows”- A Svelte component calls
api.workspaces.list({ organizationId })(packages/api-client). - The oRPC link POSTs to
${PUBLIC_API_URL}/rpc/workspaces/list. In the browser the session cookie rides along (credentials: include); in a Tauri shellAuthorization: Bearer …is attached fromlocalStorage. During SSR,hooks.server.tsrewrites the origin to the internalAPI_URLand forwards the browser’s cookies. - Hono routes to the oRPC handler;
requireAuthresolves the session via Better Auth (auth.api.getSession) and the procedure runs against Drizzle. - 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.
Hard rules
Section titled “Hard rules”These are the rules the codebase is built around. Breaking one usually means a second source of truth is being created.
- Frontends never import server packages.
@repo/db,@repo/auth/server,@repo/envand@repo/billingare never imported byapps/weborapps/site. The browser-safe surfaces are@repo/auth/client,@repo/auth/permissions,@repo/billing/catalog,@repo/api-clientand@repo/ui. - Contract first. No endpoint without a contract entry; no client without the typed client.
A new feature is: contract in
packages/api-contract→ implementation inapps/api/src/rpc/router.ts→ the UI calls the typed client. REST and OpenAPI follow. - 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.
- SSR is rendering, not a backend.
apps/webhas no server-side business logic;loadfunctions call the API. No form actions — forms validate the contract’s schema and submit to the API, on every target. - The session guard is universal. Route protection is a universal
loadthat works in SSR and in the static SPA, never a server-only layout load. - The UI learns capabilities from the API (
system.authConfig,system.flags,billing.status) instead of mirroring server configuration into frontend env. - Tenancy is enforced in the API from one shared role definition; the UI only hides what a role cannot do.
- Env is validated once (
packages/env);SKIP_ENV_VALIDATION=1only for steps that never boot the app. - Generated code is regenerated, not edited (
packages/db/src/schema/auth.ts,packages/db/drizzle/). - One formatter and linter — Biome; tabs, single quotes, 100 columns.
services/aiis not a public gateway. It is reached only by the API with a service token; the API owns identity, limits and metering.
Repository map
Section titled “Repository map”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 configsservices/ ai/ FastAPI service (uv) — internal, called by the API only: extract (+ OCR) · summarize · embedinfra/ 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.jsscripts/ 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.mdInternal 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.