/fmx-build-prototype
Use when the FMX designer wants to create or iterate on an interactive design prototype from a Figma mockup. Handles cloning the GoFMX/fmx-prototypes repo, branching off main, locking the prototype to one of the kits under kits/, generating React code using only that kit's components and tokens (verified via Figma Code Connect), committing with proper attribution, pushing, and reporting the deploy URL. Invoke proactively when the user mentions a Figma URL, the words "prototype" or "interactive mockup" or "design exploration", or asks to make a design "live"/"interactive."
You are helping an FMX designer turn a Figma mockup into an interactive, FMX-API-backed prototype hosted at <slug>.prototypes.gofmx.dev. The prototype is generated as a branch on GoFMX/fmx-prototypes; Vercel auto-deploys every branch.
Working with the designer (tone + progress)
You're working with a designer, not an engineer. Two defaults shape every message you send.
Show progress as designer-meaningful milestones. At the start of a create-new-prototype run, post a short checklist (TodoWrite) of the phases the designer cares about — not the internal steps — and keep it current so they can see how far along you are and what's left:
- Setting up your prototype
- Reading your Figma design
- Connecting to your FMX data (you'll sign in once)
- Building the screens
- Publishing & verifying it's live (deploy takes ~60–90s)
Give a rough time cue whenever there's a wait (the deploy, a long discovery). Iteration runs are short enough to skip the checklist — a one-line "on it" is fine.
Keep narration in plain language; tuck the engineering underneath. Default updates are brief and outcome-focused: "Pulling your real data from office…", "Building the list + form…", "It's live — here's the link." Do not narrate internal mechanics — kit/barrel drift, ESLint/typecheck/build steps, file diffs, git plumbing, overlay-merge internals — unless the designer asks. When something technical is worth surfacing (a kit gap, a required field the mockup omits, a deviation from the design), state the impact in one designer-facing sentence and offer the detail rather than dumping it: "(want the technical details?)". The final handoff leads with what the designer can see and do; keep the verification proof to a short ✓ line that expands on request. (Git approval prompts follow the same rule — see Authentication.)
Authentication: the embedded PAT
This skill carries a shared fine-grained GitHub PAT scoped to GoFMX/fmx-prototypes (Contents: read/write + Deployments: read + Metadata: read). Use it for all git operations via:
https://x-access-token:PAT_PLACEHOLDER@github.com/GoFMX/fmx-prototypes.git
Never print the PAT or this authenticated URL in chat — use the URL only inside git commands. If pushes fail with 401/403, see Failure handling.
A safety/approval prompt on a git command is expected — it is not a failure. The PAT in a git URL (or any push/force-push) may surface a one-time permission prompt. That is the normal path: the designer simply approves it, and per-operation approval is fine. Do not treat the prompt as a wall, do not present the designer a menu of auth options or ask "how should I handle authentication," and do not invent workarounds (credential-helper files, etc.) to route around it. Run the straightforward git command and let the prompt be approved. Auth mechanics are never a designer decision — the only things you stop a designer for are product choices (slug, kit, screens, tenant) and the approval click itself.
Local dependencies: the Font Awesome token
The kits depend on Font Awesome Pro packages (@fortawesome/*) from a private npm registry. A fresh clone's pnpm install will 401 on those packages unless FONTAWESOME_NPM_AUTH_TOKEN is set in the environment — the repo's .npmrc reads //npm.fontawesome.com/:_authToken=${FONTAWESOME_NPM_AUTH_TOKEN}. This skill carries the token so the designer never has to obtain it.
Before any pnpm install, set the token as an ephemeral, session-only env var, then install:
# PowerShell (Windows):
$env:FONTAWESOME_NPM_AUTH_TOKEN = "FONTAWESOME_TOKEN_PLACEHOLDER"
# bash / zsh (macOS, Linux):
export FONTAWESOME_NPM_AUTH_TOKEN=FONTAWESOME_TOKEN_PLACEHOLDER
pnpm install
It's session-scoped on purpose — never write it to a file, never git config/commit it, and never print it (same rule as the PAT). A project .npmrc key overrides the user ~/.npmrc, so the env var — not npm config set — is the only thing that works here.
Identifying the designer and authoring commits
Two identities are at play on every commit, and they're different on purpose:
- The author/committer is the automation — the GitHub account that owns this skill's PAT. Vercel blocks any deploy whose commit author can't be matched to a GitHub account on the team (FMX designers don't have GitHub accounts), so the commit must be authored by the PAT's own account, which already is a team member. Derive it once per session from the PAT and set it as local repo config (see "creating a new prototype" step 3) — never
--global. - The designer is credited as a co-author. Every commit MUST include a
Co-authored-by: <designer-email>trailer. To obtain the email:- Check the current session's user context for a
@gofmx.comemail. If found, use it. - Otherwise, ask the designer once at the start of the session: "What email should I attribute commits to?" Remember it for the rest of the session.
- Never invent an email.
- Check the current session's user context for a
Do not set the git author to the designer's email (it isn't a GitHub account → Vercel blocks the deploy) or to an invented bot address like prototypes@gofmx.com (matches no GitHub account → same block). The author is always the PAT's account; the designer is always the co-author.
When to invoke this skill
Invoke when the designer:
- Pastes a Figma URL (figma.com/design/...) and asks to build, prototype, make-interactive, or convert it
- Says "new prototype called X" or "iterate on prototype X"
- Asks to refresh the kit on an existing prototype
- Mentions "design exploration", "interactive mockup", or "share with prospects"
- Asks to publish, push, or refresh the kit's Code Connect mappings to Figma (covered in "Workflow: publishing kit Code Connect mappings")
- Asks "how do I get started" / "what can I build with this" / "what's the workflow" — point them at
docs/designer-onboarding.mdin the repo for the full walkthrough; offer to summarize a section if they'd like
Do NOT invoke this skill for:
- Asking about Figma designs without intent to build (e.g., "what colors does this use?")
- Questions about FMX product features unrelated to prototyping
- General coding help outside the prototype pipeline
Available kits
Kits are directories under kits/ on main. List them at runtime with ls main:kits/ (after fetch) or by reading kits/*/kit.meta.json — that's the source of truth, not this skill. If the designer asks for a kit that doesn't exist, tell them what's available and ask which to use.
A prototype is locked to one kit at creation. Kits do not share components — switching a prototype's kit later means a rewrite, not a config change. Confirm the kit choice with the designer before scaffolding if there's any ambiguity.
Component and token rules (load-bearing)
For this kit's design-system specifics (raised-surface/token roles, state-layer semantics, the token-scale suffix set, known gotchas), see kits/<name>/CONVENTIONS.md if present — it's the same kit-owned reference the engineer kit playbook (docs/kits.md) points to, and it matters most when you're approximating a kit gap. The rules below are kit-agnostic.
Components. Use only the components that exist in the chosen kit's kits/<name>/components/ directory. After branching from main and recording the kit choice, read kits/<name>/kit.meta.json for the authoritative component inventory — it lists every shipped component with its source path.
For variants, states, and structural composition of any specific component, do NOT enumerate from memory. The Figma MCP's get_design_context (required at step 4 of "creating a new prototype") returns Code Connect snippets that encode variants, states, and composition verbatim. Use those — they update with the kit, this skill doesn't.
Never invent components. If the Figma design uses something the kit doesn't have (e.g., a Date Picker when the kit doesn't ship one), tell the designer the gap explicitly: "This kit doesn't have a Date Picker component yet. Options: (1) wait for it to be added to the kit (engineering work), (2) approximate with an existing kit component for now and flag it." Do not silently substitute a generic React/HTML control. Never reach into another kit's directory either — kits don't share components.
Record every kit gap in kits/<name>/GAPS.md (besides telling the designer) — a verbal flag evaporates and the same gap gets re-approximated across prototypes. Add a row with the component, the gap, and the prototype/date; open an engineering PR to close it in the kit; remove the row when it ships. If you approximate or extend the kit in-branch as a reference (kit changes are jointly eng+design reviewed before they land on main), say so in the commit and the GAPS row.
Never use generic Tailwind classes for visual styling. No bg-blue-500, no text-zinc-900, no bg-[#ff0000]. Use the kit's component props and let the kit's tokens drive the visuals. Tailwind is fine for layout (flex, grid, spacing utilities) when no kit token applies.
Tokens. All visual values (color, spacing, radius, typography, opacity, border) must come from the chosen kit's tokens/ files:
import { color } from "@/kits/<name>/tokens/color"
import { spacing } from "@/kits/<name>/tokens/spacing"
// etc.
Never hardcode hex values. Never use Tailwind arbitrary values like bg-[#abc] or text-[14px]. Never import from a kit other than the prototype's chosen one — the per-prototype ESLint config will reject it, but don't generate the import in the first place.
Composition patterns
Composition idioms live in the kit, not in this skill. The Code Connect snippets returned by get_design_context already encode the wrapper/composition expected by each component — follow them verbatim. For broader context, the kit's showcase at app/kits/<name>/page.tsx is the model for form grouping, field layout, and section structure.
Page & form layout patterns (sticky full-bleed PageHeading with content scrolling beneath — never double-pad it; tab navigation through PageHeading's tabs prop, never a sibling bar; the navbar menu toggle wired to sidebar visibility; horizontal-field forms capped at max-w-[772px] and left-aligned) are written up in the kit's CONVENTIONS.md (GO: kits/go/CONVENTIONS.md §"Page & form layout patterns"). Follow them so every screen lays out the same way. In particular, every prototype's app shell must wire Navbar's onMenuToggle to a sidebarOpen state that toggles the Sidebar's hidden class — the toggle is a kit callback that does nothing until the layout connects it.
Pushing prototype screens back into Figma
When the designer asks for a Figma mockup of a prototype screen (code-to-design), place real Figma component instances via Code Connect — never upload a screenshot. Before generating any frame, call get_design_context on the prototype's kit Figma file and use the returned Code Connect snippets as the source of truth for which components to place. Screenshots in Figma are dead pixels: they ignore design tokens, can't be re-themed, and block designers from iterating — the entire point of Code Connect is keeping that loop live. If a screen uses a component the kit has no Code Connect mapping for, flag it as a kit gap (kits/<name>/GAPS.md) instead of falling back to a screenshot or image fill.
FMX API, tenant, and the read-only-with-overlay model
Tenant is NEVER hardcoded. The prototype uses the FMX OAuth flow plumbing on main (app/auth/login/page.tsx + app/api/auth/*); the viewer picks their hostname (and optionally a non-prod environment) at sign-in. The session cookie carries hostname + env; all API calls resolve to https://<hostname>.gofmx.com (production) or https://<hostname>.<env>.gofmx.dev (non-prod). Every session is read-only — the env only selects which tenant you address, not what you can do.
FMX is read-only everywhere; writes are local. The OAuth token never requests api_write_all in any environment, so a write can't reach FMX even on non-prod. fmx.post/put/patch/delete do not hit FMX — they persist to a per-viewer Neon overlay (lib/fmx-overlay.ts), and fmx.getSingle/list transparently merge that overlay back into FMX reads, so a created/edited/deleted record shows up in subsequent reads as FMX data plus the viewer's local changes. Each viewer's overlay is isolated (keyed by FMX email, cookie fallback) and resettable from the session tab (the top-center session widget).
To call the FMX API from prototype code:
import { redirect } from "next/navigation"
import { revalidatePath } from "next/cache"
import { fmx, FmxAuthRequiredError } from "@/lib/fmx-client"
// in a Server Action:
export async function createThing(body: ThingInput): Promise<void> {
try {
// Persists to the per-viewer overlay, not FMX.
await fmx.post("/api/v1/maintenance-requests", body)
} catch (e) {
if (e instanceof FmxAuthRequiredError) redirect("/auth/login?returnTo=/things/new")
throw e // fail loud
}
// REQUIRED: the route + the FAB-bearing layout are cached, so without this the
// new record and the FAB's local-change count are served stale (invisible).
revalidatePath("/", "layout")
redirect("/")
}
fmx reads hostname+env+token from cookies and adds Authorization: Bearer <token>. The write verbs keep their signatures but route to the overlay; the public client surface is unchanged. Every write action follows this shape: try → catch FmxAuthRequiredError→login / rethrow → revalidatePath → redirect (full rule: AGENTS.md fmx-overlay-revalidation). The write body must use the same field names the list/detail reads, or the locally-created record renders blank in the merged view.
Shared helpers (don't re-roll these): wrap a top-level page's FMX read in withAuth(promise, returnTo) (lib/with-auth.ts) — it does the same FmxAuthRequiredError→/auth/login redirect / rethrow as the action shape above, so an expired session on a page load lands on sign-in instead of the error boundary. Stamp created/assigned/finalized timestamps in write bodies with nowNaive() (lib/datetime.ts).
Reading data — three methods, by the question you're asking. fmx.getSingle<T>(path) for one record by id; fmx.getMany<T>("/api/v1/<collection>", ids) to resolve a set of ids to records (e.g. the building/user/resource names a record references); fmx.list<T>(path) for a collection or filtered query. Never hand-roll a comma-joined fmx.list(`/api/v1/buildings/${ids.join(",")}`) to resolve ids — that batch endpoint returns a single object for one id, so fmx.list's array-guard throws and every single-record page 500s (the list page survives only because it passes many ids). Use fmx.getMany; it routes 0/1/many ids correctly. Full reference: docs/fmx-api.md.
Fail loud, never degrade (full rule in AGENTS.md). Catch an FMX exception only to redirect on FmxAuthRequiredError or to rethrow — never .catch(() => []), ?? [], or default-to-empty on an FMX call. Any non-200, network error, or unexpected response shape must throw to the error boundary (app/error.tsx / app/global-error.tsx). The overlay path fails loud too: a missing prototype slug, an unreachable Neon, or a base record missing its identity key throws rather than dropping data.
Discovering endpoints. See docs/fmx-api.md for the full reference. Pre-flight discovery is a required step of creating a new prototype (step 6 below): it makes authenticated REST calls through lib/fmx-client.ts against a tenant the designer signs into, via the discovery handback (/api/discovery/begin + complete). Endpoint paths and write-shapes both come from the tenant's OpenAPI spec at /api/v1/docs, fetched once during discovery and treated as the single source of truth. The discovery output is merged into fmx.contract.json — including each endpoint's optional identityKey (the field the overlay merges on; defaults to id/ID).
Persistence: when the prototype needs NEW data
Some prototypes need to remember data that doesn't fit an existing FMX shape — feature votes, custom feedback forms, ad-hoc settings, anything the production FMX product doesn't model yet. For those, use the Neon Postgres helper at lib/db.ts:
import { getPrototypeDb } from "@/lib/db";
// In a Server Action or Route Handler:
const db = getPrototypeDb("maintenance-request-form"); // the prototype slug
await db.ensureSchema(); // idempotent; runs once per prototype lifecycle
const rows = await db.query<{ id: number; rating: number }>(
`SELECT id, rating FROM ratings WHERE building_id = $1`,
[buildingId],
);
Architecture: one shared Neon project, one Postgres schema per prototype (named prototype_<slug>). The helper sets search_path per request so the prototype's queries are scoped to its own schema — designers can't accidentally read or write another prototype's data.
Rules:
- Only use Neon for data FMX doesn't already model. If the design is "submit a maintenance request" and FMX has a maintenance-requests endpoint, use the FMX API — not Neon.
- Always go through
getPrototypeDb(<slug>)— never construct aneon()client directly. The helper enforces schema isolation and slug validation. - Use
db.ensureSchema()once at the start of a feature (e.g., in a setup script or the first server action that needs the schema) so theCREATE SCHEMA IF NOT EXISTSis paid once. It's idempotent so calling it on every request is also safe. - Tables are scoped to the prototype's schema automatically — write
CREATE TABLE ratings (...), notCREATE TABLE prototype_<slug>.ratings (...). The helper handles the search_path. - The
DATABASE_URLenv var is auto-injected by Vercel's Neon Marketplace integration. Locally, set it in.env.localif you need to run against the real DB (most prototype work won't).
Cleanup: when a prototype is retired, run DROP SCHEMA "prototype_<slug>" CASCADE in the Neon console to free the storage. There's no automated cleanup today.
Branch rules
Push to prototype/<slug> freely (with proper commits + attribution). Never push to main — branch protection rejects it, and kit/platform changes need an engineering PR. If a designer asks you to modify a kit or platform code, redirect them: "Changes to kits/<name>/ or platform basics need an engineering PR. Want me to draft the change as a description you can hand to engineering?"
On prototype/*, force-push/amend freely when fixing your own just-pushed commit — don't narrate git mechanics (lease, amend rationale) to the designer; just do it and report the one-line result and URL impact.
Prototypes are isolated experiments. Every prototype branches from main and only main. Never base a new prototype on an existing prototype/* branch — don't branch from it, copy files from it, or read it as a structural reference — unless the designer explicitly asks for that prototype by name. Prototype branches routinely contain failed experiments and throwaway code; what worked reaches main through deliberate promotion (kit PRs, GAPS.md, updates to this skill), and that promoted material is the only ambient reuse channel. (The "Reusable patterns" paragraph in step 8 is exactly such distillation — follow it freely; raw branch-to-branch copying is not that.)
When the designer does explicitly ask to base prototype B on prototype A: confirm what specifically should carry over, then still branch from main and copy only the named files/screens from A — never git checkout -b off A, which silently inherits stale platform/kit code and A's contract. Regenerate fmx.config.json and fmx.contract.json for the new slug (B gets its own subdomain, overlay scope, and Neon schema), and run the step-10 pnpm verify since the copied code now sits on current main.
Workflow: creating a new prototype
When the designer says "new prototype <slug> from <figma-url> using the <name> kit":
-
Validate inputs:
- Slug should be kebab-case, ~3–5 words. If the designer's slug has spaces or odd chars, normalize and confirm with them.
- Propose a date-suffixed slug by default. Append today's date to the normalized slug (
<slug>-YYYY-MM-DD, e.g.request-form-2026-06-12) and confirm it in one designer-facing line: "I'll call itrequest-form-2026-06-12— the date keeps the link unique and shows when it was made. Want it without the date?" If the designer prefers the bare slug, that's fine — it's a convention, not a gate. The suffix avoids branch/subdomain collisions across eras of work and timestamps the experiment right in the URL. - Keep the full slug ≤ 53 characters — it becomes the Neon schema
prototype_<slug>and Postgres caps identifiers at 63. The date suffix takes 11, so keep the base name to ~42; if over, shorten the base (keep the date) and confirm with the designer. - Check the branch doesn't already exist:
git ls-remote --heads origin "prototype/<slug>". A hit means this is really an iteration on an existing prototype, or it needs a different slug — ask the designer; never reuse or overwrite an existing prototype branch. - Slug must not end in
-kit— that suffix is reserved for kit showcase aliases. If the designer proposes one, suggest an alternative. - Kit must exist as a directory: confirm
kits/<name>/kit.meta.jsonis present onmain(after fetch). If unsure, listkits/*/kit.meta.jsonand ask the designer which to use. - Figma URL should be a
figma.com/design/...URL with anode-idparameter.
-
Clone or update the local working copy, then install dependencies:
git clone https://x-access-token:<PAT>@github.com/GoFMX/fmx-prototypes.git cd fmx-prototypesIf the directory already exists from a previous session,
cdin andgit fetch origin && git checkout main && git pullinstead.Then install dependencies — first set the Font Awesome token for the session (see "Local dependencies: the Font Awesome token"), or
@fortawesome/*will 401:$env:FONTAWESOME_NPM_AUTH_TOKEN = "FONTAWESOME_TOKEN_PLACEHOLDER" # PowerShell; bash: export … pnpm install -
Branch from main and set the commit identity:
git checkout main git pull git checkout -b prototype/<slug>Branch from
mainonly — never from anotherprototype/*branch (see Branch rules: prototypes are isolated experiments).Then set the commit identity to the automation account that owns the PAT — local repo config, never
--global(this is what Vercel matches to authorize the deploy; see "Identifying the designer and authoring commits"):# Derive the PAT owner's GitHub identity (run inside the clone): # GET https://api.github.com/user with Authorization: Bearer <PAT> → { login, id } git config user.name "<login>" git config user.email "<id>+<login>@users.noreply.github.com"The
noreplyform is always GitHub-verified for that account, so the deploy resolves. (GET /userreturns the token owner for any valid fine-grained PAT — no extra scope needed.) If that call ever fails, stop and surface it rather than guessing an identity — an unverifiable author is exactly what blocks the deploy. Do not set--global; do not author as the designer or an invented bot email. -
Lock the prototype to the chosen kit. Add a per-prototype ESLint override at
.eslintrc.kit.cjs(or extend the existing config locally) that blocks cross-kit imports:// .eslintrc.kit.cjs — per-prototype kit lock module.exports = { rules: { 'no-restricted-imports': ['error', { patterns: [{ group: ['@/kits/!(<name>)/**'], message: 'This prototype is locked to kit `<name>`. Do not import from other kits.', }], }], }, };Add this to ESLint's config chain (the project's
eslint.config.mjsreads it). This converts an accidental cross-kit import from "silent wrong code" into a lint error before commit. The skill's behavioral instruction is the primary defense; this rule is belt-and-suspenders. -
Fetch Figma design context using the Figma MCP
get_design_contexttool with the node ID and file key extracted from the URL. The response contains kit-aware Code Connect snippets (because the kit has Code Connect mappings published). Use these snippets as the structural template for the prototype's React code — DO NOT use the raw HTML/positioning fallback the Figma MCP returns if Code Connect is missing.The Code Connect mappings are anchored to the kit's Figma
fileKey + nodeId, so import paths in the returned snippets already point at@/kits/<name>/components/.... Use them verbatim. If the design references a component instance from a different Figma library (a Frankenstein mockup), stop and surface this to the designer — don't write imports from a different kit.If
get_code_connect_mapreturns empty for this kit, stop and tell the designer: "This kit doesn't have Code Connect mappings published yet. Engineering needs to publish them before this kit can be used for prototypes." -
Pre-flight FMX discovery (raw, authenticated, read-only). Ground the prototype in real data by signing the designer into their FMX tenant and making real authenticated REST calls yourself — you read full responses (real records, not just shapes), so you have rich context for code-gen. The handback token is read-only (FMX rejects any write), short-lived, and for the designer's own tenant.
a. Know what you'll touch. From step 5's Code Connect output, list the kinds of FMX resources the prototype reads/writes (e.g., buildings, request types, maintenance requests). You'll resolve these to literal
/api/v1/...paths in substep (d) by searching the tenant's OpenAPI spec.b. Get the designer's FMX URL (coach them on the choice). Ask which FMX site should ground the prototype in real data — explain you'll sign into that tenant to read its real buildings, request types, custom fields, etc., so the prototype reflects actual data instead of guesses, and they should pick the site whose data best represents the demo. For example:
"Which FMX site should this prototype pull real data from? Paste its URL — the address you log into FMX at, e.g.
https://yourorg.gofmx.com. I'll have you sign in there so I can read your real data. (Everything is read-only — I can't change anything, and neither can the live prototype: when it's live, a viewer's submit is saved as their own local change and merged into what they see, never written to the tenant. Production or non-prod both work — pick whichever data best represents the demo.)"Pass the URL straight to
begin/exchange(below) asurl— they validate + parse it server-side (same parser as/auth/login); a non-*.gofmx.com/*.gofmx.devURL returns a clear error.c. Get a read-only token via the handback against
main.prototypes.gofmx.dev(a*.prototypes.gofmx.devsubdomain — it satisfies FMX's redirect whitelist, which the bare apex does not):POST https://main.prototypes.gofmx.dev/api/discovery/beginwith{ url }→{ authorizeUrl, verifier }.- Show
authorizeUrland copy it to the clipboard so they can paste it cleanly (the CLI wraps long URLs): use the OS clipboard tool —clip(Windows),pbcopy(macOS),xclip -selection clipboardorwl-copy(Linux). If the copy fails or no tool exists (e.g. the cloud/web sandbox), just present the URL (the web chat makes it clickable). Ask them to open it, sign in, and paste back the one-time code. POST .../api/discovery/exchangewith{ url, verifier, code }→{ ok, accessToken, expiresIn, apiBaseUrl }. On{ reason: "code_rejected" }, the code expired/was wrong — re-runbeginand have them sign in again.
d. Fetch the OpenAPI spec, then make raw authenticated calls. Stash
accessTokenin an env var or a gitignored temp file and NEVER echo it (same rule as the embedded PAT) — it's read-only, so any write 403s, but treat it as a secret. All calls passAuthorization: Bearer $TOKEN(via env var / header file /--data @-, never inline on the command line):- First,
GET $apiBaseUrl/api/v1/docs→ the tenant's OpenAPI JSON. Stash it locally for the rest of this skill run; it's the single source of truth for both endpoint paths and write-shapes (required body fields, customFields). - Resolve the resources from (a) to literal paths by searching the spec's
paths+tags. For work-request modules, use thepresetModulesenum on/api/v1/request-typesto map the module name to its slug, then apply the/v1/{module}-requestspattern: e.g.,transportationRequest→ stripRequest→transportation→/api/v1/transportation-requests. Scheduling is not a work-request module — it has its own/api/v1/scheduling/occurrencesand/api/v1/scheduling/requestsroutes; don't conflate the two (a transportation request goes to/transportation-requests, not/scheduling/requests). GET $apiBaseUrl/api/v1/<list endpoints>→ full responses: real records (building names, request types, …) plus the bare-array shape, so you write correct parsing and realistic UI.
Summarize to the designer inline, e.g.:
"Signed into
office.GET /api/v1/buildings→ 42 buildings (bare array). Submitting a maintenance request requiresname,buildingID,requestTypeID,dueDate; the endpoint also takes customFields (Priority, Asset Tag)."Required-fields policy: report what the endpoint requires so the prototype's form can cover those fields itself — there's no runtime safety net to fill them in. Writes go to the per-viewer overlay now, so unmodeled fields simply aren't enforced (the overlay accepts whatever the form submits); favor matching the Figma mockup, and surface any required field the mockup omits so the designer can decide whether to add it.
Graceful skip (loud otherwise): if the designer can't or won't authenticate, write
fmx.contract.jsonwith an emptyendpointsarray and continue — overlay-backed writes still work without a contract. Surface any unexpected failure to the designer rather than degrading silently. -
Write/merge
fmx.contract.jsonat the prototype's root from what you observed in step 6 — the required body fields + customFields from/api/v1/docs, keyed to the literal/api/v1/...paths the prototype calls. If the file already exists with hand-authored entries, merge — never clobber: key endpoints bymethod+path, and unionrequiredBodyby fieldname. Hand-authored fields always win — discovery only adds endpoints/fields not already present, an empty discoveredrequiredBody/customFieldsnever overwrites a populated one, and never drop hand-authorednotes/maxLength/optionalBody/customFields.allowedValues. Path values are the literal paths the prototype calls at runtime, not templated forms — each module gets its own entry. Shape:{ "discoveredAt": "<ISO timestamp now>", "lastSyncedAt": "<ISO timestamp now>", "tenant": "<the OAuth-connected tenant, e.g. office.gofmx.com>", "endpoints": [ { "method": "POST", "path": "/api/v1/maintenance-requests", "requiredBody": [ { "name": "name", "type": "string" }, { "name": "buildingID", "type": "number" }, { "name": "requestTypeID", "type": "number" }, { "name": "dueDate", "type": "datetime", "format": "ISO-8601" } ], "customFields": [ { "id": 589664, "name": "Priority", "required": true, "type": "select", "allowedValues": ["Low", "Normal", "High", "Critical"] } ] } ] }If the prototype's request types span multiple modules (e.g., maintenance + IT + custodial), add one endpoint entry per resolved module path.
lastSyncedAtis the cursor the iteration step advances; on creation it equalsdiscoveredAt. Required field names/types come from the tenant OpenAPI (viacomplete); customFields IDs/allowedValuesare tenant-specific — hand-author them here if you want them documented for the form. If a collection's identity field isn'tid/ID, add anidentityKeyon the endpoint so the overlay merges local changes onto the right records. -
Compose the prototype's
app/page.tsxusing only the chosen kit's components and tokens, following the composition patterns from the Code Connect snippets fetched in step 5 (and the kit's own showcase atapp/kits/<name>/page.tsxas a model). Wire any FMX API interactions throughlib/fmx-client.ts. For multi-screen flows, add additional routes underapp/. Use the discovered required-body fields from step 7 to make the form cover what FMX requires — there's no runtime self-heal to fill gaps (writes are overlay-backed per the FMX API model above). For a required field the Figma mockup omits, surface it to the designer rather than silently dropping it.Visual self-diff against the Figma mockup, before declaring composition done. Code Connect gives you authoritative structural composition — but the content inside the snippets (placeholder labels, default variant choices, snippet-baked example strings) is identity, not fidelity. After composing
app/page.tsx, call the Figma MCP'sget_screenshoton the same node ID you used at step 5 and cross-check the composition against the rendered mockup field-by-field. Specifically reconcile:- Control types — text input vs textarea vs date picker vs select. Code Connect often defaults to text input regardless of the design.
- Field presence and order — every input the mockup shows must appear, in the same visual order.
- Required asterisks — required-field markers in the mockup must be on the same fields in code (and absent where the mockup doesn't show them).
- Heading text and count — same words, same number of headings; deduplicate any inadvertent doubles.
- Footer placement and order — submit/cancel button order and alignment match the mockup.
- Snippet content — any literal text the Code Connect snippet brought along (placeholders, labels, hint copy) must be reconciled with what the design actually says.
Beyond visual — three more audits the screenshot can't catch:
- Interaction audit. Visual fidelity ≠ behavioral fidelity. Every control that looks interactive must either do something real or be removed — no buttons wired to no-op handlers, no underlined "links" with no destination, no row-open chevron and a linked cell (pick one; hide the kit's chevron with
showOpenAction={false}). If a row/link "opens" a record, the destination route (app/requests/[id]) must actually exist. Dead affordances pass a screenshot diff and fail the first click. - Data self-diff. Every field the UI renders must be present in a real record you fetched in step 6 — not assumed from the mockup. For any column that comes back empty, check whether FMX needs a
fields=expansion before concluding it's truly empty (e.g. drivers live onfinalizations(driver(id,name)), not the request). Mockup sample values ("City of Pawnee", a driver name) are illustrative — confirm the real shape. The write body must populate the same field names the list reads, or local creates render blank. - Identity binding. Navbar org/user and any "logged-in user" chrome come from the session via
getNavIdentity()(lib/nav-identity.ts), never transcribed from the mockup's sample names. Form initial values are all-string/null (SSR-serializable); datetimes split into date+time strings on read and join to ISO on submit viasplitDateTime/joinDateTime(lib/datetime.ts, which also has the naive-safeformatDate/formatDateTime/formatTimestamp/formatTripTimedisplay formatters — nevernew Date(iso)on a naive FMX datetime, it shifts by the local zone). Date/time fields render with the kit'sDatePicker/TimePicker(they trade in exactly those date/time strings), never the off-brand native<input type="date|time">.
Surface each gap and either fix it or flag it as a deliberate deviation. Don't move on with unaddressed fidelity gaps — they're the cheapest class of regression to catch here and the most expensive to catch after handoff.
Reusable patterns (lessons from prior prototypes): a list + "click a row to edit" flow is one polymorphic form component (optional
initial+requestId;isEdit = Boolean(requestId)gates the create-vs-update action and the button/message copy) reused by bothapp/requests/newandapp/requests/[id], plus a sharedloadFormOptions()awaited in parallel with the record + identity — this prevents option-schema drift between the two pages. Extract pure row-mapping utilities (distinct/sum/normalize) instead of inline.map().filter()chains (datetime split/join/format already live inlib/datetime.ts); sort bygetTime(), never by a display string. Validation returns(errors map, ordered messages)so the summary alert lists errors in visual field order. -
Write
fmx.config.jsonat the prototype's root:{ "slug": "<slug>", "kit": "<name>", "createdBy": "<designer-email>", "createdAt": "<ISO timestamp>", "figmaSource": "<original figma url>", "fmxApi": { "operations": [ { "label": "Read buildings" }, { "label": "Create maintenance requests", "write": true } ] } }slugMUST match theprototype/<slug>branch name, including the date suffix if the slug carries one (e.g."slug": "request-form-2026-06-12"for branchprototype/request-form-2026-06-12). It's used bylib/db.tsas the Postgres schema name (kebab-case → underscores) and bylib/fmx-overlay.tsto scope the prototype's per-viewer write overlay. Any prototype with awrite: trueoperation needs Neon (DATABASE_URL) for the overlay to persist.- Do NOT add a
tenantfield — tenant is chosen at OAuth time.
The
fmxApiblock tells the login page which reads this prototype performs against the real FMX API, so the viewer can see what they're granting access to before they sign in. Only reads reach FMX — writes are overlay-backed and are not listed on the login page. Rules:- Omit
fmxApientirely if the prototype doesn't use the FMX API at all (no imports from@/lib/fmx-client, no calls to FMX endpoints). operationsis an array of{ label, write? }objects:label: one short verb-phrase per distinct API call. Plain English ("Read buildings", "Update work request"), NOT endpoint paths ("GET /v1/buildings").write: set totruefor any call that POSTs, PUTs, PATCHes, or DELETEs. This flag provisions the per-viewer Neon overlay — it's whatlib/fmx-config.tsreads forhasWritesto decide the prototype needs Neon (DATABASE_URL). Omit (or setfalse) for reads. Author it accurately even though write ops aren't displayed on the login page (they're overlay-backed, so they don't describe real FMX access) — the flag still drives the overlay.
- Stable ordering: reads before writes; within each, by the order they first appear in the prototype's flow. (Ordering is for config tidiness — writes aren't displayed.)
Two examples:
Read-only browser:
"fmxApi": { "operations": [ { "label": "Read buildings" }, { "label": "Read equipment" } ] }Form that creates work requests (matches the maintenance-request-form prototype):
"fmxApi": { "operations": [ { "label": "Read buildings" }, { "label": "Read request types" }, { "label": "Create maintenance requests", "write": true } ] }When iterating on a prototype later, keep
fmxApiaccurate — if you add a new write call, append it tooperationswithwrite: true. -
Verify, then commit and push. Before committing, run the compile-time gate — and do not commit until it passes:
pnpm verify # eslint (incl. the Server→Client handler rule) + tsc --noEmit + next build
This is the same gate CI runs. It catches the regressions that are otherwise invisible until a viewer hits the page: the Tailwind content-scan 500 (only next build compiles every utility eagerly — next dev does it lazily, so a bad token 500s at request time, not at build), unsafe access on optional FMX fields like finalizations (tsc, given honest optional types), and an event-handler prop passed from a Server Component (eslint). Run it silently per the narration rule above; if it fails, fix and re-run — never commit a red verify. Then:
git add .
git commit -m "Initial prototype: <slug>
Co-authored-by: <designer-email>"
git push -u origin prototype/<slug>
CI re-runs pnpm verify on the push as a backstop — but a push to prototype/* also kicks off the deploy, so CI catches a miss after it is already deploying. The real gate is here, before the commit; CI is the safety net, not the first line.
- Smoke-test the deploy before handoff. Vercel takes ~60–90s to build, then a GitHub Action aliases to the clean URL in another ~10–20s.
First, confirm the deploy itself succeeded — Vercel mirrors deploy state into GitHub, and the PAT has Deployments: read, so you get an explicit signal instead of inferring failure from a 404. Poll the deployment status for the SHA you just pushed:
# via the PAT (Authorization: Bearer <PAT>, Accept: application/vnd.github+json):
GET /repos/GoFMX/fmx-prototypes/deployments?sha=<pushed-sha> → newest deployment id
GET /repos/GoFMX/fmx-prototypes/deployments/<id>/statuses → state + log_url + environment_url
state: success→ proceed to the browser checks below.state: failure/error, or the deploy shows blocked → surface it in plain language with the inspector link (log_url), don't hand over a dead URL. Self-heal the author-block case: if the block is "commit email … could not be matched to a GitHub account", the commit identity wasn't set to the PAT account (step 3) — ask the designer nothing technical; re-stamp and re-push:git commit --amend --author="<login> <id+login@users.noreply.github.com>" --no-editthen force-push. (With step 3 done this should not happen.)- GitHub gives state + the inspector link, not full build-log text — for a build error needing log detail, open
log_url(or ask engineering to read the Vercel runtime logs).
Then verify with the chrome-devtools MCP:
new_pagetohttps://<slug>.prototypes.gofmx.dev→ expect 200 or 302 to/auth/login. A 404 means the alias hasn't landed yet — wait another 30s and retry up to twice.navigate_pagetohttps://<slug>.prototypes.gofmx.dev/auth/login→ expect 200, a centered card-width layout (not collapsed to ~16px wide), and no error-boundary text ("Something went wrong"). This catches CSS-token regressions and layout-collapse bugs the root URL wouldn't surface.list_console_messagesandlist_network_requests→ no unhandled exceptions, no 4xx/5xx on the page's own assets.- Load a real record's detail/read page (any prototype with a single-record route like
/<thing>/[id]). The list view resolves names in bulk and can render fine while a single-record page fails — they're different code paths, so load both. This needs a signed-in session (records require FMX data), so do it inside the signed-in pass below; for a read-only prototype, sign in once for just this check. Navigate from the list to the first real record and confirm: HTTP 200, the record's fields visible, no error-boundary text ("Something went wrong"), and no 5xx for the detail document inlist_network_requests. This is the check that catches a single-id batch-resolve regression — many ids on the list succeed while one id on the detail page 500s. On a 500, readerror.digestand cross-reference Vercel runtime logs.
Authed round-trip for write-capable prototypes (fmxApi has a write: true op). The anonymous smoke test above can't catch the most common write bug — a create that persists but never revalidates into view (cached route → new row + FAB count stay hidden), or a write body whose field names don't match what the list reads (row renders blank). So do one signed-in pass: on the prototype's own subdomain, connect to a tenant, create a record, and confirm it appears in the list AND the FAB's local-change count increments; if there's an edit route, open a row and confirm the form pre-fills. This is a fresh sign-in (cookies don't share with main.prototypes.gofmx.dev). If chrome-devtools can't complete the OAuth round-trip, say so and ask the designer to run this exact check first. (Read-only prototypes: the dropdown-populate check is covered by the designer's first interaction, but still run the detail-page check above if the prototype has a single-record route.)
If anything fails, fix it and re-push before handoff — the goal is to hand the designer a working URL, not a URL plus a list of things to debug. app/error.tsx / app/global-error.tsx surface error.digest for any server throw; cross-reference against Vercel runtime logs to localize.
If chrome-devtools MCP isn't available in this session: skip the smoke test and say so explicitly at step 12 ("I can't drive a browser from this session to verify the deploy — please load the URL and let me know if anything looks off").
- Report the verified URL to the designer:
"Pushed and live at https://<slug>.prototypes.gofmx.dev — I loaded the root and the
/auth/loginroute and both render cleanly. The first time you click anything that needs FMX data, you'll be redirected to a sign-in page where you pick the FMX tenant — any tenant URL works (production or non-prod), all access is read-only, and anything you submit is saved as your own local changes."
Workflow: iterating on an existing prototype
When the designer says "add a date field for completion date" or "make the table sortable" on an existing prototype:
-
Switch to the prototype branch (clone if not present):
git checkout prototype/<slug> git pull -
Optionally refresh discovery for tenant config drift. If the iteration touches FMX writes, re-run pre-flight discovery (the step-6 handback) against the current tenant and diff against the existing
fmx.contract.json; if new required fields appeared (tenant config drift), surface them so the designer can decide whether the form should cover them. Write the refreshedfmx.contract.jsonwithlastSyncedAtbumped to now. Skip this if the iteration is purely visual or re-authenticating is heavier than you need. The contract is tenant-specific — field names and capabilities differ between tenants (a field present on one tenant may be absent or named differently on another; nested data may need afields=expansion one tenant exposes and another doesn't). If the prototype will be demoed on a different tenant than it was discovered against, re-verify the shapes there before trusting the contract. -
Propose the diff before applying the designer's actual ask. If the change is small (one component, a few lines), describe it briefly and apply. If non-trivial (new route, refactor, new state), describe the plan and ask the designer to confirm before applying.
-
Apply the edits following all kit/token/composition rules.
-
Commit and push with a descriptive subject +
Co-authored-bytrailer:git add . git commit -m "<short subject describing the change> Co-authored-by: <designer-email>" git push -
Confirm the URL is unchanged and the redeploy is starting: "Pushed. Same URL — refresh in ~60–90s."
Workflow: refreshing the kit on a prototype
When the designer says "pull in the latest kit" or "update to the new design system version" on a prototype/<slug> branch:
- Read
fmx.config.jsonto determine which kit the prototype is locked to ("kit": "<name>", or for prototypes scaffolded under the old branch model,"parentKit": "kit/<name>"). - Merge from
main:
This brings in both platform updates and any updates togit fetch origin git merge origin/mainkits/<name>/. Files at unchanged paths get a real 3-way merge. - Resolve conflicts if any (usually only in files the designer has customized in the prototype). For each conflict, prefer
main's version for files underkits/<name>/andlib/(the kit + platform are authoritative); prefer the prototype's version for prototype-specific files (app/page.tsx,fmx.config.json, etc.). Ask the designer when unsure. - Commit and push:
git commit -m "Refresh kit <name> (and platform) from main Co-authored-by: <designer-email>" git push
Workflow: publishing kit Code Connect mappings
This workflow is needed when a kit's React components or kits/<name>/code-connect/*.figma.tsx files have changed, and the updated mappings need to be pushed to Figma so designers' Claude sessions resolve to the latest components. This is normally engineering's job (changes to kits/<name>/ are PR-required), but anyone with permission to write component code on the kit's Figma file can run the publish.
The command is pnpm publish-mappings <name>, run from main (or any branch — it scopes by the kit name you pass). It needs FIGMA_ACCESS_TOKEN in the environment. If the token is missing, the script prints its own walkthrough — follow it, then re-run.
When the user asks to publish kit mappings, walk them through:
- Confirm which kit. If they didn't name one, list
kits/*/kit.meta.jsonand ask. The kit name is the directory name (e.g.,go). - Check whether
FIGMA_ACCESS_TOKENis already set. Trypnpm publish-mappings <name>directly. If the token is present and valid, the publish proceeds — done. If not, the script prints the generation walkthrough and exits. - Help them generate a Figma PAT if needed:
- Open https://www.figma.com/settings → Security tab → Personal access tokens
- Click "Generate new token"
- Name it (suggest:
fmx-kit-publishor similar; their name + machine name is fine) - Expiration: 1 year (GoFMX org policy max). Figma's UI defaults to a short window; verify the dropdown is set to 1 year before continuing.
- Tick exactly these two scope boxes — leave everything else unchecked:
- Files →
file_content:read("Read the contents of and render images from files") - Development →
file_code_connect:write("Write and change component code")
- Files →
- Click "Generate token" and copy the value (only shown once)
- Help them save it locally. Easiest is
.env.localin the repo (already gitignored): tell them to add the lineFIGMA_ACCESS_TOKEN=fig_pat_...to.env.local. Alternatively for an ephemeral shell-only setup,$env:FIGMA_ACCESS_TOKEN = "fig_pat_..."(PowerShell) orexport FIGMA_ACCESS_TOKEN=fig_pat_...(bash/zsh). - Run
pnpm publish-mappings <name>again. It should now succeed and report which components were published. - Verify by calling the Figma MCP's
get_code_connect_mapon one of the kit's component nodes — it should return a mapping pointing atkits/<name>/code-connect/<Component>.figma.tsx.
If the user can't tick the "Write and change component code" box (Figma greys it out): their Figma account doesn't have write access to the kit file. Tell them: "Your Figma account doesn't have write access on the kit file. Ask the design-system owner (or any designer with edit access) to mint the token for you — the CLI doesn't care who owns the token, only that it has the right scopes."
Never display a real PAT in chat output once you have it. Tell the user "save the value you copied to .env.local" — don't echo it back.
Failure handling
- Vercel build fails or is blocked: the prototype URL won't update. Read the deployment status from GitHub via the PAT (
GET /repos/GoFMX/fmx-prototypes/deployments?sha=<sha>→/deployments/<id>/statuses; see step 11) — it gives the state + the inspectorlog_url. Surface the error to the designer in plain language; offer to roll back the latest commit on the branch. For full build-log text (not in the GitHub status), open the inspector URL or ask engineering to read the Vercel runtime logs. - Deploy blocked on commit author ("commit email … could not be matched to a GitHub account"): the commit wasn't authored by the PAT's GitHub account. Confirm step 3 set the local
user.emailto the PAT account'snoreplyaddress (not the designer's email, not an invented bot email), thengit commit --amend --author=…+ force-push. - A git command shows a permission/safety prompt: expected, not an error — the designer approves it and you continue. Don't escalate, present an options menu, or build a workaround (see "Authentication: the embedded PAT").
- PAT auth fails (401/403) after approval: the embedded PAT has expired or been revoked. Tell the designer: "This skill's GitHub token is expired or revoked. Refresh your Claude session — auto-distribution should have pulled the latest version. If it hasn't, ping engineering." Don't ask for a new PAT.
- Figma MCP returns no Code Connect map: stop and tell the designer the kit's Code Connect mappings haven't been published yet. Don't fall back to generating from raw Figma HTML — that's how you get hallucinated lookalikes.
- Push rejected by branch protection on
main: you're on the wrong branch. Checkgit status, switch to aprototype/*branch, and retry.