reboot/plugin/skills/web-app/SKILL.md
Build complete Reboot Web Apps — a Reboot backend behind a standalone browser-facing React frontend, served at a normal URL (not embedded in an MCP host). Layers on top of the python skill for backend mechanics; covers what's specific to standalone Web Apps — no MCP front door, no UI() methods, normal React/Vite SPA scaffolding, and Reboot auth for browser users.
npx skillsauth add reboot-dev/reboot web-appInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Build complete Reboot Web Apps from a user description: a Reboot backend behind a standalone React frontend served at a normal URL.
Reads from
python. This skill is the standalone-web-frontend layer on top of the Reboot Python framework. Anything about Servicers, Reboot contexts, refs, scheduling primitives, backend LLM / agent calls, error types, the testing harness, the.rbtrcshape, or pydantic API defaults belongs inpython— load those references for those concerns. This skill covers what's specific to standalone Web Apps: a plain React SPA atweb/, the generated TypeScript hooks fromrbt generate --react=..., regular auth flows (login form / cookies / OAuth), and the cross-cutting rules unique to that layer.
Not for MCP Chat Apps. If the app's primary front door is an MCP host (ChatGPT, Claude, VSCode, Goose, …) with MCP tools and embedded UIs, use the chat-app skill instead. Signals you're in the wrong place:
mcp=Tool(),UI(),Userauto-construct from the MCP host,mcp_servers.json, MCPJam inspector.
run skill, which detects
the app type and starts the backend and frontend.The Reboot backend is identical. The deltas are all on the surface:
| Concern | Chat App (chat-app) | Web App (this skill) |
| ------------ | -------------------------------------------------------- | -------------------------------------------------------- |
| Front door | MCP host (ChatGPT, Claude, …) creates a User per user. | Browser user logs in; you decide the auth scheme. |
| API exposure | mcp=Tool() on writer/transaction methods. | Methods exposed only through the generated React client. |
| UI shape | UI() methods → artifacts embedded in the MCP host. | A normal SPA at web/ opened at a URL. |
| Vite config | Special — nested dist/<ui-path>/index.html for MCP. | Stock single-page Vite output. |
| Test surface | MCPJam inspector + mcp_servers.json. | Browser + the standard React devtools / Playwright. |
| User type | Required — the MCP entry point. | Optional — only if your app needs per-user state. |
Backend mechanics (state, methods, Servicers, workflows, refs,
scheduling, stdlib actors, errors, auth predicates, testing) are
unchanged — load them from python.
Web apps wire identity via
Application(token_verifier=<TokenVerifier>), integrating with an
external IdP (Auth0, Firebase, your own JWT issuer, …). Standing
that up is a real piece of work — and until it's done, no caller
has a context.auth.user_id, so authorizer rules that depend on
identity can't be satisfied.
Don't use
Application(oauth=...)for web apps. Theoauth=slot (includingDevelopment()) is currently MCP-chat-app-only and doesn't work for browser-served web apps. For a web app, leaveoauth=unset and use thetoken_verifier=path below. (Web-app support foroauth=is planned but not yet available.)
Recommended sequence:
authorizer()
on Servicers. rbt dev allows the calls and logs a 60-second
warning naming every unauthorized method — that warning is your
TODO list. Do not paper this over with allow(); allow()
means "public, unauthenticated internet endpoint" and survives
into production.rbt serve / Reboot Cloud: install a TokenVerifier,
then add allow_if(...) rules to every Servicer that should be
externally reachable. See
python/references/servicer-authorizer.md,
python/references/auth-allow-if.md, and
python/references/auth-built-in-predicates.md.allow(). That's the one legitimate use.python FirstBefore scaffolding, load the references that cover the backend mechanics. The patterns in this skill assume you've read them.
Always relevant:
python/references/patterns-common-gotchas.md — recurring trips
(self.ref().state_id, kwargs convention, --name vs.
--application-name, etc.).python/references/api-pydantic.md — pydantic API rules (every
Field needs a zero-value default; non-Optional Model-typed
fields can't take defaults).Defining the API:
python/references/api-methods.md — factory → context type
mapping (Reader/Writer/Transaction/Workflow).python/references/api-errors.md — typed errors.python/references/state-collections.md — always read when
the app has any "list of X" concept. Decides whether each X
should be its own state Type (most of the time, yes) and picks
between in-state list[Sub], in-state list[str] of foreign
IDs, or an OrderedMap of foreign IDs. The trap is
defaulting to list[Todo]/list[Document]/etc. on one parent
for entity collections — see Step 1 of that reference.python/references/state-nested-models.md — the same rule from
the nested-Model angle.Implementing Servicers:
python/references/servicer-{reader,writer,transaction,constructor,authorizer}.md — one per context type.python/references/rpc-refs.md — self.ref().state_id (never
self.state_id); self.ref().schedule(...).python/references/rpc-calls.md — kwargs not Request wrappers.python/references/rpc-constructor-calls.md —
Service.create(context, id) semantics.Workflows:
python/references/servicer-workflow.md — the single,
comprehensive workflow reference. Read it top to bottom: the
@classmethod / WorkflowContext declaration shape, the
call-classification decision tree (Reboot scopes vs.
at_least_once vs. at_most_once), context.loop, inline state
writes,
until / until_changes, and workflow exit semantics.Project shell:
python/references/lifecycle-{project-setup,rbtrc,application-entry,initialize-hook}.md — the canonical layout,
the CLI flags, the Application(...) constructor, the
initialize hook.Auth (browser users — see "Auth in Web Apps" below for the dev-vs-prod sequence):
python/references/servicer-authorizer.md — start here.
Explains the token_verifier= vs. oauth= distinction and when
to defer writing authorizer() vs. write rules from day one.python/references/auth-allow-if.md,
python/references/auth-built-in-predicates.md,
python/references/auth-custom-predicates.md — the predicate
machinery once you're ready to write rules.python/references/auth-allow-deny.md — narrow uses of
unconditional rules; specifically, when not to reach for
allow().Always plan the design and get approval before writing code. The state model is the foundation — getting entities, field types, or method types wrong means regenerating everything across the project.
User type for owned data and route through it.For updates to existing apps, still plan: read current state, propose changes, confirm, then modify.
The plan is read by a human who has not read the skill files. They are evaluating the design — entities, collections, methods, routes, auth — not verifying that you followed the skill. Write so the plan stands on its own.
Don't quote skill-internal terms when presenting the plan. They mean nothing outside this skill:
Shape A / Shape B / Shape C — name the actual data
structure: list[Sub] of inline sub-records, list[str] of
foreign state IDs, OrderedMap of foreign state IDs.Model" — say "a flat sub-record that lives and
dies with the parent" or "no identity of its own", in domain
terms.state-collections.md / api-pydantic.md —
drop the citation; if the rule matters to the design, explain
it inline.factory=True, Field(tag=N), raw pydantic spellings — fine
to mention briefly when the spelling itself is the design
decision, but never as the explanation.For every design choice, give the what + the why. The what is the concrete data structure, method type, or route. The why is a one-clause reason rooted in the user's domain ("grows without bound, so we need pagination"; "no methods or auth of its own, so it lives inline"; "logged-in users only, because the document is per-account").
Examples.
Collection shape — BAD:
documents_index_id: str— ID of an OrderedMap actor that holds this user's Documents (Shape C from state-collections.md — unbounded).
Collection shape — GOOD:
documents_index_id: str— points to an OrderedMap that holds this user's Documents. An OrderedMap (rather than an inline list) because the document collection grows without bound and the dashboard will paginate / sort by recency.
Nested model — BAD:
Comment and Revision are non-state Models — Shape A.
Nested model — GOOD:
Comment and Revision live inline on Document as
list[Comment]/list[Revision]. They don't get their own state actors because they have no lifecycle, methods, or auth independent of the Document they belong to.
Escape hatch. When the precise type name is what the user
needs to see ("I'm proposing OrderedMap here, not list[str]"),
name the type — but pair it with the plain-English reason in the
same sentence. The rule is "no bare jargon", not "no technical
terms".
Before writing code, analyze the user's request:
Type with its own state, even
when "each user only has a few of them". Anything you can
imagine being add-ed / remove-d / find-ed by name has its
own identity and belongs in its own actor. The default wrong
move is packing everything into one parent's state as
list[Todo] (or list[Document], list[Post], …) — that
flattens N actors into one, prevents per-entity auth/methods,
and forces a full rewrite when the collection grows. See
python/references/state-collections.md Step 1 for the full
decomposition signal list.User type and route
creation through it the same way chat-app does — the
User-front-door pattern is independent of MCP. If the app is
anonymous or all users share state, skip User.Type, parents store references, not objects. Three
shapes (full table + worked example in
python/references/state-collections.md):
list[Sub] of non-state Models — bounded sub-records with
no identity of their own (line items on an Order, tags on a
Post). NOT for entity collections.list[str] of foreign state IDs — bounded entity collection
(low hundreds, occasionally low thousands) you always read
whole.OrderedMap of foreign state IDs — collection grows without
bound, needs pagination / range queries / ordered iteration.
The default choice for any "list of things the user keeps
adding to".Field(tag=N). Nested Model
sub-objects owned 1:1 by a parent state must be
Optional[X] = Field(tag=N, default=None) and hydrated in the
parent's factory create Writer; non-Optional Model-typed
fields reject default= / default_factory=. Full rules in
python/references/api-pydantic.md.Reader — read-only queries.Writer — single-state mutations.Transaction — multi-state atomic operations.Workflow — long-running control flows with loops, scheduling,
and idempotency helpers.rbt generate --react=...
wrap the calls.python/references/auth-*.md.<project-root>/
├── .python-version
├── .rbtrc
├── pyproject.toml
├── api/
│ └── <pkg>/v1/
│ └── <name>.py # API definition (pydantic)
├── backend/
│ └── src/
│ ├── main.py # Application entrypoint
│ └── servicers/
│ └── <name>.py # Servicer implementation
└── web/
├── package.json
├── tsconfig.json
├── tsconfig.app.json
├── tsconfig.node.json
├── vite.config.ts # Stock Vite SPA config
├── index.html
└── src/
├── main.tsx # RebootClientProvider entry
├── App.tsx # Routes + top-level component
├── pages/
│ └── <page>.tsx
└── api/ # Generated TypeScript client
# (output of `rbt generate --react=`)
Key differences from a chat-app layout:
web/index.html lives at the top of web/ (single SPA entry),
not under web/ui/<name>/index.html.vite.config.ts is the stock Vite config — no nested-output
override, no viteSingleFile plugin. There's no MCP host
resolving artifacts by path.mcp_servers.json. No MCPJam inspector.Only execute after plan approval. All commands run from the application directory.
.python-version, pyproject.toml, .rbtrc — same
shape as in
python/references/lifecycle-{project-setup,rbtrc}.md. In
.rbtrc, point the React codegen at web/src/api:
generate --react=web/src/api
generate --web=web/src/api
uv sync.api/<pkg>/v1/<name>.py). Pydantic
rules live in python/references/api-pydantic.md; method
marker → context-type rules in
python/references/api-methods.md. Do not add mcp=Tool()
or UI() — those are chat-app only.uv run rbt generate.backend/src/servicers/<name>.py) —
context-type patterns in python/references/servicer-*.md.main.py — python/references/lifecycle-application-entry.md.web/ with your preferred tool
(e.g. npm create vite@latest web -- --template react-ts) or
a Reboot-provided template if one exists for plain web apps.cd web && npm install and add the Reboot React client
package(s) per your project's package.json.uv run rbt generate again — the React bindings need
node_modules to resolve types correctly.main.tsx with RebootClientProvider, then build App.tsx
and the page components, calling generated use<Type>() hooks
for reader subscriptions and mutations. Field-name conversion is
Python-snake → TypeScript-camel; request/response types are
Zod-validated. A reader hook returns both isLoading and
response: use isLoading (the stream's connection state) for
loading/disconnected indicators (!isLoading, debounced, is a
connected/disconnected badge) and response !== undefined to
guard data access (it's also the only one that narrows
response's T | undefined type). They diverge: an aborted
reader is !isLoading with no response; a reconnect is
isLoading with stale response. Transport disconnects
auto-reconnect and do not surface via aborted, so don't
reach for aborted or a heartbeat for an online/offline badge.cd web && npm run build (sanity check the bundle).backend/tests/<servicer>_test.py, following the patterns
in python/references/testing-project-setup.md,
python/references/testing-harness.md, and
python/references/testing-external-context.md. Use one
IsolatedAsyncioTestCase, one external context per test
(name=f"test-{self.id()}"), and
Service.ref(id).method(context, ...) for all calls —
never instantiate Servicers directly. If any servicer has a
real authorizer(), use the permissive-subclass pattern
from testing-harness.md. Run cd backend && uv run pytest
and fix anything that fails. Do not proceed to the next
step until every user-story test passes — these tests are
the gate that catches contract bugs before the user opens
the browser.run skill and
follow it. It is the single canonical "start the app"
procedure: it makes sure dependencies and secrets are in
place, starts the backend and frontend dev server, waits for
them to come up, and hands the user the URLs plus a first page
to open.When modifying an existing app:
.rbtrc, the API definition, servicer, main.py, and
web/src/App.tsx.uv run rbt generate.run skill. If it is already running under
rbt dev run, the --watch globs reload it automatically — no
restart needed. Editing .env likewise triggers a restart, so
a new or changed secret is re-read by --env-file without a
manual relaunch.Specific patterns and file shapes live in the python skill's
references and the table above — read them on demand based on
what's changing.
tools
Run an existing Reboot application locally. Detects whether the project is an MCP Chat App or a standalone Web App, makes sure dependencies and secrets are in place, then starts every process the app needs — the backend (`rbt dev run`) and the frontend dev server (for Chat Apps it also opens the setup wizard, from which the user can launch MCPJam on demand). Use this to bring an app back up, e.g. at the start of a new session.
development
Reboot Python framework for building transactional microservices with durable actor state. APIs are defined in pydantic Python (`reboot.api`). Use this skill when writing Python code for a Reboot application, defining APIs with reader/writer/transaction/workflow methods, implementing Servicers, calling actor refs across services, scheduling work, building durable workflows with the right call primitive (`.per_workflow(alias)` / `.per_iteration(alias)` / `.always()` for Reboot calls; `at_least_once` / `at_most_once` for external calls; `until` / `until_changes` for reactive waiting on Reboot state), calling an LLM / building an AI agent in the backend via the durable `reboot.agents.pydantic_ai.Agent`, or testing Reboot applications with the `Reboot()` test harness.
tools
Build complete Reboot AI Chat Apps (MCP Apps) for ChatGPT, Claude, VSCode, Goose, and other MCP hosts. Layers on top of the python skill for backend mechanics; covers what's specific to MCP Chat Apps — the User-type front door, MCP tool exposure, the UI() method type, and the full React/Vite scaffolding.
tools
Build a Reboot application from a user description. Routes to the chat-app skill (MCP Chat Apps for ChatGPT, Claude, VSCode, Goose) or the web-app skill (standalone web apps with a browser frontend). Commits to a route only when the prompt verbatim names the front-door (MCP/Claude/ChatGPT for chat-app; a URL/SPA/"website" for web-app); otherwise asks the user. Does NOT infer the front-door from the app's domain (CRM, todo, dashboard, blog, …) — those describe what the app does, not where it lives.