Extending the boilerplate

Three distinct jobs: adding a domain to ONE scaffolded app (the common case), adding a demo app so every future scaffold can start from it, and adding a feature to the boilerplate so every future app can opt into it.

Adding a domain to an app: clone the records pattern

Preferred: copy the records feature (schema block, backend module, frontend folder), edit the one CONFIG: EntityConfig, register one line in src/features/index.tsx — 9 views for free. See the Records page for the full recipe. When the pattern doesn't fit, build from scratch: domain tables in convex/schema.ts beside ...shellTables (index by_user, never by_id), one convex/<domain>.ts shaped like convex/items.ts (requireUserId first), a FeatureModule under src/features/<domain>/, then npx convex codegen && pnpm exec tsc --noEmit to zero.

Adding a demo app

A demo is ONE spec entry in DEMOS in tools/lib/demos.mjs — entities (table, labels, fields, role keys, optional seed rows) plus defaults (design/accent/layout). Everything else derives from that entry: the codegen (demo-codegen.mjs) generates the backend and page for both db variants, the demo appears in pnpm new --list and in the --list --json manifest's demos object, and the selftest shape matrix exercises it. No template files, no new codegen code — if the 9 field types and role keys can express the domain, the spec is the whole job.

Adding a feature to the boilerplate itself

A feature is defined in exactly two places:

  • 1. Marker blocks in the template — wrap the feature's imports, registry line, and schema entries in @feature:<id> -- start/end markers (works in //, {/* */}, <!-- -->, and -- SQL comments) inside _template/app and/or _template/supabase-app
  • 2. ONE entry in FEATURES in tools/lib/features.mjs — per-db lists of the whole files the feature owns. The presence of a convex/supabase key = pool membership for that db; an empty array means marker-blocks only.
// tools/lib/features.mjs
export const FEATURES = {
  // ...
  bookmarks: {
    convex: ["src/features/bookmarks", "convex/bookmarks.ts"],
    supabase: ["src/features/bookmarks.tsx"],
  },
};
// cross-feature dependencies:
export const FEATURE_IMPLIES = { mcp: ["api-keys"] };

Everything else derives from that single entry: the feature pool, the wizard, pnpm new --list, the JSON manifest, and file deletion when the feature is excluded. Cross-feature dependencies go in FEATURE_IMPLIES.

The template must ALWAYS compile with every feature enabled. Scaffolding only removes code — it never adds any. If your feature only works when another is off, it is designed wrong.

Adding a site type or shell change

Site-type composition uses the same mechanism with @site:<id> markers and the SITE_TYPES map in tools/lib/features.mjs. Shell changes in packages/shell benefit every app instantly — apps consume it via workspace link:, no publish step.

Before committing

pnpm test                 # the 17-shape scaffold matrix — every shape tsc-clean
pnpm smoke <slug>         # browser runtime gate on a scaffolded app

Run BOTH after any template or scaffolder change. The self-test also catches leaked tokens and markers; the smoke test catches things that compile but do not run. See ROADMAP.md for the ordered backlog.