This is a workshop for the small applications a team actually runs on — a form to collect requests, a shared register, a stock list, a dashboard. You say what you want in your own words to an AI assistant, and it builds the whole thing: the screens people use, the database behind them, and who is allowed to see what.
Then it stays up. Each app gets an address of its own the moment it is built — nothing to install, no server to rent, no deploying — and everyone reaches all of them with one account.
Need a change? Ask for it the same way. The assistant can read the app's own errors, so it can usually fix what it got wrong without anyone going to look.
The master is a control plane. It holds the registry and the user pool, answers MCP calls, and supervises the fleet — but it never sits in an app's request path.
Ten tools over one MCP endpoint at /mcp.
upsert_app
Create or update an app in one call — its React sources, its server-side hooks and its database schema together.
get_app
Read an app back: its sources and the schema it declared, in the same shape
upsert_app takes.
list_apps
Every app with its URL, state and port.
get_logs
Browser console, server logs, process output and lifecycle events for one app — enough to diagnose a page without opening it.
restore_version
Puts an app back to a version it held earlier — the sources it stored and the schema they were written against. It rolls forward: what the old version held becomes the next one, so a restore can itself be restored. The schema becomes exactly what that version declared, so it asks before dropping anything and names what would go.
start_app · stop_app
Lifecycle. Data is untouched by a stop.
secrets
An app's vault: the credentials its server-side code needs and its source must not
carry. Write only — a secret goes in and is never read back out, not by this tool
and not by any other. Hooks read it as $secrets.get("NAME"); nothing
reaches the browser, and nothing reaches across apps.
rename_app
Moves an app to a new slug — its subdomain, its directory and the name every other tool calls it by. The database, the owners, the stored versions and the history come along; every link to the old address stops resolving, so it asks.
archive_app
Stops an app and sets it aside. The directory is renamed rather than removed and the slug is freed, so the data survives and the name can be reused.
delete_app
Removes the process, the registry entry and the whole directory. Irreversible, so it asks.
owners
Who may reach an app, at one of two levels: owner may change it, user may only open it. Whoever creates one owns it; owners bring in whoever else they need. Factory administrators reach every app without being listed.
branding
One look for every app at once: colours, type, shape and a logo. Setting the factory's own is a factory administrator's call; an app's is its builder's.
Interface, schema and backend logic arrive together. Sources are TSX — the page, the bundle and the stylesheet are generated, never sent.
// upsert_app
{
"slug": "todo",
"sources": [
{ "path": "index.tsx", "content": "import { BrandProvider, BrandShell } from '@pf/brand'…" },
{ "path": "TaskList.tsx", "content": "export function TaskList() {…}" }
],
"hooks": [{ "path": "main.pb.js", "content": "routerAdd(…)" }],
"schema": [{
"name": "tasks",
"access": "owner",
"fields": [
{ "name": "title", "type": "text", "required": true },
{ "name": "done", "type": "bool" }
]
}]
}
A brand is an intention, not a stylesheet. One colour becomes the ten shades Mantine wants, derived on the same curve as its own palettes; everything else is chosen from a closed vocabulary. There is no CSS to write and no theme object to assemble.
// branding
{ "action": "apply",
"brand": { "name": "Acme", "palette": { "primary": "#1e3a5f" } } }
That reaches every app immediately. Colours, type, radius and density arrive through a
stylesheet each app serves — no rebuild, no restart, including apps built before the
brand existed. The logo, the header and the component defaults come from
@pf/brand, which an app imports, so they arrive with its next build. The
sign-in page and this one wear it too.
An app sends .tsx, .ts and .css, and imports from
a fixed set. esbuild runs inside the master, against dependencies compiled into the
binary — there is no npm and no network at build time, in the image or anywhere else.
react
19.2. JSX is automatic, so an app never imports React itself — and there is exactly one React on the page, whatever it pulls in.
@mantine/core · hooks
9.6. The component set, and the hooks that go with it. The stylesheet is already linked, so an app never has to ship CSS to look finished.
@mantine/form
useForm, for anything with inputs.
@mantine/dates · dayjs
Date inputs and calendars, with every dayjs plugin and the en and fr locales.
@mantine/charts
Charts, on recharts underneath — which is not importable on its own, so an app cannot end up with two charting libraries.
@mantine/notifications
Toasts.
@mantine/nprogress
The loading bar across the top of the page.
wouter
Routing in a few KiB: Route, Link, Switch,
useLocation.
@phosphor-icons/react
1512 icons, of which an app pays only for the ones it draws.
@pf/brand
Not vendored but generated, per build, from this factory's brand: the theme, the logo, and the shell an app is built around.
Most of that set is served prebuilt and imported at runtime. The icon set is not, and the difference is what an app actually uses of what it imports.
The properties that fall out of building it this way.
An app is a directory. Copy it elsewhere, run the binary against it, and it serves — with the master switched off.
Apps ship their own PocketBase JS hooks, so they can expose real endpoints and react to their own data.
Accounts live on the master — password, Google, Entra or any OIDC. Apps inherit it without a line of change, and every front is behind that sign-in.
Every page's console, uncaught errors and failed requests are captured
automatically and land in the app's own log, ready for get_logs.
Nothing is written until the compile succeeds, so a broken edit leaves the app serving its last working version — and the caller gets file, line and column.
Each change writes a migration snapshot into the app's
pb_migrations. Diffable, replayable, worth committing.
Nothing of an app reaches an anonymous browser unless it asks to be public. Then the gate steps aside and the app's own access rules decide what a stranger sees — and its header offers a way in rather than demanding one.
Whoever creates an app owns it, and decides who else may open or change it. An app nobody shared with you does not appear in your listing, does not open in your browser, and answers none of your calls — factory administrators excepted.
Apps read their colours and type from a stylesheet the factory writes, so a rebrand reaches every one of them at once — no rebuild, and nothing to remember for the app built next week.
Separate process, separate database, separate origin. One app cannot read another's data or its storage.