The blueprint
One JSON document per space holds everything a person can see or change. It holds settings and shapes, never live data: a collection's fields are in the blueprint, its records are in the database. Type and checks: convex/lib/blueprint.ts.
Fields #
| Field | What it is | Page |
|---|---|---|
identity | { name, about? }. name is required. | |
access | { visibility, join }: who can find the space and how people join | People |
words | Every word the shell shows, by key | Words |
look | { style, scheme, changes? }: the style in use, the scheme, small changes on top | Styles |
styles | The space's own styles | Styles |
dashboard | { panes, nav? }: which panes show, in order, and how (tabs or list) | Dashboard |
pages | [{ id, title, icon?, blocks, look?, visibleTo? }] | Pages |
apps | Mini-apps: [{ id, name, html, needs?, about?, icon?, age?, config?, from?, remixOf? }]; needs lists the bridge calls it uses, age is everyone, teen or mature, from / remixOf say which library listing it came from | Mini-apps |
collections | Shapes of the space's data | Data |
roles | [{ id, name, can }]: what each member may do | People |
streams | Conversations the space defines: rooms, channels, queues, with who sees, posts and answers | Engines |
talk | Rules for chats people start themselves: direct chats, groups, receipts, typing, edits, send later; screen names the page used as the full-screen chat | Engines |
structure | Unit kinds and units (teams, classes, branches) as a tree | Engines |
feeds | Feeds people post in: replies, media, reactions, reposts, expiry, approval, outside sources | docs/SOCIAL.md |
games | Game definitions for the Rounds engine: mechanic, items, decks, teams, timer, scoring | Engines |
ledger | Units (coins, points), levels (tiers, VIP, memberships) and badges | Engines |
books | Photo books: [{ id, name, feed?, tag?, from?, to?, shape?, cover?, fill?, pages?, end?, samples? }]; pages use 8 layouts (full, framed, two, stack, trio, grid, words, chapter) and photo refs "<post id>" or "<post id>#<n>" | Photo books |
reviews | Review boards by id: { name, scale?, icon?, subjects?, subjectsFrom?, aspects?, writers?, verified?, managers?, text?, minLength?, maxLength?, titles?, photos?, approval?, replyRoles?, helpful?, reports?, hideAfter?, perDay?, links?, edit? } | Engines |
referrals | Personal invite codes, rewards, qualify, limit and milestones | People |
notifications | { topics?, defaults?, digestAt?, kidsQuiet?, inboxPage? }: the space's own topics and default modes | Notifications |
safety | Word filter (words, presets, links, contacts, action), report reasons, escalate, modRoles, autoHide, notify, who | docs/SAFETY.md |
web | { handle, domains?, home?, routes?, card?, data?, index?, embed?, appLink?, nav? }: which pages open in a browser, at what address | docs/WEB.md |
bots, automations | AI teammates (role, instructions, tools, approval) and rules (when, if, do) | AI |
connections | Outside MCP servers and HTTPS APIs the AI may use; keys stay in the vault | AI |
ai | { model?, stream?, remember?, notify?, maxTurns?, limits? }: model size (auto, small, large), streaming, AI notes, a push when a long build finishes, turns per job, and the space's own AI limits | AI jobs, AI limits |
audience | "everyone" (default) or "kids" | People |
kits | Kits this space added, and what each added | Kits |
fromLibrary | Templates and kits added from the library, and what each added, so they can be taken out | Templates |
New fields arrive with new engines. Old blueprints keep opening: upgradeBlueprint fills in anything missing with its default.
A new space starts from starterBlueprint(style): the HQ page, a Home page with a hello, the default roles, private access, and the style the person picked.
Ids #
- Block ids are unique across the whole space, not just their page. Use short kebab-case (
hero,links-ig). - Collection, field, stream and role ids: lowercase letters, digits and dashes, starting with a letter (
^[a-z][a-z0-9-]{0,39}$, roles up to 32). - A kit's ids start with the kit's id (
tasks-board,forms-responses) so kits never clash.
Pages #
{ "id": "menu", "title": "Menu", "blocks": [ … ], "look": { "colors": { "accent": "#c2410c" } }, "visibleTo": ["member"] }
blocksis a tree: a block withchildrenholds other blocks (Blocks).lookis a style layer for this page only (Styles).visibleTolists role ids (orlevel:<id>,level:<id>+,badge:<id>); unset means everyone who can see the space (People).- Every page opens at
/p/<page-id>with no new code; a button withhref: "#menu"goes there. - The
hqpage is core: it can be rearranged and added to, never removed.
Dashboard #
"dashboard": { "panes": [{ "kind": "page", "page": "hq" }, { "kind": "page", "page": "home" }, { "kind": "chat" }] }
A pane is a page or the AI chat. Phones show one pane at a time with tabs; screens 900 px and wider show them side by side. People who can't manage don't see the chat pane or pages hidden from their role.
Panes are the app's navigation. With nav: "tabs" (the default), on phones the first five are native tabs and the rest are top-bar buttons; with nav: "list" there's no tab bar: home is the chat list and panes are rows in it (the chats block's rowsTop and rowsUnder), switched in HQ → Navigation; each page's icon (a name from src/icons/icons.json) is its tab icon. Wide screens get a sidebar, or this side-by-side dashboard when a space has three panes or fewer.
Photo books #
Full detail in docs/PHOTOBOOK.md. A book points at photos in a feed (the Gallery by default) and never copies them, so a deleted photo simply drops out. tag makes it an album, while from and to (YYYY-MM-DD) pick a date range. Without pages, a book lays itself out from its photos (fill: order, photos per page, chapters by month, day or tag, captions, max) and grows with them. The same photos always make the same pages. Hand-made pages are { id, layout, photos?, title?, text?, date?, tint? }, where tint is a style colour name and never a hex value. Limits are 24 books, 200 pages a book and 1,200 placed photos in all. Words are words["book.<key>"].
{ "op": "add", "path": "/books/-", "value": { "id": "italy", "name": "Italy", "tag": "italy", "fill": { "chapters": "day" } } }
Words #
Every word the shell shows is words.<key>; a missing key falls back to a built-in default. Keys today: chatTitle, chatPlaceholder, approve, reject, undo, back. Blocks carry their own words in their props (label, empty, submit, done). A new screen that needs a word adds a key here, never a string in code.
Changes #
Every change is a list of JSON Patch ops:
| Op | Example |
|---|---|
add | { "op": "add", "path": "/pages/1/blocks/-", "value": { "id": "hi", "kind": "text", "props": { "text": "Hi" } } } |
replace | { "op": "replace", "path": "/look/style", "value": "futuristic" } |
remove | { "op": "remove", "path": "/pages/2" } |
- Paths are JSON Pointers.
-adds at the end of a list.~1is a/inside a key,~0a~. addon an object key sets it;addon a list index inserts there.- Replacing the whole blueprint (path
"") is refused. - Ops apply in order to a copy; if any fails or the result fails a check, nothing changes.
Keep a change small and about one thing, with a one-line summary people read in History.
Checks #
checkBlueprint runs on every change, from anyone. It refuses, with a readable message:
- unknown block kinds, missing required props, props of the wrong type, children on a block that can't hold them, duplicate ids;
- a missing
hqpage, a pane pointing at a page that doesn't exist; - a
miniappblock pointing at no app, astreamblock at no stream, arecords.*block at no collection or field, agroupBythat isn't a choice field, a poll or wheel with fewer than two options; - bad collections, roles, streams, access, styles or looks (see each page);
- a kit id that isn't in the library.
The AI gets the same messages and fixes its proposal itself.
Versions and undo #
- Every applied change bumps
versionand stores the whole blueprint inversionswith its summary and who made it (personorai). - Undo is
restore(version): the old blueprint is saved as a new version, so undo can be undone and nothing is ever lost. - AI changes wait in
proposalsaspendinguntil a person approves (or discards) them.
Three ways in #
- HQ: system blocks (
hq.*) that edit parts of the blueprint. - The AI: proposes ops; a person approves.
- The API:
blueprint.changewith ops, under the same permission (manage). See API.