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 depends on the transport, not the platform. 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 to 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.