An AI-native, skill-driven back-office substrate (a "中台" template). The repo
is not a product. It is the source of truth for the skill catalogue
(.claude/skills/): one add-component skill — a catalogue + retriever over
35+ copy-ready admin UI shapes (CRUD, detail, master-detail, kanban, calendar,
wizard, billing, RBAC, i18n, …), each a reference doc + a generated template — plus
a handful of operation skills (scaffold-dashboard, add-backend,
rebrand). A Skills
Gallery renders every shape's own demo. The demos, the
resources, and the two business cases all exist for one reason: to back a shape
and be the live proof it produces working UI. A shape's distributed templates/
are generated from this repo's working source and kept byte-for-byte in sync, so
the catalogue never ships code the repo hasn't typechecked, built, and tested.
The stance is frontend-opinionated, backend-agnostic — preserve it. The frontend
is one fixed, curated stack you compose from (TanStack Start + the shape catalogue +
the UI / form / table / chart system on @base-ui) — be opinionated here. The backend
(business data and auth) is swappable behind two seams — Repository and
AuthProvider — never the frontend's concern: pick one of the add-backend presets
(Postgres + Drizzle + better-auth by default; or Hono / FastAPI / Supabase, with
Prisma / Auth.js / custom-JWT). The enforcing rule is mechanical: backend specifics
(@/db, @/lib/auth, drizzle-orm, pg, an SDK client) live only in server-only
modules (a resource's server.ts, src/db/*, src/lib/auth*.ts, the infra/data
adapters) and reach the client solely through createServerFn + the two seams. Don't
let a backend leak upward, and don't fork the frontend stack per project.
Two modes of work — know which you're in:
- Authoring the substrate — the default here. You're adding or fixing a
skill, the platform layer, a gallery demo, or a business case inside this
repo. Whatever you produce must (a) be real, working repo code — so it's
verified and visible in the gallery — and (b) stay in lockstep with its
distributed skill via
sync-skills. If you are editing files in this repo, you are almost certainly in this mode. → see Authoring a skill below. - Porting out. A different agent stands up a real product by scaffolding the
clean base into a new project and composing it (scaffold → rebrand → pick a
backend → add resources → add shapes). The base is demo-free + gallery-free, so
there's nothing to strip. That workflow lives in
PORTING.mdand thescaffold-dashboard/rebrand/add-backendskills — not here. Don't confuse "build a product" (port-out) with "maintain the substrate" (the work in this repo).
- The repo source is the single source of truth. A skill's bundled
templates/*is a generated copy of a repo file, produced byscripts/sync-skills.ts. The UI shapes all live in one skill —add-component— whose templates are the flattened union ofCOMPONENT_SOURCES(aRecord<componentName, repoSourcePath[]>insync-skills.ts); each shape also has a hand-authoredadd-component/references/<name>.md(the "Add it / Foundation / Invariants / Verify" prose — not generated). Templates are a flat folder, basename only (a basename-collision guard fails the sync if two sources clash). Whole-project templates (the standalone backend presets) come from a second map insync-skills.ts,TREE_MANIFEST, which copies a source directory tree (backends/<preset>/) recursively intotemplates/<preset>/— same drift guard, structure preserved, install/build junk skipped. - NEVER hand-edit anything under
templates/. Edit the repo source, then runbun run sync-skillsto regenerate.bun run sync-skills --checkis the drift guard — byte-for-byte compare, exits non-zero on any drift or missing source. Run it before finishing. - The gallery demo is the skill's test. A skill's source is a real, self-contained, zero-config route/component the repo typechecks / builds / tests / renders in the Skills Gallery — so "does this skill produce working UI" is continuously proven. A skill whose demo doesn't render is not done.
scripts/build-base.tsassembles the clean base bundle thescaffold-dashboardskill ships (the platform shell, with demo / scenario / gallery code stripped and theclean/overrides applied). Re-runbun run build-baseafter changing the platform layer or theclean/files.
Adding a new UI shape means adding a component to the add-component
catalogue — not a new top-level skill. To add one:
- Build the demo in the repo, self-contained and zero-config (local/static
data, no Drizzle table): a route under
src/routes/_app/gallery/<demo>.tsx, plus any small component undersrc/components/…,src/infra/…, orsrc/lib/…. Make it real, working code — that is both the proof and the test. - Surface it in the Skills Gallery: add a
SHAPESentry insrc/routes/_app/gallery/index.tsx(title / route / category / icon) and a sidebar item in the matchingSkills · …group insrc/lib/sidebar-items.ts. - Register it for distribution: add a
COMPONENT_SOURCESentry inscripts/sync-skills.ts(in the matching category group) mapping the component name → its repo source path(s). - Write
.claude/skills/add-component/references/<name>.md— the house format: Add it (cp .claude/skills/add-component/templates/<file>into a route/component, then rewire) · Foundation it assumes · Invariants · Verify. Then add a one-line catalogue entry under the right category inadd-component/SKILL.md. Keep it terse — the template carries the code. - Generate + verify:
bun run sync-skills, thenbun run typecheck && bun run check && bun run test && bun run build && bun run sync-skills --check.
Operation skills (scaffold-dashboard, rebrand) ship no templates/: they
point at a canonical in-repo example (e.g. features/products,
src/lib/auth-provider.ts) and/or a command (bun run create-resource), and stay
slim SKILL.md skills (no COMPONENT_SOURCES entry). add-backend is the
exception: it ships directory-tree templates — six runnable backend presets generated from
backends/<preset>/ via TREE_MANIFEST, each with a hand-authored
references/<preset>.md — plus its in-repo pointers for the resource / adapter /
auth-swap operations and the dormant frontend wiring in src/lib/auth-providers/.
Platform changes (UI primitives, form system, charts, the Repository /
AuthProvider seams, the shell): edit the repo source, run the full suite, then
bun run build-base (and bun run sync-skills if a COMPONENT_SOURCES source changed).
ALWAYS
- Treat the repo source as truth. After changing any file a component's
COMPONENT_SOURCESmaps, runbun run sync-skills, and finish withbun run sync-skills --checkgreen. - Keep every gallery demo self-contained + zero-config (local/static data, no Drizzle table) so it backs its shape and renders standalone.
- Add/remove a shape as a unit: the gallery route + its
SHAPESentry + itsSkills · …sidebar item + itsCOMPONENT_SOURCESentry + itsadd-component/references/<name>.mdmove together. - Re-run
bun run build-baseafter a platform-layer orclean/change so thescaffold-dashboardbundle stays current.
NEVER
- Hand-edit
.claude/skills/*/templates/*— edit the repo source and re-sync. - Ship a shape whose demo doesn't render/verify, or whose
COMPONENT_SOURCESsource is missing (--checkwill fail the build).
ALWAYS
- Call
requireUser()first in every protected server-fn handler, and validate input with Zod via.validator(...). Data crosses the client↔server boundary only throughcreateServerFn— there is no manual fetch/REST layer. - Keep list/sort/filter/page state in the URL (
validateSearch+useTableSearch/useResourceList), not localuseState. (Multi-row selection and dialog open-state are the exceptions — transient local state.) - Wrap a
DataTable/CardListpage in a full-height flex column (<div className="flex h-full flex-col gap-6">, header asshrink-0) so the pagination bar pins to the page bottom (the shell sizes each page to the viewport). The generator emits this. - Report mutations with a toast and route destructive actions through
useConfirm(). Invalidate the resource's query keys on success. - Use a
Repositoryadapter inserver.ts(never inline Drizzle/fetchin a resource). ImportdrizzleRepositoryfrom@/infra/data/drizzle-repository. - Compose from the platform layers (form system, charts,
DataTable/CardList, archetypes). Find the closest pattern inPATTERNS.mdand copy it. - Use the
@/*alias. Before finishing, runbun run typecheck && bun run check && bun run test(andbun run buildfor infra changes).
NEVER
- Import
@/db(or the Drizzle adapter) from a client-reachable module — it leakspginto the browser. Adapters/secrets stay in server fns only. - Hand-edit
src/routeTree.gen.ts(it's generated). - Hardcode the brand — change
src/config/app.ts. - Reintroduce Next.js, Hero UI, TypeORM, or Refine. shadcn/ui here is built on
@base-ui/react, not Radix. - Sort by raw user input — use the adapter's
sortColumnswhitelist.
The rest of this file documents the substrate the skills compose. It is accurate reference, but secondary to the skill model above.
- Framework: TanStack Start — full-stack React on Vite + Nitro. Server logic runs in server functions created with
createServerFnfrom@tanstack/react-start. - Routing: TanStack Router — file-based, type-safe routes under
src/routes/. Route tree is generated intosrc/routeTree.gen.ts(do not edit by hand;typecheckrunstsr generatefirst). - Server state: TanStack Query — caching + mutations, SSR-integrated via
@tanstack/react-router-ssr-query. - Tables: TanStack Table — headless, wrapped by the generic
DataTableinsrc/infra/table. - Database: PostgreSQL via Drizzle ORM (
drizzle-orm/node-postgres). Schema insrc/db/schema.ts; client insrc/db/index.ts. Migrations in./drizzleviadrizzle-kit. The database is the default backend, not a hard requirement: with noDATABASE_URLthe app boots on in-memory adapters (bun dev, no Docker). Seedocs/backends.md. - Auth: better-auth — email + password, real hashed passwords; sessions in Postgres via the Drizzle adapter when
DATABASE_URLis set, else better-auth's in-memory adapter. Reached through theAuthProviderseam (src/lib/auth-provider.ts) + the browser client (src/lib/auth-client.ts), so the auth backend is a swappable preset; better-auth config (cookies via thetanstackStartCookiesplugin — must be the LAST plugin) is insrc/lib/auth.ts. Seedocs/backends.md. - UI: shadcn/ui on
@base-ui/react(NOT Radix), insrc/components/ui. Tailwind CSS v4, Phosphor icons (@phosphor-icons/react), light/dark vianext-themes. - Charts: Recharts. Client state: Zustand. Validation: Zod (v4).
- Tooling: Bun (package manager + script runtime), Biome (lint + format), Vitest. TypeScript strict, path alias
@/*→./src/*. Dev server on port 3000.
__root.tsx— HTML shell, head, providers (next-themes)._app.tsx— auth-guarded layout (theDashboardShell). ItsbeforeLoadcallsgetSession()andthrow redirect({ to: "/login" })if there's no session; otherwise returns{ user }into the route context. All protected pages live undersrc/routes/_app/._auth.tsx— public auth layout; redirects already-authenticated users to/. Children:login,register.api/auth/$.ts— mounts the auth provider's HTTP handler forGET/POST.
Auth is reached through a seam so the backend is swappable (docs/backends.md):
src/lib/auth-provider.ts— theAuthProviderinterface (getSession(headers)/handler(request)) + the activeauthProvider(better-auth by default). Server-only.getSession()(src/lib/auth-server.ts) — acreateServerFnwrappingauthProvider.getSession; use in routebeforeLoad/loaders.requireUser()(src/lib/require-user.ts) — asserts an authenticated user; call at the top of every mutating/protected server-fn handler. Throws"UNAUTHORIZED".- The browser auth client (
signIn,signUp,signOut,useSession) is insrc/lib/auth-client.ts.
Every data resource is a self-contained folder under src/features/<name>/, paired
with a route under src/routes/_app/<name>.tsx that renders the generic DataTable.
products is the canonical example — copy it. A resource folder contains:
| File | Responsibility |
|---|---|
schema.ts |
Zod schemas + inferred types: input, update, and list-params (page/pageSize/search/sort/filter). |
server.ts |
createServerFn handlers (list*, get*, create*, update*, delete*). Each calls requireUser(), validates via .validator(...), and delegates to a Repository adapter — drizzleRepository(table, { searchColumns, sortColumns, filterColumns, defaultSort, updatedAtKey }) for Postgres, or restRepository/graphqlRepository/memoryRepository. Returns { rows, total }. |
queries.ts |
TanStack Query glue: a *Keys factory, a *ListQuery(params) returning queryOptions, and useCreate*/useUpdate*/useDelete* hooks that invalidate the resource's keys on success. |
columns.tsx |
ColumnDef[] factory taking a context (onEdit/onDelete). Uses shared cells from @/infra/ui (StatusChip, ActionMenu). |
config.ts |
Filter definitions (FilterConfig[]) + table config (search placeholder, page-size options, empty message). |
A Drizzle-backed resource adds its table to src/db/schema.ts. A resource can also
be memory-backed (memoryRepository + a demo-data.ts, no table) — that is how
the business-case scenarios run zero-config.
src/infra/table/DataTable.tsx is a fully-controlled, server-driven table. The
page owns list state — page/pageSize/search/filter/sort synced to the URL via
useTableSearch (only multi-row selection + dialog state are local useState) — and
passes it down. The table uses manualPagination/manualSorting/manualFiltering,
and composes TableToolbar + TablePaginationControls. The body flexes to fill and
scrolls internally, so the pagination bar pins to the bottom when the page wraps it
in a full-height flex column (see the App-code rules). Reference wiring (incl. the
create/edit dialog): src/routes/_app/products.tsx. CardList (src/infra/list) is
the card-grid counterpart with the same plumbing (useResourceList).
- Atoms (
src/components,src/config): the form system (@/components/form— TanStack Form + zod;TextField/NumberField/SelectField/TextareaField/SubmitButton/FormError), toast (@/lib/toast→ sonner),useConfirm()(@/components/ui/confirm-dialog), chart components (@/components/charts—StatCard/ChartCard/AreaChart/BarChart/PieChart, CSS-var themed), andappConfig(src/config/app.ts— the single rebrand surface: name/logo/nav/theme). - Data access (
src/infra/data): theRepository<T, TInput>interface +drizzleRepository/restRepository/graphqlRepository/memoryRepository(zero-config default) overListParams/ListResult. A resource binds an adapter inserver.ts, typically viahasDatabase(@/lib/backend). Seedocs/data-adapters.md. - List views (
src/infra/table,src/infra/list):DataTable(server-driven, URL-synced, debounced search, opt-in bulk select) andCardList+useResourceList. - Page archetypes: CRUD table (
products), Detail/Show (products_.$id.tsx+DescriptionList), Master-detail split (orders.tsx+orders.$id.tsx), Card/grid list (posts). Each is a component inadd-component(add-detail-page,add-master-detail,add-card-list, …); CRUD-resource scaffolding lives in theadd-backendoperation skill. Catalogue:PATTERNS.md.
Navigation is src/lib/sidebar-items.ts (mainMenuItems, bottomMenuItems), surfaced
via appConfig.nav. Two halves:
- Business cases — two complete back-offices that compose the shapes into real
verticals: E-commerce (
/, products / orders / customers / refunds + store dashboard + blog) and Sales (CRM) (/crm/*: forecast / pipeline kanban / contacts / companies).productsandordersare real Drizzle resources (with an in-memory fallback); the rest are memory-backed. These double as the live demos for the foundational archetypes — theadd-backendoperation skill plus theadd-detail-page/add-master-detail/add-card-list/add-chart-pagecomponents ofadd-component. They live only in this repo as proof — thescaffold-dashboardbase ships without them (build-basestrips them). - Skills Gallery — one entry per shape, grouped (
Skills Gallery · Overview, thenSkills · Forms/Lists & tables/Rich views/Detail & pages/Display & feedback), each linking to that shape's demo under/gallery/*(every shape is a component inadd-component). The Overview (gallery/index.tsx) is a tabbed catalogue of every shape (incl. variants not pinned to the sidebar). Repo-only too — stripped from the scaffold base. Full menu:docs/gallery-catalogue.md.
Anchors: generated CRUD resources insert at // create-resource:anchor (in the first
business group); new business-case groups go above // gallery:anchor; the Skills · …
groups stay last.
- Add a Drizzle table to
src/db/schema.ts; runbun run db:generatethenbun run db:migrate. (Or skip the table and bindmemoryRepositoryfor a zero-config, in-memory resource.) - Create
src/features/<name>/(schema,server,queries,columns,config) — copy fromproducts. - Add a route
src/routes/_app/<name>.tsxwiringDataTable— copy fromproducts.tsx. - Add a sidebar entry in
src/lib/sidebar-items.ts.
Or run bun run create-resource <name> to scaffold all of the above (it also appends
the Drizzle table); then customise the fields and migrate. Walkthrough: docs/resources.md.
bun run dev— dev server (port 3000; zero-config if noDATABASE_URL).bun run build/start— production build / run Nitro server.bun run check/lint/format— Biome.bun run typecheck—tsr generate+tsc --noEmit.bun run test— Vitest.bun run sync-skills/sync-skills --check— regenerate skilltemplates/from repo source / verify in sync (the drift guard).bun run build-base— reassemble thescaffold-dashboardbase bundle.bun run create-resource <name>— scaffold a CRUD resource.bun run db:up/db:down/db:generate/db:migrate/db:push/db:studio/db:seed— Postgres (Docker) + Drizzle.
See README.md for setup, PATTERNS.md for the shape catalogue, PORTING.md to start
a real product, and docs/{resources,data-adapters,backends,gallery-catalogue}.md.