This template is a backend-agnostic admin substrate, not a starter welded to one database. The portable layer — page archetypes, the form/table/chart system, query conventions, the agent skills — is written against two seams and never against a specific backend. Pick a backend per project by swapping what sits behind those seams; everything above stays the same.
| Concern | Seam | Default | Swap point |
|---|---|---|---|
| Business data (your resources) | Repository<T, TInput> |
drizzleRepository (Postgres); memoryRepository when no DB |
each resource's server.ts |
| Auth (server) | AuthProvider |
betterAuthProvider |
the authProvider binding |
| Auth (browser) | @/lib/auth-client |
better-auth React client | reimplement that file |
- Platform data — users, sessions, later RBAC. Owned by the auth preset;
must persist reliably. Swap via
AuthProvider+auth-client. - Business data — your resources (products, orders, …). Owned by each
resource. Swap via the
Repositoryadapter inserver.ts.
Conflating the two is what makes people think "the template forces Postgres." It doesn't: business data is backend-agnostic by construction, and the platform backend is a default you can replace.
With no DATABASE_URL the app boots on in-memory presets:
- Auth uses better-auth's in-memory adapter (
@/lib/auth.tspicks it viahasDatabase). Accounts reset on server restart; register on first run. - Resources use
memoryRepositorywith the seed infeatures/<name>/demo-data.ts.
So bun dev works with no Docker/Postgres — build and demo the UI before choosing
a backend. @/lib/backend.ts exposes hasDatabase; it pulls in no DB client, so
it is safe to read from a client-reachable server.ts.
Set DATABASE_URL (copy .env.example to .env, then bun run db:up) to switch
the whole stack onto Postgres + Drizzle + better-auth.
Use the add-backend skill. It ships six ready-to-run backend presets plus the
frontend wiring to connect each to this app through the two seams — no page/query/table/
form changes:
| Preset | Stack | Frontend wiring |
|---|---|---|
tanstack-drizzle-betterauth |
TanStack Start + Drizzle + better-auth (in-process — this app's default) | none |
hono-drizzle-betterauth |
Hono + Drizzle + better-auth (standalone TS service) | restRepository + remoteBetterAuthProvider |
hono-prisma-betterauth |
Hono + Prisma + better-auth (standalone TS service) | restRepository + remoteBetterAuthProvider |
hono-drizzle-authjs |
Hono + Drizzle + Auth.js / NextAuth v5 (standalone TS service) | restRepository + remoteAuthjsProvider |
fastapi-sqlalchemy-jwt |
FastAPI + SQLAlchemy + JWT (standalone Python service) | restRepository + externalJwtAuthProvider |
supabase |
Supabase Postgres + Auth (BaaS) | supabaseRepository + Supabase AuthProvider |
The preset sources live in backends/ (their in-repo source of truth,
each independently runnable + tested); the skill carries them under
.claude/skills/add-backend/templates/. The two HTTP-API presets' auth providers ship
pre-wired and typechecked in src/lib/auth-providers/. In
short, the localized building blocks are:
- External REST/GraphQL data → bind
restRepository/graphqlRepositoryin the resource'sserver.ts(seedata-adapters.mdand theadd-backendskill). Nothing else in the vertical changes. - Supabase / external-API auth → point
authProviderat the matching provider insrc/lib/auth-providers/(or the Supabase copy-ready wiring) and swap@/lib/auth-client.
Drizzle is multi-dialect, so you keep drizzleRepository and change only the
preset plumbing:
src/db/index.ts— swap the driver (drizzle-orm/mysql2,…/better-sqlite3, …).src/db/schema.ts—mysqlTable/sqliteTable+ that dialect's column types.src/lib/auth.ts— better-authprovider: "mysql" | "sqlite".drizzle.config.ts— thedialect.- Make
drizzleRepository's case-insensitive search dialect-aware (it uses Postgresiliketoday).
- Data crosses the boundary only through
createServerFn, each handler callingrequireUser()and validating input with Zod. requireUser/ the_appguard / route context all speak the normalizedAuthSession({ user: { id, email, name } }), never a vendor session shape.- Adapters/SDK clients and secrets stay server-side — never statically imported from a client-reachable module.