Bun-native SaaS starter
Build once. Serve anywhere.
Starterdough is the Svelte and Bun answer to next-forge, Makerkit and ShipFast. Auth, organizations, teams, workspaces, billing and an admin surface, built once and served as a website, an installable PWA, a desktop app, a mobile app, or self-hosted on your own box.
- Bun
- Turborepo
- SvelteKit 3 + Svelte 5
- Astro 7
- Tailwind 4
- Hono + oRPC
- Postgres + Drizzle
- Better Auth
- Stripe
- Tauri 2
- FastAPI
- Docker + Caddy
One HTTP API, every surface
Every surface is a thin client of one HTTP API. The SvelteKit app, the Tauri desktop and mobile shells, the dynamic bits of the Astro sites, CLIs and the Python service all consume the same API: the same auth, the same procedures, the same OpenAPI document. No Tauri IPC, no per-platform data layer, no duplicated business logic.
-
Public sites
Astro, static: marketing, blog, docs
-
Application
SvelteKit, one codebase: SSR on Node or Cloudflare, static SPA, PWA install
-
Native shells
Tauri 2, no IPC: desktop (Windows, macOS, Linux) and mobile (Android, iOS)
The one API
Hono on Bun
- Better Auth: sessions, organizations, admin
- oRPC: typed RPC + REST with OpenAPI 3.1
- Stripe webhooks
-
Postgres
Drizzle on Bun’s native driver. The API is its only client
-
FastAPI service
AI, data and documents. Internal, reached only by the API with a service token
- Add a feature once.
- Declare it in the contract, implement it in the API, call it from any client with full types. REST and OpenAPI come for free.
- Ship the frontend anywhere.
- The app builds for Node, Cloudflare Workers or as a static SPA with one environment variable.
- Auth follows the transport.
- Browsers use an httpOnly cookie; the native shells use a bearer token. The UI code is identical.
- The database has exactly one client.
- Only the API touches Postgres. Frontends cannot reach it even by accident.
What ships
The parts every SaaS needs before its first feature, implemented once and available on every surface.
-
Authentication
Email and password with verification, password reset, change email, delete account. GitHub and Google switch on when their credentials are present. Two-factor, passkeys, session management and rate limits included.
-
Organizations, teams, workspaces
The organization is the tenant and billing entity, teams group its members, workspaces are the product container. Roles are defined once and enforced in the API. Every mutation is audited.
-
Billing
Stripe through the Better Auth plugin, with the organization as the customer. Plan catalog in code, per-seat plans follow membership, dunning emails and a payment-failed banner. Dormant until you add Stripe keys.
-
Documents and AI jobs
Presigned uploads to S3/R2 or the local disk with a per-workspace storage quota, a Postgres-backed job queue that extracts, summarizes and indexes them, semantic search over pgvector, OCR for images, and live progress over server-sent events.
-
Admin surface
Platform administrators search, ban and impersonate users, see tenants with their billing state, manage feature flags with per-organization overrides and check system health.
-
Frontend platform
shadcn-svelte components in a shared package, forms in SPA mode with Zod 4, TanStack Query derived from the API contract, command palette, toasts, skeletons and error boundaries. Dark mode follows the OS.
-
Internationalization
Paraglide JS with messages per locale. English and German ship. The locale comes from a cookie, then Accept-Language, then English; users switch it in Settings.
-
PWA and offline
Web manifest, an install button when the browser offers it, and a service worker that precaches the build and serves a prerendered offline page. Registered in production browsers only.
-
Desktop and mobile shells
Tauri 2 shells for Windows, macOS, Linux, Android and iOS around the same static build. No IPC: the shell talks to the API over HTTPS with a bearer token, exactly like the browser.
-
Self-hosting
Docker Compose with Postgres, the API, the web app, the AI service and Caddy. Two Caddy modes (public subdomains or a single origin) and Tailscale for access from anywhere with no open ports.
How a request flows
The same four steps on the web, in the desktop and mobile shells, and for third-party callers.
-
A component calls the typed client
A Svelte component calls api.workspaces.list({ organizationId }) from the shared API client.
-
The request goes over HTTP
The client POSTs to /rpc/workspaces/list. In the browser the session cookie rides along; in a native shell a bearer token is attached. During server rendering the origin is rewritten to the internal API and the browser’s cookies are forwarded.
-
The API resolves the session and runs the procedure
Hono routes to the oRPC handler, requireAuth resolves the session through Better Auth, and the procedure runs against Drizzle.
-
The same procedure is plain REST
GET /api/v1/workspaces?organizationId=… returns the same data and is documented at /api/v1/openapi.json. That is what the Python service, curl and third parties use.
Start with the repository
Bun, Docker for Postgres, and a handful of commands. Python and Rust are only needed for the AI service and the native shells.
bun install
bun run setup # every .env, with generated secrets
bun run db:up && bun run db:migrate
bun run dev:app # api on :3000 + web on :5173