Prototype with your real components. Review with people and agents.
@compozy/proto is a prototype gallery and review loop for React + Vite apps. You write
*.proto.tsx files beside your views, using the components you ship. proto dev opens a review
shell where people pin comments to elements, regions or flow steps. Each comment records the JSX
source chain of what was clicked, so a coding agent can open the exact line, fix it and answer
through the CLI or MCP. Agents never approve; people do.
Warning
Experimental (0.x). The public surface described here is the 0.x contract and may change between minor versions until 1.0. See Status.
- Prototypes become the shipped UI. Prototypes compose your production views and design system, not throwaway HTML, so the reviewed state is the code that ships.
- Comments point at source. Every comment stores the selector, the element text, the rects
and the
file:line:columnchain of the JSX that rendered it, plus the commit, state, theme, role and viewport. - Agents run the review loop. List, read, reply, resolve, attach screenshots and capture
evidence from the CLI (
--json) or MCP. Everything except approval works without a browser. - Prototypes can't reach production. Build guards fail the build when prototype code reaches your production bundle, or when a prototype reaches your data layer.
- Gallery shell. Overview, spec pages, screen view with states, viewports, themes and roles, flow canvas with live previews, comments inbox, drafts and search. Prebuilt and self-styled, so it never imports your design system.
- Typed prototypes.
definePrototypemeta (title, spec, surface, route, states, roles) is read statically, never executed.proto checkreports broken literals with stable problem codes. - Flows.
defineFlowjourneys across prototype states and external pages, with coverage warnings for states no flow reaches. - Anchored comments. Pick an element, drag a region or move a crosshair with the keyboard. Pins follow their anchor as the layout changes. Threads support replies, image attachments, resolve and reopen. Each prototype or flow has its own review status (in review or approved).
- Isolated rendering. Prototypes render in iframes on a separate origin, compiled by your own
Vite config. They talk to the shell only through a typed, validated
postMessageprotocol. - Evidence.
proto screenshot(PNG via Playwright Chromium) andproto evidence(static HTML without scripts) underdocs/design/<slug>/. - Static build.
proto buildwrites a read-only gallery todist-proto/that works on any static host, at/or under a subpath. - Remote review server.
@compozy/proto/serveris a self-hosted fetch handler for comments and approvals behind your own sign-in, with Postgres or file storage. - Migration.
proto migraterewrites existing v1/v2 review files to the current format losslessly, with a verification report.
Requires a React 19 + Vite 8 app. Proto uses the app's Vite installation; install React and Vite in the consumer before adding Proto.
bun add -d @compozy/proto
bunx proto init --skills
bunx proto dev --openinit writes the config, canvas, welcome draft and Vite plugin integration; --skills also
installs the agent skills. proto --help lists the commands; proto --version
prints the installed package version.
Install these optional peers only for the features you use:
| Feature | Optional peer | Setup |
|---|---|---|
| Screenshots and evidence | playwright (^1.64.0) |
bun add -d playwright then bunx playwright install chromium |
MCP server (proto mcp) |
@modelcontextprotocol/sdk (^1.32.2) |
bun add -d @modelcontextprotocol/sdk |
| Postgres review storage | postgres (^3.4.0) |
bun add postgres in the backend that mounts the review server |
The gallery, local reviews and static builds do not require these optional integrations.
A component prototype lives beside the view it composes and renders it with fixture props:
// src/components/login-form.proto.tsx
import { definePrototype, setParam, type PrototypeProps } from "@compozy/proto";
import { LoginForm } from "./login-form";
export const meta = definePrototype({
title: "Sign in",
spec: "auth",
surface: "A1",
route: "/login",
standalone: true,
states: [
{ id: "sign-in", label: "Sign in" },
{ id: "error", label: "Wrong password" },
],
});
export default function Prototype({ state }: PrototypeProps) {
return (
<LoginForm
mode="sign-in"
error={state === "error" ? "Invalid email or password" : null}
pending={false}
onModeChange={() => {}}
onSubmit={() => setParam("state", "error")}
/>
);
}The gallery lists Authentication → Sign in with both states, without a reload.
- Component prototypes (
src/**/*.proto.tsx) compose views. Unless they setstandalone, they render inside your production chrome (Frame). - Drafts (
src/drafts/<slug>/index.proto.tsx) may use free markup for early exploration. useSearch,setParam(s)andprotoHrefkeep prototype state in the URL, so every state has a shareable link and comments record the state the reviewer saw.
proto.canvas.tsx provides your app's providers and chrome:
import type { CanvasProps, FrameProps } from "@compozy/proto";
import "./src/styles.css"; // your CSS loads on the render origin only
import { ThemeProvider } from "@acme/ui/theme";
import { AppShellView } from "./src/systems/shell/app-shell-view";
/** Providers around every rendered state. */
export function Canvas({ children, theme }: CanvasProps) {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
/** Production chrome around component prototypes (not drafts, not `standalone`). */
export function Frame({ route, children }: FrameProps) {
return <AppShellView activeRoute={route}>{children}</AppShellView>;
}Flows chain prototype states (and external pages) into journeys:
// flows/sign-in.flow.ts
import { defineFlow } from "@compozy/proto/flow";
export const flow = defineFlow({
title: "Sign in",
spec: "auth",
start: ["site"],
nodes: {
site: { external: "site", url: "/" },
"sign-in": { proto: "login-form", state: "sign-in" },
error: { proto: "login-form", state: "error" },
home: { proto: "welcome", state: "default", end: true },
},
edges: [
["site", "sign-in", { on: "Sign in" }],
["sign-in", "error", { on: "Wrong password", kind: "overlay" }],
["error", "home", { on: "Signed in", kind: "redirect" }],
],
});proto flow show|list|coverage print the steps; proto check reports unknown prototypes or states
and warns about states that no flow covers.
-
A reviewer opens the gallery, presses C and clicks an element (or drags a region), writes a comment and optionally attaches images.
-
An agent reads open comments with their source chain and attachments, fixes the code and replies or resolves:
$ proto comments list login-form-7 error reviewer 4m The error should sit under the password field… src/components/login-form.tsx:58:27 $ proto comments resolve login-form-7 --note "Fixed in src/components/login-form.tsx." ✓ login-form-7 resolved
-
The reviewer approves the prototype in the shell or with
proto review approve. Approval records the commit and the number of comments still open. A new comment on an approved prototype puts it back in review. Locally, approval is trust-based (anyone with the repository can approve); on a remote gallery only a browser session with thereviewscope can.
Every command has --json output with stable shapes and exit codes. proto mcp exposes the same
capabilities as MCP tools over stdio (proto_comments_list, proto_comments_reply,
proto_comments_resolve, proto_screenshot, …), and the results equal the CLI --json.
Approval, credentials, migration and process lifecycle are intentionally not MCP tools:
agents never approve.
proto init --skills installs two agent skills into .agents/skills/ at the repository root (the
app root outside Git): proto, which covers how proto works, authoring, setup, the CLI and
troubleshooting, and proto-apply-comments, the review-comment loop. It also runs in an app that
is already set up, without touching its configuration. Run it again after upgrading: it replaces
stale copies (reported as updated) and lists copies of the 0.1.x author-prototype and
apply-prototype-comments skills for you to delete. Installed copies belong to the package, so
keep project-specific rules in your AGENTS.md.
Local review data lives in proto/review/<key>.json (one file per prototype or flow, committed with
your code), written under a per-key lock so the CLI, MCP and the dev server can write concurrently.
Set the deployed gallery URL, including its base path, in the app's proto.config.ts:
import { defineProtoConfig } from "@compozy/proto/config";
export default defineProtoConfig({
title: "Acme",
review: { remote: { url: "https://acme.com/proto/" } },
});Run proto build --remote from that app (in this repository,
bun run proto build --remote --app app). The boolean --remote flag uses PROTO_REMOTE_URL
or the configured review.remote.url and emits a render host that trusts the public origin.
Rebuild when that origin changes. Plain proto build always produces an offline, read-only
gallery for static hosting, even when a remote URL is configured. Node callers select remote
mode with buildProject(project, { remote: "https://acme.com" }) from @compozy/proto/node.
Mount the self-hosted review server in your backend, pointing gallery.dir at that build:
import { createReviewServer, postgresStorage } from "@compozy/proto/server";
// sql is your postgres.js connection; install the optional postgres peer in this backend.
const storage = postgresStorage(sql);
await storage.migrate(); // shipped DDL
export const protoReview = createReviewServer({
storage, // or fileStorage(dir)
basePath: "/proto/",
publicOrigin: "https://acme.com", // gallery + API
renderOrigin: "https://acme-proto.net", // prototypes, on another registrable domain
ticketSecret: process.env.PROTO_TICKET_SECRET!,
gallery: { dir: "../app/dist-proto" },
signIn: (_request, returnTo) =>
Response.redirect(`https://acme.com/login?next=${encodeURIComponent(returnTo)}`, 302),
authorize: async request => identityFromSession(request), // { id, name, scopes } | null
credentialOwner: async ownerId => identityOfUser(ownerId),
});Route /proto/* on acme.com and all paths on acme-proto.net to the returned
protoReview(request) fetch handler. Preserve the original Host header. Both origins require
HTTPS (loopback excepted); use separate registrable domains, not two subdomains of one domain.
Keep review session cookies host-only. The ticket secret must be at least 32 bytes.
The repository's apps/api already mounts this handler and migrates its tables at startup.
Set PROTO_PUBLIC_ORIGIN, PROTO_RENDER_ORIGIN, PROTO_TICKET_SECRET, PROTO_GALLERY_DIR
(the absolute path to apps/app/dist-proto) and the comma-separated PROTO_REVIEW_EMAILS.
Only allowlisted Better Auth users with verified email addresses receive review/comment scopes.
This template has no email sender: an operator must independently confirm address ownership and
provision verification as described in reviewer provisioning.
See deployment details for routing and login setup.
- Sign-in is yours. Browser sessions become reviewers with the
reviewand/orcommentscopes yourauthorizereturns. - Agents use bearer credentials. Reviewers create credentials in Settings → Credentials. A
credential always has only the
commentscope (comment, reply, resolve, attach), so it can never approve, delete comments or manage credentials. The CLI stores it withproto auth loginand uses it with--remote. - Prototype code can't act as the reviewer. Prototypes render on a separate origin that holds
no review cookies, behind short-lived render tickets. Cookie-authenticated writes require the
gallery's origin plus an
X-Proto-Requestheader.
Sign in to the gallery, then run:
proto auth login --remote https://acme.com/proto/
proto auth credentials --json
proto comments list --remote --json
proto comments reply login-form-7 --note "Fixed." --remoteLogin prints the browser's credential settings URL and accepts a pasted token without echoing it.
The CLI saves it locally with mode 0600; tokens are never printed. Store commands use
review.remote.url (or PROTO_REMOTE_URL) with --remote; PROTO_REMOTE_TOKEN can override the
saved token. Manage server credentials, including revocation, in the browser.
Local and remote review are separate; a project chooses where a review happens (no syncing).
proto() in your vite.config.ts and protoPresentation() in Storybook and Vitest enforce two
boundaries:
- Production boundary.
vite buildof your app fails if a.proto.tsx, a prototype fixture or gallery code reaches the production bundle. - Network guard. A prototype, draft or story whose runtime imports reach a module listed in
guards.forbidRuntimeImports(your API client, for example) fails Storybook, Vitest,proto checkandproto build. Your application routes may import it.
// vite.config.ts
import { proto } from "@compozy/proto/vite";
export default defineConfig({ plugins: [react(), proto()] });
// vitest.config.ts / .storybook/main.ts (viteFinal)
import { protoPresentation } from "@compozy/proto/vite";
plugins: [react(), protoPresentation()];Every command accepts --app <dir> (default: cwd), --json and --verbose. Store-backed commands
accept --remote. Exit codes: 0 ok · 1 problems or user error · 2 usage · 3 missing
dependency · 4 remote or auth.
| Command | What it does |
|---|---|
proto init [--skills] |
Scaffold the app; --skills installs or updates agent skills |
proto dev [--port 4400] [--open] |
Gallery on port, render origin on port + 1 |
proto build |
Read-only dist-proto/; --remote builds for the review server |
proto list |
Prototypes and flows |
proto check |
Problems and warnings, network guard, no-write render build |
proto new <slug> [--draft] [--in <view>] [--spec <id>] |
Scaffold a prototype or draft |
proto flow new|show|list|coverage |
Scaffold and inspect flows |
proto url <slug> [--state <id>] [--bare] |
Gallery URL, or the bare render URL |
proto comments list|show|add|reply|resolve|reopen |
Read and answer threads (--status, --waiting, …) |
proto comments attach|detach|edit|attachments|delete |
Images, edits, downloads; delete is reviewer-only |
proto review status|approve|reopen |
Review status; approval is for people |
proto review unlock <key> --force |
Recover a review lock left by another machine |
proto screenshot <slug> [--state] [--theme] [--width] |
PNG under docs/design/<slug>/ (Playwright Chromium) |
proto evidence <slug> [--state] |
Static HTML evidence, no scripts |
proto migrate --from <dir> [--dry-run] |
Rewrite v1/v2 review files to v3 with a verification report |
proto mcp |
MCP server over stdio |
proto auth login|logout --remote <url> |
Store or remove a local remote credential |
proto auth credentials --json |
List local credentials without tokens or network access |
proto auth revoke <id> --remote <url> |
Print the browser-only credential management URL |
proto.config.ts at the app root is the only config file. The Vite plugins, the CLI and proto mcp
read it.
import { defineProtoConfig } from "@compozy/proto/config";
export default defineProtoConfig({
title: "Acme",
specs: { auth: "Authentication", billing: "Billing" },
roles: { member: "Member", admin: "Admin" },
guards: { forbidRuntimeImports: ["src/lib/api.ts", "src/lib/gateway-*.ts"] },
});| Key | Default | Meaning |
|---|---|---|
title |
— | Gallery title |
prototypes |
"src/**/*.proto.tsx" |
Glob(s) of component prototypes |
drafts |
"src/drafts" |
Drafts directory (free markup allowed) |
flows |
"flows" |
Directory of *.flow.ts files |
canvas |
"proto.canvas.tsx" |
File exporting Canvas and optional Frame |
specs |
— | { id: label }, or (root) => Record<string, string> | null |
roles |
— | { id: label } offered by the role picker |
siteUrl |
— | Resolves site nodes in flows |
locale |
"en" |
Shell language: "en" or "pt-BR" |
guards.forbidRuntimeImports |
[] |
Module ids, globs or RegExp that prototypes, drafts and stories can't import |
guards.productionBoundary |
true |
Fail the app build when prototype or gallery code reaches it |
review.remote.url |
— | Default --remote target for CLI/MCP (including build --remote) |
evidence.dir |
"docs/design" |
Where screenshots and evidence are written |
build.includeReview |
false |
Ship review history in static builds |
dev.port |
4400 |
Gallery port; the render origin is port + 1 |
dev.poll |
false |
Polling file watcher |
dev.allowedHosts |
[] |
Extra Host values accepted by the dev servers |
Environment: PROTO_PORT, PROTO_POLL=1, PROTO_REMOTE_URL, PROTO_REMOTE_TOKEN.
| Entry | For |
|---|---|
@compozy/proto |
Authoring: definePrototype, useSearch, setParam(s), protoHref |
@compozy/proto/flow |
defineFlow |
@compozy/proto/config |
defineProtoConfig |
@compozy/proto/vite |
proto(), protoPresentation() |
@compozy/proto/node |
loadProject(): registry, review store, URLs, screenshots |
@compozy/proto/server |
createReviewServer, postgresStorage, fileStorage |
@compozy/proto/mcp |
The MCP server behind proto mcp |
@compozy/proto is experimental (0.x) and under active development in this repository.
- Milestone 1: package, gallery shell, local review loop (shell, CLI, MCP), guards, evidence,
proto migrate, static build. - Milestone 2: remote review server,
--remoteCLI and MCP, credentials. - Then: a documentation site.
Out of scope: frameworks other than React + Vite, replacing Storybook, visual regression diffing, a hosted SaaS, and syncing local and remote review data.
Contributions are welcome. Read CONTRIBUTING.md for setup, commands and conventions, and follow the Code of Conduct. To report a vulnerability, see SECURITY.md.
MIT © 2026 Pedro Nauck and Compozy contributors