9.2 — Architecture configuration

9.2 — Architecture configuration

🚧 Dark-launched (admin-only). Contributor reference for how a project's architecture choices drive the deterministic compiler. The pipeline overview is 9.1 Architecture; the user-facing guide is User docs: 12.2 App architecture options.

9.1 describes the build pipeline and the emitter model. This page covers the input that steers it: the architecture config on a project, and how each emitter branches on it. The headline property is determinism — the config is structured data, not prompt text, so compileBlueprint turns the same config into the same file tree every time.

The config

Architecture choices live on projects.app_architecture (JSONB) and resolve to ArchitectureConfig in types/app-architecture.ts:

interface ArchitectureConfig {
  authentication: AuthChoice;     // "none" | "email_password" | "social" | "magic_link"
  mfa: boolean;
  roles: RolesChoice;             // "single_user" | "admin_users" | "rbac"
  payments: PaymentsChoice;       // "none" | "subscription" | "one_time" | "usage_based" | "marketplace"
  modules: {
    adminDashboard; userDashboard; search; comments; notifications;
    content: ContentChoice;       // "none" | "static" | "blog" | "full_cms"
    teamWorkspaces: TeamChoice;   // "none" | "open" | "invite_only" | "approval"
    fileStorage: boolean;
  };
  pages: MarketingPage[];         // ("landing" | "pricing" | "about" | "contact" | "help")[]
}

History. This replaced a projects.app_architecture_prefs TEXT[] column that was flattened into LLM prompt hints. The old column + GIN index were dropped in the storage migration (the JSONB is backfilled from the array first), so that migration and its consuming code must ship in the same deploy.

Resolver + accessors

lib/utils/app-architecture.ts owns resolution and is the only thing emitters should read the config through:

  • parseArchitectureConfig(raw) → a fully-defaulted ArchitectureConfig (DEFAULT_ARCHITECTURE is the zero value); architectureConfigSchema is the zod validator.
  • Accessors the emitters branch on: authType, hasMfa, rolesModel, paymentsModel, contentMode, teamInvitation, hasModule(name), marketingPages.
  • The blueprint carries it as ProjectBlueprint.architecture (lib/atomic-blueprint/projection.ts); emitters read ctx.blueprint.project.architecture.
  • architectureToPromptKeys / preferencesForPipeline flatten the config back to the legacy prompt-key list so the features/pages/schema LLM pipelines still get their guidance — the deterministic builder and the generation pipelines share one source of truth.

How emitters branch on it

Each emitter reads one accessor and emits nothing when its slice is off — so a DEFAULT_ARCHITECTURE blueprint produces no extra files and the golden snapshots stay byte-stable. This is the rule that keeps the registry composable.

ConfigEmitsEmitter
authType() (+ hasMfa())Supabase auth flow trimmed to the chosen method; TOTP enrol/verify when MFA is on. Clients + guards are emitted even for none.authEmitter
rolesModel()single_user → owner-only RLS; admin_usersadmins + is_admin() + admin-bypass RLS + requireAdmin; rbacroles/user_roles + has_role()/is_admin() + requireRole/requireAdmin. Role infra is prepended inside the RLS migration so create-order is correct.accessEmitter
paymentsModel()subscription → sub + portal + plan-gate + subscriptions; one_time → checkout + orders. usage_based/marketplace deferred (scaffold + unsupported). Webhook raw-body-verify invariants preserved.paymentsEmitter
contentMode()blog9200_content.sql (categories+posts, public-read RLS, author-or-admin write) + public list/detail + editor + actions; full_cms → same + unsupported (media/rich-text deferred).contentEmitter
teamInvitation()openmembers registry + auto-activate trigger + roster; invite_only → + requireMembership() guard + accept/gate pages + send/revoke + accept_invite RPC; approval → + pending page + approve/decline + pending-on-signup trigger.teamEmitter
hasModule("fileStorage")private Storage bucket + files table (owner-only RLS) + upload component + files page.fileStorageEmitter
marketingPages()static pages for the selected set; pricing wired to the payments model.marketingPagesEmitter

The registry in lib/builder/emitters/index.ts is scaffold, schema, access, auth, payments, pages, fileStorage, content, team, marketingPages. marketingPages stays last — its landing overrides the dashboard app/page.tsx. Later emitters overlay earlier ones on path collision; the config-driven emitters mostly emit disjoint paths, so order otherwise doesn't matter.

Determinism + deferred support

  • Default-off gating (above) is what makes the compiler reproducible and unit-testable: tests/unit/builder/** asserts each emitter returns { files: {} } for a default blueprint, plus the exact file set per choice.
  • Deferred / partial support is surfaced through EmitterResult.unsupported: string[], not gaps (which is reserved for agent-filled codegen slots — see 9.1). Anything an emitter scaffolds but can't fully build — usage_based/marketplace payments, full_cms media, or the team emitter's requireMembership() (emitted but not auto-wired into middleware, to avoid clobbering the auth emitter) — names itself here so the build report can show it.
  • Migration sequence numbers for the optional emitters are fixed and high, so they never collide with the schema emitter's 0000_init / 0001..N: payments 9000, file-storage 9100, content 9200, team 9300.

Adding a config-driven emitter

  1. Add the accessor to lib/utils/app-architecture.ts (and the field/enum to types/app-architecture.ts).
  2. Add lib/builder/templates/<domain>/* pure build*() functions returning { path, contents }. Escape \${…} for expressions that must appear literally in the generated app; leave ${…} for build-time interpolation.
  3. Add the emitter under lib/builder/emitters/, gate on the accessor, return { files: {} } when off, and register it in emitters/index.ts (before marketingPages).
  4. Pick a non-colliding migration number if you emit SQL. Surface anything you can't fully build via unsupported.
  5. Unit-test: default blueprint → {}; each choice → its exact file set; assert no [object Object]/undefined leaked through the templates.

The source of truth for the config→output mapping is lib/utils/app-architecture.ts (accessors) and the emitters in lib/builder/emitters/.

Where to go next