Skip to content

Repository files navigation

clean-stack

The SaaS boilerplate that says no. Auth, multi-tenant, email, storage already wired. You clone, you write business logic — everything else is settled.

Bun + Hono API · Vite + React 19 + TanStack app · Drizzle + Postgres · DDD-kit for the domain · BetterAuth + Resend + R2 for the SaaS layer.


Status — work in progress. This boilerplate is under active iteration. No support, no SLA, no public issue queue.


Quick start

Two ways to run it. Pick one.

Option A — Native (fastest hot reload)

PrerequisitesBun 1.4+, Node 24.20+, pnpm 11, Docker with Compose v2.

git clone https://github.com/axelhamil/clean-stack my-saas
cd my-saas
pnpm install
pnpm bootstrap # copies .env.example → .env in each workspace
docker compose up postgres -d # only Postgres on :5433 (api/app run natively below)
pnpm db:migrate # apply migrations
pnpm dev # API :3000, App :5173

Option B — Fully containerized (Docker only)

No Bun, no Node, no pnpm on your host. Everything runs in containers (api + app + Postgres) with hot reload via compose develop.watch.

PrerequisitesDocker with Compose v2.22+ (the develop.watch minimum). Follow the official guide for your OS — do not use apt install docker.io (Canonical's package, not Docker Inc.).

OSOfficial guide
macOSDocker Desktop for Mac
WindowsDocker Desktop for Windows (WSL2)
DebianInstall Docker Compose on Debian
UbuntuInstall Docker Compose on Ubuntu
FedoraInstall Docker Compose on Fedora
RHEL / Rocky / AlmaInstall Docker Compose on RHEL
Arch / Manjaroextra/docker + extra/docker-compose (already v2, no plugin needed)
NixOSDocker — NixOS Wiki

v1 (docker-compose, hyphen) is EOL since June 2023 — always use docker compose (space).

git clone https://github.com/axelhamil/clean-stack my-saas
cd my-saas
bash scripts/bootstrap.sh # copies .env.example → .env in each workspace
docker compose up --watch # api runs migrations on boot, then starts

Open http://localhost:5173, sign up with any email, you're in.


Why bother

Most SaaS templates ship a half-baked auth you'll rip out and zero opinion on what goes where. This one starts from the opposite premise.

clean-stack
AuthBetterAuth — passkeys (WebAuthn), 2FA (TOTP + backup codes), magic-link, DB-backed sessions, cross-tab sync via BroadcastChannel. Bearer alongside cookies for Capacitor. Native Bun + Hono, no hacks.
Multi-tenantorganizationId FK on every business table from migration #1 + ScopedRepository enforces it at the port. Personal org auto-created on signup, role-based invitations, ownership transfer. Bolting tenancy on later is hell — the reverse is free.
AuthorizationCapability-based SSOT (@packages/access-control). Same predicate at server middleware, route beforeLoad gate, and <Can requires={...}> UI — drift impossible by construction.
Legal / complianceDay-one EU-legal across the GDPR surface: Art. 7 privacy/terms versioning (re-acceptance gate when a version bumps, user.policy.accepted evidence), Art. 16 rectification (profile + email-change + password), Art. 17 erasure (POST /me/delete — 2FA-required, 7-day soft-delete grace, sole-owner preflight, cancel-on-sign-in, cron wipe + ref anonymization), Art. 20 portability (POST /me/export — signed 7-day R2 URL, 1/24h throttle). Passwords follow NIST SP 800-63B (min 15, HIBP k-anonymity breach check, contextual ban-list, no forced complexity). Without this, fines up to 4% of revenue.
Direct uploadsThree-step presign → PUT direct to provider → server HeadObject confirm. Server is blind during transfer; owner-scoped keys (<userId>/<scope>/<uuid>-<filename>); R2 / S3 / B2 / Wasabi / Tigris — provider swap = one env var.
Internal endpoints/internal/* (cron, queues) HMAC-SHA256-signed (X-Internal-Signature); signing key never on the wire. Stack private-network on Railway/Fly via INTERNAL_AUTH_LAYERS for defense-in-depth.
Event-driven + auditTransactional outbox + LISTEN/NOTIFY dispatcher → 82 typed events auto-emitted on every state change (35 public + 47 internal), append-only audit_log (SOC2 §CC7.2 / RGPD Art. 30), outbound webhooks (HMAC-signed, AEAD-encrypted secrets, decorrelated-jitter retry → dead-letter) — operator UI at /settings/webhooks, public event catalog at /developers/events. Each audit row carries the request's X-Request-Id via an AsyncLocalStorage context — one key joins an audit entry to its logs and Sentry event.
DDD scopeReserved for what your customers pay for. Not for billing, auth, gating, or quotas (config + middleware suffices). Lesson learned the hard way: ~70% less code than full-DDD on the SaaS plumbing.
Type safetyHono RPC end-to-end (hcWithType). No client to write, no schema to sync, refactor in API → red squiggle in App on save.
PerformanceBun-native Bun.serve() (~7 ms cold). Route-level code-splitting on the front (initial bundle ~355 KB, route chunks 1–43 KB) + defaultPreload: "intent" — perceived latency near zero.
AI-pair readyCLAUDE.md at the root + per-layer sub-CLAUDE.md. Your agent already knows the rules — Result/Option, no throw in domain, capability gates, vertical-slice modules.
Zero-warning pipelineHusky + lint-staged + commitlint + pre-push CI (Biome, knip, jscpd, type-check). Conventional Commits enforced; devmain merge triggers semantic-release. No --no-verify shortcut.

Features

Everything wired today, and the short list of what's left. Prefer prose? docs/OVERVIEW.md is the guided tour. Full inventory in docs/FEATURES.md; detailed plan with constraints in ROADMAP.md.

Shipped

Auth & identity

  • Email + password (required verification + reset), magic-link, passkeys (WebAuthn), 2FA (TOTP + backup codes)
  • Active-session list & revoke · bearer tokens alongside cookies · 5-min session cookie cache · cross-tab sync (BroadcastChannel)
  • NIST SP 800-63B password baseline — min 15, HIBP k-anonymity breach check (fail-open), contextual ban-list, no forced complexity

Multi-tenant & authorization

  • organization plugin — Personal org auto-heal, team orgs, email invitations, role-based members, ownership transfer
  • Capability-based authorization SSOT (@packages/access-control) — same predicate at server middleware, route beforeLoad gate, and <Can> UI
  • Enterprise SSO + SCIM (business tier) — per-org OIDC/SAML providers with DNS domain verification, JIT provisioning, a domain-wide enforce-SSO toggle closing all four sign-in paths, SCIM 2.0 Users CRUD (RFC 7644) for directory-driven provisioning and deprovisioning, local Keycloak profile for development

Security & hardeningthe deploy-safe perimeter (Phase C.1)

  • Unified rate-limit — one Hono middleware (rate-limiter-flexible, BetterAuth built-in disabled) → global + 8 auth-burst policies, multi-window, IETF RateLimit/RateLimit-Policy/Retry-After headers, fail-closed on auth (a store outage can't silently disable brute-force protection — OWASP A10:2025), trusted-proxy IP resolution (private/CIDR/exact, OWASP rightmost-non-trusted), memory → Postgres (dedicated pool) stores
  • Strict CSP — per-request nonce (Caddy native {http.request.uuid} + Vite html.cspNonce), 'strict-dynamic', public /csp-report (IP-rate-limited + cross-origin CORP + document-uri origin filter) → security.csp.violation audit event
  • CSRF — Origin-allowlist on unsafe methods (stateless, no token/cookie/endpoint — the Next.js Server Actions / SvelteKit model), reuses the CORS allowlist as SSOT, Bearer-skip for Capacitor, security.csrf.rejected audit event
  • Hardened headers (HSTS, CSP frame-ancestors 'none', nosniff, Referrer-Policy, Permissions-Policy via Caddy) · credentialed CORS allowlist · prod boot fails hard on missing CORS_ORIGIN / signing keys
  • Abuse prevention (Phase C.1 s5a) — disposable/throwaway email domains refused at sign-up (~90k-domain list + DNS MX-record check, fail-open, DISPOSABLE_EMAIL_BLOCK_ENABLED) → security.signup.rejected; HIBP breach hit at sign-up/reset/change → security.password.breached

Legal / compliance (GDPR)

  • Art. 7 — privacy/terms versioning + re-acceptance gate (@packages/policies, /legal/accept)
  • Art. 16 — rectification (profile, email-change, password)
  • Art. 17 — erasure (2FA-gated, 7-day grace, sole-owner preflight, cron wipe + ref anonymization)
  • Art. 20 — portability (signed 7-day R2 export, 1/24h throttle)

Storage & email

  • Direct uploads — three-step presign → PUT → confirm, owner-scoped keys (R2 / S3 / B2 / Wasabi / Tigris)
  • Transactional email (Resend) — typed templates, idempotency, provider-side suppression

Event-driven core & transactionsthe hard distributed-systems part, already solved

  • Transactional outbox — domain events persisted in the same DB transaction as the state change (IUnitOfWork.run() + EventCollector AsyncLocalStorage) → no event ever lost, none emitted for a rolled-back write (the dual-write problem, solved)
  • Post-commit dispatch — Postgres LISTEN/NOTIFY + SELECT … FOR UPDATE SKIP LOCKED drain (multi-instance safe); built-in subscribers run inside the dispatch TX (audit + webhook fanout + notification fanout), user onEvent(...) handlers isolated post-commit
  • 82 typed events (35 public + 47 internal), append-only audit_log (operational 90d / compliance 7y), HMAC-signed webhooks (AEAD-encrypted secrets, decorrelated-jitter retry → dead-letter, replay, SSRF guard, dual-secret rotation), X-Request-Id correlation
  • Zero plumbing post-clone — declare the event in @packages/eventsaddEvent() in the aggregate → run via uow.run(); the audit row, webhook fanout, and in-process handlers (auto-discovered via inwire) come for free
  • Internal /internal/* endpoints — HMAC-signed, optional private-network layer

Architecture & conventionsthe part that keeps shipping fast sustainable

  • Clean Architecture + DDD enforced by structure — layers import inward, modules never import each other (only via domain events / shared ports), each module removable by a documented contract (trash + line delete, tsc points to the rest)
  • 8 cross-cutting rules, each tied to an architectural property with its why — every state change emits a typed event (audit + webhooks opt-out, not opt-in) · every I/O method spans + captures (no silent prod failure) · every event payload names its actor (RGPD forensic trail) · internal packages ship source not artifacts · ORM-first, raw SQL only where the ORM can't model it
  • DDD scoped to the business domain only — never billing / auth / gating / quotas (config + middleware suffices); the discipline that cut ~70% of code vs full-DDD plumbing (lesson learned the hard way)
  • Domain & repo rules — no throw in domain/application (Result), no null for absence (Option), value objects validated via zod, aggregates expose only get id(); ownership enforced at the port (ScopedRepository scopes organizationId/userId across HTTP, cron, queue, events — wrong owner leaks nothing)
  • Rules live next to the code — root + per-layer CLAUDE.md, recursively auto-loaded; conventions are executable guardrails for AI agents and humans, not tribal knowledge

Building blocks

  • DDD-kit (@packages/ddd-kit) — Result / Option, Entity / Aggregate / ValueObject (zod-validated) / UUID / DomainEvent, BaseRepository / ScopedRepository / IUnitOfWork
  • Vertical-slice modular monolith — modules/<context>/{domain,application,infrastructure} + per-feature front slices
  • CQRS (commands via use-cases, queries direct to Drizzle) · inwire DI (type-inference container, no declared interfaces)
  • Drizzle + Postgres 18 — TransactionService, org-scoped withOrg(table, orgId) helper, dedicated port 5433

Frontend & UI

  • App shell — sticky top-nav, org switcher, theme toggle, user menu, ⌘K command palette (capability-filtered)
  • TanStack Router file-based routing (routes.ts virtual config → generated, versioned routeTree.gen.ts) with a 2-file feature pattern (route + lazy component), near-zero TanStack Start migration
  • Route-level code-splitting + defaultPreload: "intent" (initial bundle ~355 KB, route chunks 1–43 KB)
  • TanStack Query server-state · RHF + zod forms (loose/strict schema split) · next-themes + View Transitions theme · sonner toasts
  • Typed i18n (@packages/i18n) — en/fr.ts catalogs bound to t() through CustomTypeOptions, so an unknown key fails tsc; locale from cookie then user.locale, never from the URL; en/fr key parity enforced by a test
  • shadcn-pure UI kit (@packages/ui) — typography exports + custom primitives (NavLink, TextLink, FormTextField, DestructiveActionDialog, ListRow), theme tokens, <Can> authz component

Infra, ops & DX

  • Hono RPC end-to-end types (hcWithType) · pino logging with single error envelope · CQRS error contract
  • Health probes (/livez/readyz/startupz) + graceful shutdown · Sentry error tracking (RGPD-scrubbed, NoOp without DSN)
  • Disaster-recovery runbook (PITR-first) · Railway reference deploy (config-as-code)
  • pnpm bootstrap clone-ability · Docker dev (native hot-reload or fully containerized compose watch) · Turborepo
  • Zero-warning pipeline (Biome · knip · jscpd · type-check · commitlint · semantic-release)
  • WCAG 2.1 AA gate on every PR — @axe-core/playwright over 11 pages in light and dark, plus landmark, keyboard, focus-trap and reduced-motion checks

Roadmap

The SaaS plumbing is shipped — auth, multi-tenant, RGPD, consent, billing, quota gating, security perimeter, admin + impersonation, API tokens, audit log, webhooks, in-app notifications, durable email, enterprise SSO + SCIM, typed i18n (en/fr), the 82-event rail, Railway deploy. What's left is deliberately short; the rest was cut rather than carried as a wishlist. Full detail in ROADMAP.md.

  • A manual review pass over the shipped surface — the last step before this is genuinely clone-ready.
  • C.1 S5b/S6 — impossible-travel, ASN deny-list, captcha hook. Deferred for calibration: these need real traffic to tune.
  • D.5 known debts — three, including a duplicate deletion email if the worker crashes mid-batch.

Cut and not returning: marketing site, status page + SLO dashboards, SOC2 checklist, OpenAPI docs, Capacitor shell, feature flags, GPC. Each was a per-clone decision, a third-party service to wire, or an ambition with nothing to exercise it. The specs live in git history.


Infrastructure

What's in docker-compose.yaml, what's optional, and how dev maps to prod.

Database — Postgres 18

Imagepostgres:18-alpine
Host port5433 (deliberately not the default 5432 — avoids collision with a system Postgres)
In-networkpostgres:5432 (used by the api container)
Volumepostgres_data (persistent across compose downnot across a Postgres major bump, see below)
Healthcheckpg_isready every 5 s — api waits for healthy before starting
SchemaDrizzle ORM — sources in packages/drizzle/src/schema/

Action required if you already had postgres_data populated before this branch: the volume mount moved from /var/lib/postgresql/data to /var/lib/postgresql (Postgres 18+ images store PGDATA in a major-version-specific subdirectory — docker-library/postgres#1259). An existing volume won't crash on docker compose up — it will silently reinitialize as an empty database, since the image finds nothing at the new mount point and just creates a fresh one. Run docker volume rm clean-stack_postgres_data once before the first up on this branch, then rebuild dev data with pnpm db:push && pnpm db:seed.

pnpm db:push # dev — push schema directly (drizzle-kit push --force, non-TTY safe)
pnpm db:generate # generate a SQL migration from a schema diff
pnpm db:migrate # apply migrations (prod-style — ship as artifacts)
pnpm db:studio # browse data in Drizzle Studio

db:push runs with --force because Turbo pipes stdout in non-TTY, and drizzle-kit's interactive data-loss prompt would otherwise hang. Safe in dev. Prod uses db:migrate which doesn't prompt.

Storage — S3-compatible (opt-in)

Object storage for uploads. Off by default — the storage profile is opt-in. Turn it on when you need it.

Dev imagechrislusf/seaweedfs — Apache 2.0, ~96 MB (why not MinIO?)
Compose profilestorage (off by default)
Host portpinned to 8333 (ports: ["8333:8333"])
In-networkseaweedfs:8333 (set as S3_ENDPOINT for the api)
Bucketclean-stack — auto-created on startup by the seaweedfs-init sidecar
Authnone required in dev — accepts any access key / secret
Prod targetCloudflare R2 (zero egress fees) — same SDK, swap via S3_ENDPOINT + creds
docker compose --profile storage up -d # start SeaweedFS + bucket init

The S3 client is provider-agnostic (region: "auto", forcePathStyle: true). Anything S3-compatible works: R2, AWS S3, Backblaze B2, Wasabi, Scaleway, Tigris.

Email — Resend (optional)

Resend is optional everywhere — without RESEND_API_KEY (or with unconfigured template IDs), email sends are logged at warn and the app boots normally; verification, magic-link, password-reset and org-invitation mails simply don't deliver. Wire it before relying on any email flow in prod.

SDKresend
Templatesdashboard-managed, retry + idempotency, provider-side suppression
DNS prereq prodSPF + DKIM (3 CNAMEs) + DMARC — see docs/INTEGRATIONS.md
EU regionsupported via region: "eu-west-1" in adapter config

Billing — Stripe (opt-in)

Billing is wired via @better-auth/stripe (plugin v1.6+, SDK stripe@22+). Off until you add the two required env vars. Without them the billing routes return 402 for all subscription calls; the rest of the app is unaffected.

Packagesstripe@22.3.0 + @better-auth/stripe@1.6.23
Webhook endpointauto-mounted at /api/auth/stripe/webhook by the plugin
Subscription SSOTplugin-managed subscription table (webhook-synced) — not organization.metadata
Entitlementstyped ENTITLEMENTS[tier] in apps/api/src/modules/billing/config.ts — edit + deploy to change gates
Tiersfree (3 members) · pro (20 members, audit_log + api) · business (unlimited members + SSO)
UnlimitedmaxMembers: null (JSON-safe) — not Infinity
Org modelunlimited team orgs per user; each is free or paid independently (seat-only gate)

Required env vars (apps/api/.env):

STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

Dev webhook forwarding (forwards Stripe events to your local API):

stripe listen --forward-to localhost:3000/api/auth/stripe/webhook

Stripe product metadata contract — each paid Product (Pro, Business) in the Stripe dashboard must carry:

  • metadata.tier = the tier key (pro or business) matching a key in ENTITLEMENTS. This is the join key between Stripe and the entitlements config.
  • marketing_features (Stripe product field) = array of bullet strings shown in the /pricing UI and Stripe Checkout.

Entitlements (features, rank, maxMembers) live exclusively in code at apps/api/src/modules/billing/config.ts. The Stripe dashboard never controls gates — it only carries the price and display copy. Changing a gate = a code change + deploy (reviewable, version-controlled).

Containers at a glance

ServiceImagePort (host)ProfilePersistent volume
postgrespostgres:18-alpine5433defaultpostgres_data
apibuilt (apps/api/dev.Dockerfile)3000default
appbuilt (apps/app/dev.Dockerfile)5173default
seaweedfschrislusf/seaweedfs8333storageseaweedfs_data
seaweedfs-initchrislusf/seaweedfsstorage

Environment variables

Three .env files, on purpose. Do not collapse them into one at the root — it's a safety rail, not an oversight.

FileHoldsWhy isolated
apps/api/.envDB password, auth secret, S3 keys, Resend key, RGPD/storage limitsBackend-only — never travels to the browser
apps/app/.envVITE_API_URL and other VITE_* varsVite inlines VITE_* into the client bundle — these are publicly visible. Splitting prevents a backend secret from ending up in the JS bundle by mistake
packages/drizzle/.envDATABASE_URLConsumed by drizzle-kit at migration time (separate process, separate cwd)

pnpm bootstrap (or bash scripts/bootstrap.sh) copies each .env.example.env if missing. Idempotent — never overwrites.

Only three variables are required to boot in dev: DATABASE_URL, BETTER_AUTH_URL, BETTER_AUTH_SECRET. Everything else is optional with sensible behavior when missing (storage off, email warns, etc.). In production the api additionally fails hard if CORS_ORIGIN, INTERNAL_SIGNING_KEY, INTERNAL_AUTH_LAYERS (incl. signature), WEBHOOK_MASTER_KEY, or API_TOKEN_PEPPER is missing — a silent fallback there is worse than a refused boot. API_TOKEN_PEPPER (min 32 chars, openssl rand -hex 32) is the server secret that makes a database dump insufficient to forge tokens; API_TOKEN_PREFIX should be unique per clone (e.g. myapp_). Behind a proxy/PaaS, set TRUSTED_PROXIES=private (Railway/Fly) or every request shares the LB IP as rate-limit key (collective lockout). See apps/api/.env.example for the full template.


Deployment

The api ships an always-on event-driven rail (transactional outbox + Postgres LISTEN/NOTIFY dispatcher + webhook delivery worker, all in-process). Two deployment shapes follow from this:

Compatible — Railway, Fly.io, Render, Coolify, dedicated VM, Kubernetes/EKS/GKE/AKS. Anywhere the api process stays alive between requests.

🟡 Compatible with min_instances ≥ 1 — Google Cloud Run, AWS App Runner, Azure Container Apps. They scale-to-zero by default; with zero replicas the dispatcher dies and outbox_event rows pile up unhandled. Pin minimum 1 instance (Cloud Run: --min-instances=1) and you're fine.

Incompatible without re-wiring — Vercel Functions, Netlify Functions, AWS Lambda, Cloudflare Workers, edge runtimes generally. Functions terminate after the response, killing LISTEN. To go serverless, swap the in-process dispatcher for a cron-triggered drain endpoint or an external queue (Inngest, QStash, SQS) — see docs/EVENTS.md for the workaround. Edge runtimes also can't run the Postgres LISTEN client at all — keep the api on a regular runtime; if you need edge for specific endpoints, split them into a separate service.

Reference deploy (Railway) — a prod-validated config-as-code runbook for the 3 services (api + app + cron) + Postgres + R2: env baseline, per-service infra/railway/*.toml, the boot-trap gotchas hit in practice (NODE_ENV override, app start-command, cross-site cookies), cookie strategy by domain topology, and a provider-swap section (Fly / Render / Cloud Run — Dockerfiles are portable). See docs/DEPLOY-RAILWAY.md.

Health probes — three endpoints (/livez, /readyz, /startupz) following K8s 2026 convention + IETF draft-inadarei format, with tri-state aggregation (pass/warn/fail) and SIGTERM-driven graceful shutdown. Per-PaaS recipes (Railway, Fly, Render, K8s, Cloud Run) in docs/HEALTH-PROBES.md.

Disaster recovery — PITR-first (delegated to your managed Postgres provider), with copy-paste recipes for a weekly portable pg_dump export and a monthly automated restore-test. RPO/RTO targets, restore runbook, lifecycle + versioning snippets in docs/DISASTER-RECOVERY.md.

Observability — error tracking via Sentry on api + app, RGPD-clean payload scrubbing by default, pino integration for log breadcrumbs, global TanStack Query/mutation error capture + user-id tagging on the front, NoOp without SENTRY_DSN. OpenTelemetry tracing and Prometheus /metrics are deferred to Phase D.1 (managed alongside dashboards). Port usage, removability runbook, provider swap recipe (GlitchTip / Highlight) in docs/OBSERVABILITY.md.


Stack

LayerChoice
RuntimeBun 1.3+ (api, scripts, tests) · Node 24+ for tooling
APIHono 4 on native Bun.serve()
AppVite 8 · React 19 · TanStack Router/Query · Tailwind 4 · shadcn/ui
AuthBetterAuth + organization, twoFactor, passkey, magicLink, bearer
EmailResend (typed templates, idempotency, provider-side suppression)
StorageCloudflare R2 prod · SeaweedFS dev (S3-compatible, opt-in)
DBDrizzle ORM + Postgres 18
API ↔ AppHono RPC (hcWithType) — end-to-end types
i18ni18next + react-i18next — typed .ts catalogs (@packages/i18n), locale outside the URL
DDD@packages/ddd-kit (Result, Option, Aggregate, ScopedRepository, …)
Toolingpnpm 11 · Turborepo · Biome 2 · Husky · semantic-release · knip · jscpd

Documentation

Guided tourdocs/OVERVIEW.md — what you get, in plain terms
What ships todaydocs/FEATURES.md — file-level inventory
What's nextROADMAP.md — plumbing shipped; left: review pass, abuse signals, D.5 debts
Architecture rulesCLAUDE.md (root) and the per-layer sub-CLAUDE.md
Back ↔ front surface mapdocs/SURFACE.md — every live route, its front consumer or the reason it has none, enforced by a parity test
Integrationsdocs/INTEGRATIONS.md — BetterAuth, Stripe, Resend, R2, email DNS
Eventsdocs/EVENTS.md — DX guide · docs/EVENT_PIPELINE.md — visual walkthrough
Health probesdocs/HEALTH-PROBES.md — endpoints, registry, graceful shutdown, per-PaaS recipes
Disaster recoverydocs/DISASTER-RECOVERY.md — PITR-first, restore runbook, weekly export + monthly restore-test recipes
Observabilitydocs/OBSERVABILITY.md — Sentry api+app, RGPD scrubbing, removability runbook, provider swap recipe
Deploy (Railway)docs/DEPLOY-RAILWAY.md — config-as-code runbook, boot-trap gotchas, cookie topology, provider swap
Historydocs/HISTORY.md — design decisions trail

Scripts

pnpm bootstrap # copy .env.example → .env in each workspace (idempotent)
pnpm dev # Turbo TUI (all apps)
pnpm dev:docker # containerized dev (compose up --watch)
pnpm build # full build with parallel type-check
pnpm test# bun test (api) + vitest (rest)
pnpm ci:check # pre-push pipeline (Biome, type-check, knip, jscpd)
pnpm db:push # dev schema push
pnpm db:migrate # prod-style migration
pnpm db:studio # Drizzle Studio

Made by Axel Hamilcaro · © 2026-present — All rights reserved

About

SaaS-ready boilerplate. BetterAuth + multi-tenant + Resend + R2 already wired (Stripe on the roadmap). Bun, Hono, React 19, TanStack, Drizzle, Postgres. Clean Architecture + pragmatic DDD. Fourteen non-negotiable architecture rules, zero-warning pipeline, AI-pair ready.

Topics

Resources

Stars

17 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages