Docs

Enterprise / ISV SPA-delivery surface — artifacts, health, handoff zip, and the Studio wizard / LLM-authored extension generators.

Advanced App Builder

Enterprise / ISV tier tenants see an Advanced tab on every app at /admin/apps/:id/advanced. It's a thin shell around the app's advanced_config_json envelope and a handful of delivery actions.

This page walks through the surface. For the raw spec see Advanced-App-Builder-Enterprise-Spec.md at the repo root; this doc is the admin-facing rewrite.

What lives here

SectionPurpose
Experience strategyPick workspace pages (Path 1) vs extension SPA (Path 2). Drives AI Designer proposals.
ArtifactsTwo markdown documents — engineering_spec_md and feature_phases_md — that describe the app's SPA shape. Authored by the admin (or the AI Designer).
Nav + page healthEvery nav entry classified as ok / missing_page / orphaned_extension, with per-row deep links.
Handoff zipOne-click download with both markdowns + a manifest stub + a README for an external SDK developer.
Generate extensionProduce a working extension bundle — via the Studio wizard or LLM-authored.

Authoring artifacts with AI

The Advanced page's two markdown editors (engineering_spec_md

  • feature_phases_md) feed the bundle generator. Drafting them from a blank page is tedious, so an Author artifacts with AI button above the editors calls a frontier Claude model with a pinned spec-writing prompt and populates both fields for you to review and edit before Save.
  • Requires the same Anthropic key as the bundle generator (llm.anthropic_api_key). The button is disabled with a tooltip when no key is stored — wire one up at /admin/settings/llm.
  • Each call ≈ $0.05. Metered as a distinct resource for spec authoring (separate from bundle generation) so spend reports can distinguish spec authoring from bundle generation.
  • Overwrite protection: if either editor already has content, clicking the button toggles an inline confirm strip. A second click (or Cancel) proceeds / aborts. No blocking modal — the text just warns.
  • Nothing persists on the LLM call itself. The response goes into the textareas as draft; the admin clicks Save to write them to apps.advanced_config_json.
  • If you have a half-authored draft, the button passes your existing content to the prompt as an <existing_*> XML block — the model refines rather than blank-slates.

Failure modes mirror the bundle generator: 422 for missing key or invalid LLM JSON, 502 for upstream transport errors, every call is metered regardless.

Generate extension

The generator turns your annotated entities + chosen layout + brand into a scan-clean ES-module extension bundle the host knows how to load. Two synthesis paths:

Synth paths

PathOutputWhen to use
Studio wizardDeterministic — same picks → identical bytes. A 4-step wizard (Layout → Branding → Entities → Review) emits a themed, multi-page React/Tailwind app: per-entity list / detail / new / edit pages, plus a textual workflow viewer + advance-state buttons on detail pages of FSM-bearing entities.The standard path. Free, no LLM, predictable.
LLM-authoredA frontier Claude model authors a bespoke React functional component from your engineering_spec + feature_phases markdown. Every call is non-deterministic.Bespoke UI shaped by prose — when "list / detail / form" doesn't fit the requirement. ≈ $0.05 per call.

Pick the path with the segmented control above the action buttons. LLM mode is disabled until an Anthropic key is stored — see Setup below.

For the full Studio walkthrough — what each step does, how the preset palettes work, what the generated bundle contains — see the Studio wizard page.

Studio install flow

Clicking Open Studio… launches the wizard modal. The wizard collects your selections without hitting the server. Only on Install at the Review step does the client:

  1. POST /api/v1/tenant/themes to materialize the chosen palette as a tenant theme (named <App> theme by default — editable in the Review step).
  2. POST .../generate-extension with mode: "install", synth: "studio", the new theme_id, the chosen layout, and the ticked entity_names.

Both calls are scoped to the authenticated tenant. If theme creation fails, the wizard surfaces the error inline and never calls generate. If generate fails after theme creation, the wizard names the orphaned theme so you can delete it from /admin/themes (or retry the install — same theme, no orphan).

LLM install flow (preview-then-confirm)

Clicking Install while synth = llm runs a two-step flow:

  1. POST .../generate-extension with mode: "preview", synth: "llm".
  2. Modal opens with the authored source in a scroll-locked <pre>.
  3. Cancel discards and closes the modal. Confirm install fires a second LLM call with mode: "install", synth: "llm" that actually writes the tenant_extensions row.

The double-LLM-call (≈ $0.10 per install flow) is deliberate — each confirm re-rolls the bundle, so admins who don't like the first preview can Cancel → Install → preview again for a fresh attempt. If you want the same source twice, use Download after Preview-then-Confirm and re-install locally via the Extensions page.

Delivery modes

ModeBehavior
Open Studio… (Studio only)Opens the wizard modal. Install + Download buttons live inside the wizard's Review step.
Download (LLM only)Streams a three-file zip: manifest.json, index.mjs, README.md. Nothing persists server-side beyond a delivery.last_generated_at timestamp.
Install (LLM only)Runs the preview-then-confirm flow above.
Preview (LLM only)Returns the authored source as JSON and opens a modal. No persistence. Used internally by the Install button — you don't call it directly from the UI.

What you get out

Regardless of path, the manifest advertises exactly one page at /ext/<id>/<app-slug>. Required scopes are pinned per path: Studio bundles ship ["data:read", "data:write"] (CRUD always writes); LLM bundles get scopes inferred from the authored source (read-only by default; widened to data:write if the bundle calls useCreateEntity, etc. — see Real-data extensions below).

Installed rows are always is_active = 0. The admin must explicitly activate from /admin/extensions. Deliberate for LLM output (untrusted input — the scanner + heuristic validator are defenses-in-depth). Same gate applies to Studio bundles even though they're deterministic — no exceptions.

Setup for LLM mode

LLM-authored generation needs a dedicated Anthropic API key, stored separately from the AI Designer's primary LLM config so OpenAI / Gemini tenants can opt in without reconfiguring their chat.

Configure at /admin/settings/llm:

  1. Scroll to Anthropic API Key — code generator.
  2. Paste an sk-ant-… key.
  3. Save Settings. The LLM-authored toggle on the Advanced App Builder becomes enabled immediately.

The primary API Key field above is independent. You can run OpenAI / Gemini / DeepSeek / custom for AI Designer chat while still having an Anthropic key on file for the code generator.

What's pinned

The LLM synthesis path is not tenant-configurable beyond the api_key. Pinned to:

  • Provider: anthropic
  • Base URL: https://api.anthropic.com
  • Model: a pinned frontier Claude model
  • Required scopes on the emitted manifest: ["data:read"]
  • Bundle size cap: 128 KiB

Code-generation quality is load-bearing, and cheap models produce unparseable JS. Changing any of these is a source-level edit.

Failure modes

The generator surfaces four distinct HTTP responses — all admin-readable in the red error banner below the buttons:

StatusCauseFix
400 Bad RequestLLM mode: blank artifacts. Studio mode: missing layout, missing theme_id, theme not owned by tenant, or empty entity_names. Either mode: invalid synth or mode value.Fill in the markdown editors (LLM); finish the wizard's required fields (Studio).
422 UnprocessableLLM mode: missing Anthropic key → error points at /admin/settings/llm. Or: validator rejected the authored bundle (forbidden token, missing import, size cap).Add the key, or re-roll via Install (cheaper to retry than to debug).
502 Bad GatewayAnthropic API returned an error or timed out.Transient — try again in a minute. Check your Anthropic account is in good standing.
500 InternalOur bug.File an issue with the request timestamp + tenant id.

Every LLM-mode call is metered against the LLM-generation meter in your usage ledger regardless of outcome — a 422 validator reject or 502 transport failure still costs. Log-aggregation carries the full detail (actor, model, bundle bytes, violation count, outcome) via tracing::info! structured fields.

Cost expectations

Rough estimate at ship: $0.05 per LLM call, scaled by your organization's wholesale_discount_multiplier. The Install flow uses two calls (preview + confirm), so ≈ $0.10 per installed extension. Studio mode is free (no upstream).

Tunable — the LLM-generation meter's base cost lives in the platform's metering configuration; adjust once you have real spend data.

Real-data extensions (SDK 0.2.0+)

Generated bundles read and write live tenant data. The Studio wizard and the LLM-authored path both have access to the full SDK hook surface, so an extension can list entity records, render FSM workflow state, and mutate tenant data through the exact same backend API the admin shell uses.

What this means for generated bundles

  • Studio wizard — list, detail, new, and edit pages compose <SmartField> from each entity's annotations, so labels, required flags, enum options, and field types all flow from /admin/entities/:name/annotations automatically. FSM detail pages additionally mount <WorkflowSection> with the current state badge, advance buttons (one per legal outgoing transition), and the merged event-log history.
  • LLM-authored — the system prompt enumerates every shipped hook with signatures + three worked examples (paginated useEntities, useJob + <WorkflowHistory>, transition button). The model is forbidden from emitting const SEED_* / const FAKE_* stubs — prompt-level enforcement; slips get caught in human review at the Preview modal.
  • Scope inference — Studio bundles always ship ["data:read", "data:write"] (CRUD always writes). LLM bundles go through a pre-install analyzer that greps the generated source for hook names and widens the manifest's required_scopes. A bundle that calls useCreateEntity lands with ["data:read", "data:write"]; useTransitionJob adds "workflow:execute"; useCancelJob adds "workflow:admin". The admin sees the inferred set in the amber scope-disclosure strip on the preview modal before confirming.

Realtime auto-refresh

Every read hook subscribes to the provider's shared WebSocket by default. useEntities('widget') refreshes whenever another session creates, updates, or deletes a widget; useJob(id) + <WorkflowHistory jobId={id} /> redraw on every transition — no polling, no backend changes on your end.

Opt out per hook when a view is deliberately snapshot-style:

useEntities('widget', undefined, { realtime: false });

See the CRUD UI recipe for a full list + create + update + delete page using these hooks.