9.2 — Architecture configuration
On this page
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-defaultedArchitectureConfig(DEFAULT_ARCHITECTUREis the zero value);architectureConfigSchemais 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 readctx.blueprint.project.architecture. architectureToPromptKeys/preferencesForPipelineflatten 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.
| Config | Emits | Emitter |
|---|---|---|
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_users → admins + is_admin() + admin-bypass RLS + requireAdmin; rbac → roles/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() | blog → 9200_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() | open → members 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[], notgaps(which is reserved for agent-filled codegen slots — see 9.1). Anything an emitter scaffolds but can't fully build —usage_based/marketplacepayments,full_cmsmedia, or the team emitter'srequireMembership()(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: payments9000, file-storage9100, content9200, team9300.
Adding a config-driven emitter
- Add the accessor to
lib/utils/app-architecture.ts(and the field/enum totypes/app-architecture.ts). - Add
lib/builder/templates/<domain>/*purebuild*()functions returning{ path, contents }. Escape\${…}for expressions that must appear literally in the generated app; leave${…}for build-time interpolation. - Add the emitter under
lib/builder/emitters/, gate on the accessor, return{ files: {} }when off, and register it inemitters/index.ts(beforemarketingPages). - Pick a non-colliding migration number if you emit SQL. Surface anything you can't fully build via
unsupported. - Unit-test: default blueprint →
{}; each choice → its exact file set; assert no[object Object]/undefinedleaked 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
- The pipeline + emitter model → 9.1 Architecture.
- The user-facing options → User docs: 12.2 App architecture options.