9.1 — App Builder architecture
On this page
9.1 — App Builder architecture
🚧 Dark-launched (admin-only). The App Builder is a premium add-on gated behind the
buildAppfeature flag, which is currentlyfalseon every paid plan andtrueonly for admins. This page is for contributors working on the feature; the user-facing guide is User docs: 12 App Builder.
The App Builder compiles a VibeMap project's atomic blueprint into a complete, deployable Next.js + Supabase app. Where VibeMap's other pipelines stop at a structured spec, this one emits source code, verifies it, and ships it.
The core reframe: this is not a from-scratch "Bolt clone." VibeMap already owns the hardest thing prompt-first builders lack — a validated, normalized spec (entities, access rules, interactions, acceptance criteria). The App Builder brings the coding agent in-house and gives it a place to run.
The build pipeline
A single Inngest orchestrator runs the whole thing. The shape mirrors the existing prepare-for-development.ts pattern: an event triggers a multi-step function that writes status back as it goes.
build/app.requested ──▶ inngest/functions/build-app.ts
│
1. PREFLIGHT blueprint ready + entitlement + (at GA) OAuth connected
2. COMPILE deterministic compiler: blueprint -> file tree + GAPS (no LLM)
3. FILL one scoped agent per gap, write-clamped to its own files (LLM)
4. VERIFY install · type-check · lint · build -> structured errors
5. REPAIR red? per-file repair agents -> re-verify (≤ 3 rounds) (LLM)
6. DEPLOY push to GitHub repo (Octokit) · deploy on Vercel
7. REPORT live URL · repo · gap/fill counts · error summary -> build_runs
runBuild() in lib/builder/orchestrate.ts owns stages 2–5: it calls compileBlueprint, then fillGaps, then runVerifyRepair, and returns a BuildOutcome (ok, stage, files, gap/fill counts, ownership violations, remaining errors). It never throws on a build failure — it returns ok: false with diagnostics. Deploy (stage 6) lives in the Inngest function so the pure pipeline stays side-effect-free.
Why this is the differentiator
The blueprint is used three ways in one run:
- Input — the compiler walks it deterministically (stage 2).
- Goal — each gap's
acceptanceCriteriaIdsbecome the agent's objective (stage 3). - Test oracle — verification gates the output against the same spec (stage 4).
access_rules + op_conditions become RLS policies and route guards deterministically; acceptance_criteria become both the agent goals and the verify gate. That triangle is what a prompt-only competitor can't replicate.
The emitter model
The compiler is a pure function — compileBlueprint(blueprint) → { files, gaps, unsupported }, no I/O, no Date.now() / Math.random(). Same blueprint in, same tree out (file keys sorted, gaps sorted by id) so output is reproducible and fully unit-testable. See lib/builder/compile.ts.
It runs an ordered registry of emitters, each consuming one slice of the blueprint and contributing files (later emitters overlay earlier ones). The registry lives in lib/builder/emitters/index.ts:
| Emitter | Blueprint slice | Emits |
|---|---|---|
scaffoldEmitter | project | package.json, config, Tailwind, app/layout, globals.css (pinned versions) |
schemaEmitter | tables + relationships | per-table migration (columns, FKs, indexes), typed row interface, CRUD server actions |
accessEmitter | access rules + page access rules | RLS policies (all / own / own_via / custom) and middleware route guards |
authEmitter | which pages are auth-required | @supabase/ssr module: login/signup/callback routes, session middleware, protected-route wrapper |
paymentsEmitter | payments flag | Stripe module — subscriptions + one-off payments + webhook handler (only when payments are enabled) |
pagesEmitter | pages / sections / navigation | route files + layout shells; pure-CRUD pages fully deterministic, others emit a shell + a Gap |
The registry has since grown with config-driven emitters — fileStorageEmitter, contentEmitter, teamEmitter, and marketingPagesEmitter — and several of the emitters above now branch on the project's architecture config: authEmitter trims to the chosen sign-in method (+ MFA), paymentsEmitter to the chosen payment model, and accessEmitter to the roles model. Each gates on a config accessor and emits nothing when its slice is off. See 9.2 Architecture configuration for the full config→output mapping.
The emitter contract
An emitter implements the Emitter interface from lib/builder/types.ts:
interface Emitter {
name: string;
emit(ctx: EmitterContext): EmitterResult;
}
EmitterContext is read-only ({ blueprint, projectName }). EmitterResult carries the files it produced, plus optional gaps, unsupported, npm dependencies / devDependencies, and env vars. The compiler merges declared dependencies into the generated package.json and assembles declared env vars into a generated .env.example — so an emitter never edits those files directly; it just declares what its code needs.
Adding a new emitter or template
- New emitter → add a file under
lib/builder/emitters/, implementEmitter, and register it inemitters/index.ts(order matters — later emitters overlay earlier ones). - New template → add it under
lib/builder/templates/(organized by domain:scaffold,schema,access,auth,pages,payments). Templates are versioned, same convention aslib/prompts/, and every emitted file carries a provenance comment so repair/rebuild can reason about its origin. - Keep emitters pure. Anything non-deterministic belongs in the gap-fill or deploy stages, not the compiler.
Gaps, fill, and verify
A Gap is a slot the compiler can't fully resolve — a non-CRUD page body, an interaction handler, or an acceptance criterion with no covering file:
interface Gap {
id: string;
kind: "page_body" | "interaction_handler" | "uncovered_ac";
targetFiles: string[]; // the ownership manifest — the only files this gap may write
acceptanceCriteriaIds: string[]; // the agent's goal
context: string;
}
- Fill (
lib/builder/gaps/) dispatches one scoped agent per gap, in parallel and capped. Each agent gets the frozen skeleton as context and is write-clamped to itstargetFilesvia the ownership manifest; writes outside the manifest are reported asownershipViolations, not silently applied. - Verify + repair (
lib/builder/verify/) runs the generated tree through install / type-check / lint / build, parses the output into structured errors, and routes per-file repair agents. Repair is hard-capped at 3 rounds (maxRepairRounds), then the build is marked failed with the remaining errors.
LLM calls reuse the shared generation infrastructure (createGapLLM); CI runs through an injected VerifyDriver (createNodeDriver), so the pipeline is testable with fakes.
Deploy
Provisioning + deploy adapters live in lib/builder/deploy/ (github.ts / github-client.ts via Octokit, vercel.ts, env.ts). The orchestrator pushes the verified tree to a new private GitHub repo, then triggers a Vercel deploy linked to it; secrets are stamped into the Vercel project env and never committed (the repo ships .env.example only). A Vercel failure leaves the repo intact (deployUrl stays null) rather than losing the build.
Current state of the last mile. Deploy reads
process.env.GITHUB_PAT/VERCEL_TOKENtoday — if they're absent the build runs in build-only mode (no deploy, graceful skip). The GA plan, marked withTODO(vibemap)inbuild-app.ts, is to read each user's per-provider tokens fromapp_integrationsvia Supabase Vault and stamp the user's own Supabase keys into the deployed app. Reusing one repo across rebuilds and a ZIP-artifact producer are also still TODO. The Inngest function runs withretries: 0— by design, a user must manually retry from the UI (cost control).
Data model
| Table | Purpose |
|---|---|
app_integrations | Per-user provider connection state (provider, connected) for Supabase / GitHub / Vercel / Stripe. At GA this also custodies encrypted OAuth tokens. |
build_runs | One row per build: project_id, status, stage, gap_count, filled_count, cost_cents, repo_url, deploy_url, zip_url, error_summary, timings. |
build_runs.status is constrained to pending → compiling → filling → verifying → deploying → succeeded / failed (migration supabase/migrations/20260605120001_build_runs.sql). The generated file tree itself is stored in a Supabase Storage bucket keyed by build run, not in Postgres. Per-gap work units reuse acceptance_criteria + the kanban lifecycle — no new table. Each rebuild ties to a new project_changeset.
build_runsandapp_integrationsare not yet in the generateddatabase_types.ts, so the route and orchestrator usesupabase as anycasts withbiome-ignorecomments. Regenerate types once the schema settles to remove them.
Surfaces
| Surface | Path | Responsibility |
|---|---|---|
| Build tab (server) | app/project/[projectId]/build/page.tsx | Auth + entitlement gate, loads integration status + latest run, renders the client. |
| Build tab (client) | app/project/[projectId]/build/_components/build-client.tsx | Connect checklist, output-target chooser, Build button, progress stepper (polls every 2.5 s), results panel. |
| Stage helpers | app/project/[projectId]/build/_lib/stages.ts | Pure stage/label/order helpers mirroring the build_runs.status CHECK constraint. |
| Trigger route | app/api/projects/[projectId]/build/route.ts | POST authenticates, entitlement-checks, inserts a build_runs row, fires build/app.requested; GET returns runs for polling. |
| Orchestrator | inngest/functions/build-app.ts | Fetches the blueprint, runs the pipeline, deploys, writes status back to build_runs. |
The gating flag
Access is controlled by the buildApp feature flag in lib/billing/plans-config.ts, checked with canAccessFeature(planName, "buildApp") on both the API route and the Build page:
- Starter / Pro —
false(currently a Pro+ feature, but dark-launched). - Team / Enterprise —
false, commented🚧 Dark-launched — admin-only until deploy last-mile ships (then flip to true). - Admin —
true(admins, resolved via theis_billing_exemptRPC, get all features).
So today the only accounts that can reach the Build tab are admins. When the connect-and-deploy last mile lands, the plan is to flip buildApp to true for Pro and above. Until then, treat the feature as internal preview — don't reference it as available in user-facing copy.
Where to go next
- The data model the compiler reads → 2.2 The data model in plain English.
- How the rest of the pipeline works → 2.1 How VibeMap works.
- How architecture choices drive the build → 9.2 Architecture configuration.
- The user-facing guide → User docs: 12 App Builder.