Skip to content
Starterdough
Menu

Features

Everything before your first feature

Each item below is implemented once in the API and available on every surface. Where the source lives is noted so you can read the real thing.

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.

packages/auth · apps/web/src/routes/(auth)

  • Email + password with verification (default on in production), password reset, change email confirmed from the old address, delete account confirmed by email. Every email goes through one provider abstraction: console in development, Resend when a key is set.

  • Social sign-in with GitHub and Google is enabled by setting the client id and secret. The sign-in page asks the API which providers are live, so there is no second configuration in the frontend.

  • Two-factor authentication (TOTP + backup codes) and passkeys (WebAuthn), managed from the security settings. Users can list and revoke their sessions.

  • Rate limits on the auth routes and on every procedure. The client address comes from a single forwarded IP, which is what Caddy sends.

  • Browsers use an httpOnly cookie. The native shells use a bearer token. The UI code is identical.

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.

packages/auth/src/permissions.ts · apps/api/src/rpc/router.ts

  • Organizations own members, invitations and teams. Workspaces are scoped to an organization and optionally to a team. New sessions start in the user’s first organization; a switcher changes it.

  • Three roles (owner, admin, member) are defined in one file and used by the auth server, the auth client (to hide what a role cannot do) and the API router. Every workspace procedure refuses non-members.

  • Invitations are emailed as links that expire after 7 days. Plan limits for workspaces and seats are enforced server-side and reported to the UI; with billing off nothing is capped.

  • An audit log records who did what per organization; owners and admins read it from the organization page.

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.

packages/billing · /app/billing

  • One browser-safe plan catalog (names, prices, features and limits) renders the pricing page and the in-app billing page, and drives the API’s limits.

  • Checkout, the customer portal and cancel/restore are the plugin’s endpoints. Per-seat plans keep the seat quantity equal to the member count as people join and leave.

  • Active, trialing and past-due subscriptions entitle the plan. A failed payment raises a banner while Stripe retries instead of locking the organization out. Dunning, cancellation and trial-ending emails go to owners and admins.

  • Billing stays dormant until the Stripe keys and price ids are set. Without them nothing is capped, and the billing page says so.

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.

packages/storage · apps/api/src/jobs · services/ai

  • Files never travel through the API. The browser asks for a presigned PUT and uploads straight to the bucket (Cloudflare R2, AWS S3, MinIO), or to the API’s own disk when no bucket is configured. A confirm step verifies the stored object before the document counts as ready. Accepted types: PDF, plain text, Markdown, CSV, HTML, JSON, and PNG, JPEG, WebP and TIFF images. One environment variable caps the size of a single file (25 MiB by default).

  • Background work is a row in Postgres, claimed with FOR UPDATE SKIP LOCKED. There is no broker to run. Five job kinds ship: extract a document’s text, summarize a document, index a document for search, summarize raw text, embed raw text. Failures retry with backoff, a lost attempt is requeued, and cancelling a queued job refunds it.

  • Semantic search runs on pgvector inside the same Postgres. Indexing chunks the extracted text (about 1,200 characters with 150 of overlap, on paragraph and sentence boundaries), embeds the chunks in batches and stores each one with the model that produced it, so changing the embedding provider cannot mix vectors. Searching embeds the query and returns the nearest chunks of that workspace with a cosine score.

  • Images go through an OCR backend picked at start-up: the Tesseract CLI, any OpenAI-compatible vision model, or none. With none, extracting an image fails cleanly and refunds its credit.

  • Every job kind has a price in AI credits, charged against the organization’s monthly budget through an append-only ledger and refunded when a job fails for good. Storage is capped per workspace by the plan. Both limits are enforced in the API and shown as meters in the app.

  • The compute runs in a small FastAPI service behind the API. It is never public and is reached only with a service token. A deterministic local provider works offline on a fresh clone. One environment variable points it at any OpenAI-compatible model.

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.

apps/web/src/routes/(admin) · apps/api/src/rpc/admin.ts

  • A platform administrator is a user with the admin role, independent of organization roles. The first one is created from the command line; further admins are promoted in the UI.

  • Users: search, ban and unban with reason and expiry, set role, revoke sessions, impersonate for one hour with a visible banner and a way back.

  • Organizations: every tenant with members, workspaces, plan and subscription state; force-cancel a subscription at period end or immediately.

  • Feature flags with a global default and per-organization overrides; clients read the resolved map from one endpoint. A system page reports version, uptime, counts, database and migration state, AI service health and which optional subsystems are configured.

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.

packages/ui · apps/web/src/lib

  • Hand-written primitives plus shadcn-svelte components generated into one shared package, so every surface imports one copy. Design tokens are light-dark() pairs: the page follows the OS with no JavaScript, and the app adds a Light/Dark/System toggle.

  • Forms use sveltekit-superforms in SPA mode with Zod 4. There are no form actions, because the API is the backend on every target. Schemas come from the contract where a procedure exists.

  • Data fetching with TanStack Query over the contract: keys, fetchers and types derive from the procedures; mutations invalidate exactly what they changed.

  • App shell with skip link, responsive sidebar, command palette (Ctrl/⌘K), toasts after mutations, dialog confirmations for destructive actions, skeletons and empty states, error boundaries with a reference id. Optional Sentry and PostHog behind env switches; PostHog loads only after consent.

  • Accessibility is tested: axe (WCAG 2.2 AA) runs on the public pages in the end-to-end suite.

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.

apps/web/messages · apps/web/project.inlang

  • Messages live in apps/web/messages/{en,de}/*.json, one file per area of the app, and are compiled by Paraglide JS as part of the check and build steps.

  • Locale resolution: a cookie set by the switcher in Settings, then the browser’s Accept-Language, then English. No URL prefixes, so the same build works as a static SPA.

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.

apps/web/src/service-worker.ts · apps/web/src/lib/pwa.svelte.ts

  • The same URL serves the website, the installable PWA and the native shell’s remote-URL mode.

  • The service worker precaches the build, tries the network first and falls back to a prerendered offline page for navigations without a connection. It is never registered in development or inside the Tauri shells.

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.

apps/native

  • The Rust side is a small shell that opens a system webview on the SvelteKit static build. There are no Tauri commands and no per-platform data layer; native capabilities the web lacks are added as Tauri plugins.

  • Two distribution modes per platform: bundle the static build, or point the shell at the hosted app URL and ship the frontend by deploying the web app.

  • Auth switches to bearer tokens automatically in the static build, because a webview has no first-party cookies for the app origin.

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.

infra/compose.yml · infra/caddy

  • One compose file runs the whole stack on a VPS or a home server. Caddy in subdomains mode gives app., api. and docs. under your domain with automatic HTTPS; single-origin mode routes by path for tailnet or LAN use.

  • Tailscale on the host: `tailscale serve` exposes the stack to your devices, `tailscale funnel` makes the same URL public. No open ports, no DNS to manage, and TLS is handled.

  • The public sites are static and deploy to Cloudflare Workers static assets or to Caddy; the web app builds for Node, Cloudflare Workers or as a static SPA with one environment variable.

The stack

Chosen for one JavaScript toolchain, static public surfaces, and an API that runs on any long-lived host. The reasoning behind each pick is recorded in the repository’s decision log.

Layer Choice
Runtime, package manager, test runner Bun
Task orchestration Turborepo
Lint and format Biome
Application UI SvelteKit 2 + Svelte 5
Public sites Astro 7 (+ Starlight)
Styling Tailwind 4 with shared tokens
UI kit shadcn-svelte on bits-ui
Forms and client cache sveltekit-superforms (Zod 4) · TanStack Query
API Hono + oRPC on Bun
Database Postgres 17 + Drizzle
Auth, organizations, teams, admin Better Auth
Billing Stripe via @better-auth/stripe
Desktop and mobile shells Tauri 2
Compute services Python 3.13 + FastAPI
Edge and static hosting Cloudflare Workers static assets
Server hosting Docker Compose + Caddy, Tailscale

See it running

Sign up in the app, or read the guides for the details behind each feature.