Add backend
Everything about the data layer — pick one of six ready-to-run backend templates (TanStack Start + Drizzle + better-auth, Hono + Drizzle + better-auth, Hono + Prisma + better-auth, Hono + Drizzle + Auth.js/NextAuth, FastAPI + SQLAlchemy + JWT, Supabase) and wire the frontend to it through the Repository + AuthProvider seams; scaffold a CRUD resource (Postgres table + server fns + query hooks + DataTable page + create/edit dialog + sidebar entry); bind or swap one resource's data source (Drizzle / REST / GraphQL / in-memory); or re-point the whole app's data + auth at a different backend. Use when adding a data entity or pointing the app at a backend other than the default Postgres + better-auth.From its SKILL.md
npx -y skills add ahpxex/open-dashboard --skill add-backendAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- reads credentialsReads from 4 credential sources: `process.env.WIDGETS_API_URL` and 3 more.
- runs commandsInstructs the agent to run 4 commands, including `bun run create-resource <plural-name>` and 3 more.
SKILL.md
14.1 KB, ~3.5k tokens by cl100k_base, as published. Nobody here has run it
Add a backend (resources, data sources & presets)
The app is backend-agnostic by design: it reaches data and auth only through two seams, so adding a resource, swapping one resource's data source, or re-pointing the whole app at a different backend are all localized operations.
Routing rule: use add-backend whenever a screen needs its own data (a new
entity/table, or a different backend); use add-component when composing UI on top
of data that already exists.
| Concern | Seam | Default preset | Swap point |
|---|---|---|---|
| Business data (resources) | Repository<T, TInput> (@/infra/data/repository) | Postgres via drizzleRepository; in-memory when no DB | each resource's server.ts binding |
| Auth (server) | AuthProvider (@/lib/auth-provider) | better-auth (betterAuthProvider) | the authProvider binding |
| Auth (browser) | @/lib/auth-client | better-auth React client | reimplement that one file |
Zero-config (bun dev, no DATABASE_URL) already runs on in-memory presets, so you
can build UI before wiring a real backend. Full guides: docs/data-adapters.md,
docs/backends.md.
Backend presets — ready-to-run templates
Six runnable backend templates ship in this skill's templates/<preset>/ (generated
from the repo's backends/ source and contract-tested — templates/<preset>/README.md
- a shared
CONTRACT.md). Pick one as the project's backend; each connects to the dashboard frontend through the two seams above — no page, query, table, or form changes — and each is independently verified.
| Preset | Stack | Frontend wiring | Reference |
|---|---|---|---|
tanstack-drizzle-betterauth | TanStack Start server fns + Drizzle + better-auth (in-process — the default) | none — it is the scaffold | references/tanstack-drizzle-betterauth.md |
hono-drizzle-betterauth | Hono + Drizzle + better-auth (standalone TS service) | restRepository + remoteBetterAuthProvider | references/hono-drizzle-betterauth.md |
hono-prisma-betterauth | Hono + Prisma + better-auth (standalone TS service) | restRepository + remoteBetterAuthProvider (same as Drizzle) | references/hono-prisma-betterauth.md |
hono-drizzle-authjs | Hono + Drizzle + Auth.js / NextAuth v5 (standalone TS service) | restRepository + remoteAuthjsProvider + authjs client | references/hono-drizzle-authjs.md |
fastapi-sqlalchemy-jwt | FastAPI + SQLAlchemy + JWT (standalone Python service) | restRepository + externalJwtAuthProvider | references/fastapi-sqlalchemy-jwt.md |
supabase | Supabase Postgres + Auth (BaaS — SQL + config) | supabaseRepository + Supabase AuthProvider (copy-ready) | references/supabase.md |
The HTTP-API presets (the hono-* services + fastapi) all speak one shared wire
contract, so their frontend auth providers ship pre-wired and typechecked in the base
under src/lib/auth-providers/ (externalJwtAuthProvider, remoteBetterAuthProvider,
remoteAuthjsProvider) — activating is a one-line swap. Supabase needs the Supabase SDKs,
so its wiring ships as copy-ready files in templates/supabase/frontend-wiring/.
Stack matrix: framework (TanStack / Hono / FastAPI / Supabase) × ORM (Drizzle / Prisma / SQLAlchemy) × auth (better-auth / Auth.js / custom-JWT / Supabase). The presets are the idiomatic, verified combinations — not a blind cartesian product.
To stand up a preset:
- Read
references/<preset>.md(Add it / Foundation / Invariants / Verify). - Copy the template out as the service (
cp -R templates/<preset> <dest>); follow its README to run it (zero-config: SQLite + a dev secret, one install + one run command). - Wire the frontend: bind the resource's
server.tsto the preset'sRepositoryadapter (§2) and pointauthProviderat the preset'sAuthProvider(§3). - Verify end-to-end (the reference's Verify block).
The numbered sections below are the building blocks these presets compose: add a resource (§1), bind/swap a data source (§2), swap the auth preset (§3).
1. Add a CRUD resource
The CRUD table is the base archetype. products is the canonical reference;
orders is a generated example.
- Generate the vertical (table + feature + route + sidebar entry, auto-formatted):
This createsbun run create-resource <plural-name> # e.g. customerssrc/features/<name>/{schema,server,demo-data,queries,columns,config}.ts(x)and the routesrc/routes/_app/<name>.tsx, appends a Drizzle table tosrc/db/schema.ts, and inserts a sidebar item. Likeproducts, the generatedserver.tsbindsdrizzleRepositorywhenDATABASE_URLis set and falls back tomemoryRepositoryoverdemo-data.tsotherwise — so the new resource runs under zero-configbun dev(no database) before you ever migrate. - Customise the fields in
src/db/schema.ts(the appendedpgTable) and insrc/features/<name>/schema.ts(the zod input / form / list-params schemas). Keep numeric form fields non-coercing in*FormSchema(input must match the form value type); the server*InputSchemamay coerce. Filter params that can be numeric must usez.coerce.string()(the router JSON-parses search params). - Adjust
columns.tsx(cells, sortable columns),config.ts(filters, search placeholder), theserver.tsrepository config (searchColumns/sortColumns/filterColumnson the drizzle branch, and the matchingsearchFields/sortFields/filterFieldson the memory branch), and the seed rows indemo-data.ts. - Migrate (only on Postgres — zero-config dev needs no DB):
bun run db:generate && bun run db:migrate. - Verify:
bun run typecheck && bun run check && bun run test, then open/<name>(it lists thedemo-data.tsrows with no DB).
Resource invariants (must hold)
- Every server-fn handler calls
requireUser()first and validates input via.validator((data) => zodSchema.parse(data))(an arrow wrapper, not a barezodSchema.parsemethod reference). - List state lives in the URL (
validateSearch+useTableSearch); never localuseState. - Mutations show a toast and invalidate the resource's query keys; deletes go
through
useConfirm(). - The repository binding lives in
server.ts(server-only):drizzleRepositoryfrom@/infra/data/drizzle-repositorybehindhasDatabase, with amemoryRepositoryfallback for zero-config dev — never imported from a client component. - The page wraps
DataTablein a full-height flex column —<div className="flex h-full flex-col gap-6">with the header asshrink-0— so the pagination bar pins to the bottom (the shell sizes each page to the viewport). The generator emits this; keep it.
2. Bind / swap a resource's data source
Every page archetype is written against Repository<T, TInput>
(src/infra/data/repository.ts), so swapping a resource's backend touches only its
server.ts binding — queries.ts, columns.tsx, the table/detail/form, and the
route do not change.
Adapters:
- Drizzle (Postgres) —
drizzleRepository(table, config)from@/infra/data/drizzle-repository(import directly; server-only). Backsproducts/orders. - In-memory —
memoryRepository(seed, config). The zero-config default; backs every demo whenDATABASE_URLis unset. - REST —
restRepository({ baseUrl, path, map, … })from@/infra/data. Backsposts(jsonplaceholder). Defaults target json-server (_page/_limit/_sort/_order/q+x-total-count); overrideparams/totalHeaderfor other shapes. - GraphQL —
graphqlRepository({ endpoint, map, operations })from@/infra/data. Each op supplies a document + variable builder + extractor.
To back a resource with REST/GraphQL (no DB table needed):
- Define the resource's type + zod schemas.
- In
server.ts, build the repository with the right adapter and amapfrom the raw API record to your type; wrap each op in acreateServerFnhandler that callsrequireUser()(the fetch stays server-side, so API keys never reach the client):export const widgetsRepository = restRepository<Widget, WidgetInput>({ baseUrl: process.env.WIDGETS_API_URL!, path: "/widgets", map: (raw) => ({ ...raw }), }); - Map the resource's flat list params to the repository's
filters(toListParams). Numeric filter params →z.coerce.string().
Data-source invariants
- Adapters run only inside server fns. The
@/infra/databarrel is isomorphic-safe (no@/db); the Drizzle adapter is imported from its own path. - Always provide a unit test mocking the transport (fetch / db) — see
rest-repository.test.ts,graphql-repository.test.ts,drizzle-repository.test.ts.
3. Swap the whole-app backend + auth preset
Two classes of data, two different rules:
- Business data (your resources) — swap per resource via the
Repositoryadapter inserver.ts(section 2). - Platform data (users/sessions, later RBAC) — owned by the auth preset; swap
it via
AuthProvider(server) +@/lib/auth-client(browser).
Implement AuthProvider and point authProvider at it. The contract:
export interface AuthProvider {
getSession(headers: Headers): Promise<AuthSession | null>; // AuthSession = { user: { id; email; name; image? } }
handler(request: Request): Promise<Response>; // serves /api/auth/*
}
For the bundled presets these now ship as real files — activate, don't hand-write.
src/lib/auth-providers/external-jwt.ts(externalJwtAuthProvider, custom-JWT like the FastAPI preset) andsrc/lib/auth-providers/remote-better-auth.ts(remoteBetterAuthProvider, remote better-auth like the Hono preset) are pre-wired and typechecked; the Supabase provider is copy-ready intemplates/supabase/frontend-wiring/auth-provider.ts. The examples below show the shape behind those files.
Example: Supabase auth provider
// src/lib/auth-provider.ts (replace betterAuthProvider). bun add @supabase/ssr
import { createServerClient, parseCookieHeader } from "@supabase/ssr";
export const supabaseAuthProvider: AuthProvider = {
async getSession(headers) {
const cookies = parseCookieHeader(headers.get("cookie") ?? "");
const supabase = createServerClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{ cookies: { getAll: () => cookies } },
);
const { data } = await supabase.auth.getUser();
if (!data.user) return null;
return {
user: {
id: data.user.id,
email: data.user.email ?? "",
name: data.user.user_metadata?.name ?? data.user.email ?? "",
},
};
},
// Supabase auth runs client-side; no /api/auth/* routes to serve.
handler: async () => new Response("Not found", { status: 404 }),
};
export const authProvider: AuthProvider = supabaseAuthProvider;
Then reimplement @/lib/auth-client with Supabase's browser client, exporting the
same signIn / signUp / signOut / useSession surface the auth pages use.
Example: external-API (JWT) auth provider
// src/lib/auth-provider.ts (replace betterAuthProvider)
export const externalApiAuthProvider: AuthProvider = {
async getSession(headers) {
const cookie = headers.get("cookie") ?? "";
const token = /(?:^|;\s*)session=([^;]+)/.exec(cookie)?.[1];
if (!token) return null;
const res = await fetch(`${process.env.AUTH_API_URL}/me`, {
headers: { authorization: `Bearer ${token}` },
});
if (!res.ok) return null;
const u = await res.json();
return { user: { id: String(u.id), email: u.email, name: u.name } };
},
// Proxy login/logout to the upstream API (set the session cookie on success).
handler: async (request) => {
const hasBody = request.method !== "GET" && request.method !== "HEAD";
return fetch(
`${process.env.AUTH_API_URL}/auth${new URL(request.url).pathname.replace("/api/auth", "")}`,
{
method: request.method,
headers: request.headers,
// Buffer the body before forwarding. Passing the raw `request.body`
// ReadableStream to Node/undici's fetch requires `duplex: "half"` and
// still can't be retried; reading it to a string sidesteps both.
body: hasBody ? await request.text() : undefined,
},
);
},
};
export const authProvider: AuthProvider = externalApiAuthProvider;
A different SQL engine (MySQL/SQLite/Turso) needs no preset swap — Drizzle supports
them; see docs/backends.md for the files to touch.
Preset invariants
- The app reaches auth ONLY via
authProvider(server) and@/lib/auth-client(browser). Never call a specific auth SDK from a route or component. getSessionreturns the normalizedAuthSession({ user: { id, email, name } }) sorequireUser, the_appguard, and the route context are backend-neutral.- Keep the server seam server-only:
auth-provider.tsmay import DB/SDK clients, so it must only be reached fromrequire-user, the api route, and the dynamic import inauth-server— never statically from a client-reachable module.
Verify
bun run typecheck && bun run check && bun run test (and bun run build for an
auth-preset swap), then bun run dev: open /<name> and confirm
list/paginate/filter/search work; for an auth swap, an unauthenticated request to
/ redirects to /login, sign-in works, and a protected page loads its data.
What ships with it: 99 files
332.2 KB alongside SKILL.md, 58 of them executable
references/
- fastapi-sqlalchemy-jwt.md2.9 KB
- hono-drizzle-authjs.md3.2 KB
- hono-drizzle-betterauth.md2.7 KB
- hono-prisma-betterauth.md2.2 KB
- supabase.md3.4 KB
- tanstack-drizzle-betterauth.md1.7 KB
templates/
- fastapi-sqlalchemy-jwt/alembic.ini1.4 KB
- fastapi-sqlalchemy-jwt/app/auth.pyruns4.8 KB
- fastapi-sqlalchemy-jwt/app/config.pyruns3.5 KB
- fastapi-sqlalchemy-jwt/app/db.pyruns1.6 KB
- fastapi-sqlalchemy-jwt/app/__init__.pyruns0 B
- fastapi-sqlalchemy-jwt/app/main.pyruns2.0 KB
- fastapi-sqlalchemy-jwt/app/models.pyruns2.3 KB
- fastapi-sqlalchemy-jwt/app/routers/auth.pyruns2.5 KB
- fastapi-sqlalchemy-jwt/app/routers/__init__.pyruns0 B
- fastapi-sqlalchemy-jwt/app/routers/products.pyruns5.7 KB
- fastapi-sqlalchemy-jwt/app/schemas.pyruns3.0 KB
- fastapi-sqlalchemy-jwt/.env.example2.1 KB
- fastapi-sqlalchemy-jwt/migrations/env.pyruns2.7 KB
- fastapi-sqlalchemy-jwt/migrations/script.py.mako656 B
- fastapi-sqlalchemy-jwt/migrations/versions/0001_initial.pyruns3.5 KB
- fastapi-sqlalchemy-jwt/pyproject.toml993 B
- fastapi-sqlalchemy-jwt/README.md9.7 KB
- fastapi-sqlalchemy-jwt/tests/conftest.pyruns2.0 KB
- fastapi-sqlalchemy-jwt/tests/__init__.pyruns0 B
- fastapi-sqlalchemy-jwt/tests/test_auth.pyruns2.6 KB
- fastapi-sqlalchemy-jwt/tests/test_data_token.pyruns2.5 KB
- fastapi-sqlalchemy-jwt/tests/test_products.pyruns5.2 KB
- hono-drizzle-authjs/bun.lock9.2 KB
- hono-drizzle-authjs/.env.example1.4 KB
- hono-drizzle-authjs/.gitignore59 B
- hono-drizzle-authjs/package.json745 B
- hono-drizzle-authjs/README.md10.2 KB
- hono-drizzle-authjs/src/app.tsruns3.5 KB
- hono-drizzle-authjs/src/auth.tsruns3.1 KB
- hono-drizzle-authjs/src/db/index.tsruns5.1 KB
- hono-drizzle-authjs/src/db/schema.pg.tsruns1.1 KB
- hono-drizzle-authjs/src/db/schema.sqlite.tsruns1.4 KB
- hono-drizzle-authjs/src/index.tsruns661 B
- hono-drizzle-authjs/src/lib/env.tsruns2.9 KB
59 more files not listed here. See all 99 in the repository.
Gives 0 of the 12 instructions most databases sql skills give in ~3.5k tokens
Counted across 609 of the 712 authors here whose files we hold, read 2026-09-06
- Index all foreign key columnsin 26 of 609
- Use cursor pagination instead of offsetin 25 of 609, across 20 files
- Use timestamptz for timestampsin 21 of 609
- Specify columns instead of using select starin 20 of 609, across 10 files
- Use parameterized queries for all database interactionsin 20 of 609, across 19 files
- Use Enum for categorical datain 17 of 609, across 7 files
- Order by frequently filtered columnsin 17 of 609, across 7 files
- Batch data insertsin 17 of 609, across 7 files
- Use expand-contract pattern for schema changesin 17 of 609
- Use materialized views for real-time aggregationsin 16 of 609, across 6 files
- Partition tables by timein 16 of 609, across 6 files
- Use smallest appropriate data typesin 16 of 609, across 6 files
Said here and by no other author read
- use add-backend when a screen needs its own data
- use add-component when composing UI on existing data
- copy the chosen template to the destination directory
- bind resource server.ts to the repository adapter
- point authProvider at the chosen auth provider
- run create-resource to generate a new CRUD vertical
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.