# Zvessa
> Zvessa is an AI-first app where anyone builds a space (pages, chats, games, shops, journals, team spaces, anything) by describing it. The space is one JSON document, the blueprint, made of blocks, styles, data collections, optional kits and sandboxed mini-apps. The AI proposes changes as JSON Patch, a person approves, and every version can be undone. Outside AIs build through its MCP server with the same tools.
Each page below is also available as Markdown: swap .html for .md in its address. llms-full.txt has every page in one file.
# Getting started
Zvessa is a place you build by talking. You describe what you want, the AI makes it, and you
approve it. There's no builder to learn. You can make a business page, a team space, a family
group chat, a game night, a journal or a photo book.
## Sign up
1. Open the app (iPhone, Android or the web) and sign up with your email and a password.
2. Pick **how you want to start**:
- **Business starter**: a messaging app for your business, with chats, calls, party games, a
services page, an apps page and an HQ with numbers.
- **Personal starter**: a soft home for you, with chats, calls, plans, a private journal and photos.
- **Feeds starter**: your own social app with posts, photos, stories and profiles.
- **Journal starter**: a private journal with moods, prompts, streaks and a calendar.
- **Photo book starter**: photo books that fill themselves from your photos.
- **Build my own**: say what the space is for (just me, a business, a team, family, kids, a
creator, a community, games) and pick the features you want.
3. You're in. A **Get ready** card on your home ticks off the first steps (connect your AI, invite
someone, try a change) and then goes away. Everything can be changed and undone.
## What you see
Every space has three parts:
| Part | What it's for |
| --- | --- |
| **HQ** | Your control room: Overview, Apps, People and Settings. |
| **Your pages** | What people see: a home page, chats, games, anything you build. |
| **The AI** | Where you build. Tell it what you want. |
On a phone these are tabs or rows in a chat list. On a big screen they sit side by side.
## Your first change
Open the AI and type something like:
> Make my home page a link-in-bio with my name, a short line and buttons for Instagram and my shop.
The AI shows a short summary of what it wants to change. Tap **Approve**, and it's done. Nothing
changes until you approve, and **HQ → Settings → History** can always take you back.
Next: Building by chat.
# Building by chat
The AI can do everything HQ can do. It **proposes** a change, you **approve** it, and every
change is saved as a version you can undo. Turn on **Auto** and its changes apply right away.
## How to ask
- **Say what it's for, not how to build it.** "A page where my class signs up for a time slot"
works better than a list of buttons.
- **Ask for one thing at a time.**
- **Describe the look in words.** For example, "warm, paper, serif" or "dark and neon". The AI turns that into a style.
- **If a proposal is wrong, say what's wrong.** The AI changes it, and Undo is always there.
- **Paste a link** if you've added the Web research feature. The AI can read pages and search the web.
## Things to try
| Ask | You get |
| --- | --- |
| "Add a booking page: weekdays 9 to 5, 30-minute slots" | A page where people book a time with you |
| "Make a trivia night about 90s movies with teams" | A live game people join with a code on their phones |
| "Add a channel for announcements only staff can post in" | A broadcast channel |
| "Make a VIP page only Gold members can see" | A members-only page |
| "Give everyone 10 coins when they join and a shop to spend them" | Coins and a shop |
| "Make a habit tracker with streaks" | A list with streaks and a calendar |
| "Put the scoreboard on the TV" | A TV link that shows the page full screen |
| "Make a games page like a real game app" | A game lobby with posters |
| "Add a countdown to our trip on June 1st" | A ready-made mini-app, set up for you |
| "Make it look like a dark arcade" | A new style |
## While it works
The reply appears as the AI writes it, with a line underneath saying
what it's doing. **Stop** takes the place of Send. A message you send meanwhile waits its turn, and
a long build keeps going if you leave, with a notification when it's done. In HQ → Settings → AI
pick **Fast**, **Careful** or Auto. Add the **AI helper**
feature and the AI keeps short notes on how you like things built, which you can read and delete.
## Bots and automations
Add the **Bots** feature for a Bots page. Ask the AI for "a helper
bot people can @mention" or "every Monday at 9, post the week's open tasks in General". Until you
let a bot act on its own, everything it would change waits on the Bots page for you to approve,
and you can change its words first. A bot's role decides what it may do, like a person's, and bots
never approve anything. On a help desk, a bot can draft answers that your teammate sends. Outside
services can start an automation through a webhook address.
## Approve, discard, undo
- **Approve** applies the change. **Discard** throws it away.
- **Auto** (HQ → Settings → AI, owner only): the AI's changes apply at once and the card says
**Applied automatically** with **Undo**. Changes to who can come in, roles, connections, bots,
automations, web pages and AI limits still wait for you. Kids spaces always ask.
- Say **"undo that"** and the AI takes its last change back.
- **HQ → Settings → History** lists every version. **Undo to here** goes back; you can undo the undo.
- You can also edit any page directly: tap **Edit** on the page (Editing pages).
## Limits
- The AI runs on your own Claude key. Paste it in **Connect your AI** at the top of HQ (or on the
Connections page). Until it's connected, the AI chat asks the owner to connect one.
- A space can set its own limits per person or per role, and a message above the box says when
you'll get more.
- In a kids space, the AI keeps every word, page and game kid-safe.
# Editing pages and HQ
## Edit a page in place
People who manage the space see an **Edit** button on every page. It turns the page into its own editor:
- **Rename** the page in the title.
- Every part sits in a frame where you can move it up or down, open its **settings**, **Duplicate** it or **Remove** it.
- Tap a header, tile, link or service to change it, or tap **+** to add one.
- Settings open as a sheet: words, numbers, switches, lists, icons, colours, links, choices, and
**Who sees it** (pick roles).
- **Add to page** offers headings, text, services, links, app tiles, a header, a picture, a
button, a card, a number, a list, a poll, a wheel, a call button and a line.
- **Save** saves the whole page as one version, and **Cancel** asks before throwing changes away.
## HQ, part by part
HQ has its own tabs: **Overview**, **Apps**, **People** and **Settings**.
| In HQ | Does |
| --- | --- |
| **Overview** | Proposals waiting for you, reports, and numbers over 7, 14 or 30 days |
| **Apps** | Everything the space has, sorted into sections; anything you turn on shows up here |
| **People** | Members, roles, invite links, join requests, **Who can come in** |
| **Settings → Look** | Styles (Classic, Futuristic, Tech or your own), light or dark |
| **Settings → Navigation** | A chat list as home, or tabs at the bottom; page order |
| **Settings → Pages, Starters** | Every page; switch the whole space to a starter (undoable) |
| **Settings → AI** | Your Claude key, Fast or Careful, and **Ask** or **Auto** approval |
| **Settings → Connections, Library** | Keys, outside services, tokens; add or take out features |
| **Settings → Safety, History** | Reports and blocked words; every version, with undo |
Every part of HQ is a block too, so you can rearrange it like any page.
# Features (kits)
Every feature is optional. Nothing is on until you add it, and taking one out is one tap that
can be undone. Your data stays: if you add a feature back, its records come back with it.
Add features from **HQ → Settings → Library**, or ask the AI ("add a support desk").
## What you can add
| Group | Features |
| --- | --- |
| **Talk** | Group chat, channels, direct chat, a team space like Slack, a support desk, posts and threads, a photo feed, stories, a forum |
| **People** | Contacts, teams and hierarchy (classes, squads, branches), tiers, VIP, verified badges, profiles and followers |
| **Money** | A catalog of products or services, coins, memberships. Card payments are coming. |
| **Play** | Polls, a wheel, Hot Potato, High Card, leaderboards, points, quests and achievements, puzzles |
| **Party** | Trivia night, a quiz board, a survey showdown, charades, a party pack, and a game maker where the AI builds any game |
| **Plan** | Group plans (who's coming, who brings what, shared costs), Secret Santa, a journal |
| **Live** | Calls and group calls, live streams, show on TV |
| **Time** | Appointments, and posts scheduled for later |
| **Work** | Boards and tasks, drafts and approvals |
| **Grow** | Analytics, reviews, a guestbook, outside feeds (X, Bluesky, Mastodon, Instagram, Threads, YouTube, RSS) |
| **Build** | Forms, connections, web research, a media library, a gallery, photo books |
| **Tools** | Over 20 mini-apps (a timer, dice, flashcards, weather, a QR code maker and more) and the public app library |
| **Safety** | Moderation (report, block, a review queue and a word filter), a kids space |
The full, current list with what's ready is in the catalog.
## Changing a feature
After you add a feature, what it made is yours to rename, restyle, move or hide from some roles.
"Support desk" is just a queue with a team behind it, and "VIP" is just a rank, so you can turn
them into anything.
## Search
Add the **Search** kit, or ask the AI to put a search box on any page (coming with the next
update). It looks through pages, lists, messages, posts, people and files, and you only find what
you're allowed to see.
## Put your pages on the web
Add **Public links**, pick a handle, and your pages open in any
browser, without the app. That's good for a link-in-bio, a portfolio, a menu or a business page.
You can add your own domain too: HQ's Web panel lists the DNS records to add, including a TXT
record `_zvessa.` with the value `zvessa-verify=…` that proves the domain is yours.
Only public pages go on the web, and kids spaces never do.
Ask for a QR code anywhere: "put a round QR code for our web page on the home page, in our
colours, with our logo". People can share it, save it to Photos or print it.
## Share what you made
Add **Templates and kits** to share your work. Save your whole
space as a template, or a few pages as a kit. It stays private to you, and you can add it to your
other spaces. Before anything is saved, keys, emails, phone numbers and private links are taken
out, and you see the list. When you tap **Publish**, a Zvessa admin checks it first, then anyone
can add it next to what they have, or switch their space to a template. Everything added can be
undone from History or taken out again. You can also ask the AI: "save my party page as a kit".
# People, roles and privacy
## Who can see your space
| Setting | Who can look |
| --- | --- |
| **Private** | Members only (new spaces start here) |
| **Link** | Anyone with the link |
| **Friends** | Your friends |
| **Public** | Anyone, and it's listed in Explore |
And how people join: **invite only**, **ask to join** (you approve and pick their role), or **open**.
## Roles
You name the roles (coach, player, parent, staff, VIP). Each role gets some of seven permissions:
view, post, play, add records, edit anyone's records, invite, and manage the space. The owner
always has everything.
## Hiding pages and parts
Any page or part can be shown to some roles only, or to people with a level or badge ("Gold and
up", "verified"). Hidden things never reach other people's phones, and neither does the data
behind them.
## Invites
Make an invite link with a role, limit how many times it can be used, or revoke it.
Add **Invites and referrals**, and everyone gets their own invite code
and QR to share. Ask the AI to style the QR: round like Apple's, dotted, in your colours, with your
letter or logo in the middle and a caption under it. It always stays scannable. You can reward people who bring friends with coins (never real money). Rewards
wait a day after someone joins, and by default the coins can be spent in the space but not sent to
someone else, so nobody can farm them with fake accounts.
## Report and block
**Report** and **Block** are in every post menu, people list and
profile. A report goes to the space's moderators, and serious ones to Zvessa too. Blocking someone
works everywhere: neither of you sees the other's posts or can message, call or follow the other.
Add **Moderation** for a Safety page with the review queue and a word filter. The AI can show you
the queue, but only a person decides what happens.
## Kids spaces
A kids space stays private and invite-only. Kids can only message, friend or call people who
share a kids space with them, and only the owner can open the space up. The AI keeps everything kid-safe.
# Games, calls and TV
## Party games
Add a party feature (Trivia night, Quiz board, Survey showdown, Charades, Party pack) or ask the
AI to make any game. People join with a code on their phones, and visitors can too. The server decides
every random pick, timer and score, so everyone sees the same result and nobody can cheat.
Ask for "a games page like a real game app" and you get a lobby with bright posters, a featured
carousel, games you can join now, recent winners and your own record.
## Show on TV
Add **Show on TV**. On the TV (or any browser) open `/tv`, and a code appears. Type that code into
the TV block in your space, and the TV shows a page live. You can switch what each TV shows from
your phone. The big screen never shows secret answers.
## Calls and live streams
Add **Calls** for one-to-one and group calls with video and screen share, or **Live streams** for a
stage with hosts and an audience. A call can ring a closed phone.
# Mini-apps and the app library
A mini-app is a small app inside your space, like a calculator, a timer, a drawing board or a game.
- **Starter mini-apps**: HQ → Settings → Library → Tools has over 20 ready ones. Ask the AI to set one up:
"a countdown to our trip on June 1st", or "a scorekeeper for golf where low wins".
- **Your own**: ask the AI to make one ("a snake game in our colours"). It matches your style and
pauses when you scroll away, so it doesn't drain your battery.
- **The public library** (the App library feature): browse apps other people shared, try one
in a preview, then **Add to my space** or **Remix** it. To share one of yours, tap **Publish**.
A Zvessa admin reviews every version first. Kids spaces only see apps rated for everyone.
Mini-apps run in a locked sandbox. They can only reach your space or your phone (location,
camera, sensors) through calls the app declares, and the phone asks you first.
# Connect your AI, keys and services
## Build from Claude, Cursor or any AI
Your space is an MCP server. In **HQ → Settings → Access tokens**, make an access token, then add it to your AI
app. In Claude Code, for example:
```sh
claude mcp add --transport http zvessa https://your-backend/mcp --header "Authorization: Bearer mbk_…"
```
New tokens can read and propose. The outside AI proposes changes, and you approve them in the app.
Only give a token "approve" for an AI you'd let change the space on its own (it can still be undone).
## Ask any AI about Zvessa
Your space's AI already knows the whole app and these docs. Other AIs can too:
- Point any MCP app at `https://site.zvessa.com/mcp/library`. No account or token is needed. It
searches and reads these docs, and browses the public mini-app and template library.
- Or give a chatbot `https://docs.zvessa.com/llms-full.txt`, which has every page in one file.
## Your own keys
Save keys in **Connections**. They're sealed on the server, and nobody can read one back, including
the AI. Each key only goes to the hosts you allow. A Claude key with use "AI" is what your space's AI
runs on. The quickest way to add it is **Connect your AI** at the top of HQ.
## Outside services
Connect another MCP server or any HTTPS API. The AI and mini-apps can then read from it. They can only
read, unless you allow more. Details for developers: Capabilities.
# Your account and notifications
## Notifications
Open **Notifications** from HQ or the bell. For each kind of notification (chats, mentions,
calls, likes, a space's announcements), and for every space or just one, choose:
- **On**: right away.
- **Digest**: one summary a day, at a time you pick.
- **Off**.
Set **quiet hours** (for example 22:00 to 07:00), and anything that arrives then comes as one
summary when they end. Calls still ring unless you hold them too. **Pause** stops everything for 1
hour, 8 hours or a day. Tapping a notification opens the exact chat, post or page. You can also ask
the AI: "only notify me about mentions after 6pm".
## Your account
Open **Spaces**, then **Account**.
- **Change email:** enter your password, then the code sent to the new address. Your old address
gets a notice.
- **Change password:** this signs you out on your other devices.
- **Sign out on other devices.**
- **Forgot your password?** Tap **Forgot password?** on the sign-in screen. You get an 8-digit
code by email, good for 15 minutes.
- **Delete my account:** this removes the spaces you own, with everything in them. The screen
names each one and how many people are in it. Your posts in other people's spaces stay in place
but empty, and everything else of yours is removed. It can't be undone.
# Questions
**Can the AI break my space?** No. It only proposes changes, and you approve them. Every change is a
version you can undo in HQ → Settings → History.
**Does it drain my battery?** It's built not to. Animations, timers, sensors and mini-apps stop
when you can't see them, lists only load what's on screen, and icons are the phone's own.
**Who can see my space?** Only members, until you change it in HQ → People → Who can come in. Hidden pages
never reach other people's phones.
**Can I take a feature out?** Yes, with one tap in HQ → Settings → Library. Your data stays, so adding the feature back brings it back.
**Do I need a Claude key?** Yes. The AI runs on your space's own Claude key. Paste it in **Connect your AI** at the top of HQ, or add it in HQ → Settings → AI.
**Can I build it from Claude Desktop, Claude Code or Cursor?** Yes, through the MCP server (Connect).
**Card payments?** Coming. Coins, levels and memberships work today with the space's own units.
**Is it on iPhone, Android and the web?** Yes, all three, from the same app.
# Building on Zvessa
Everything a person, a developer or their AI needs to build anything on Zvessa, and everything
the system gives them from the first minute. Start here.
**The idea in one line.** A space is one JSON document, the *blueprint*. Pages, blocks, styles,
words, roles, data shapes, mini-apps and kits all live in it. You build by changing it: ask the
AI, tap in HQ, or call the API. Every change is checked, saved as a version and can be undone.
## What you get from the start
Every new space has, with no setup:
| You get | What it is | Page |
| --- | --- | --- |
| A dashboard | HQ, a hello page and the AI chat (tabs on phones, side by side when wide) | [Blueprint](https://docs.zvessa.com/dev/blueprint.html#dashboard) |
| The AI builder | Describe anything; it proposes a change you approve | [AI](https://docs.zvessa.com/dev/ai.html) |
| Versions and undo | Every save is kept; undo is one tap and can itself be undone | [Blueprint](https://docs.zvessa.com/dev/blueprint.html#versions-and-undo) |
| Blocks | Layout, words, media, data, chat, coins, games, bookings, charts, calls, TV | [Blocks](https://docs.zvessa.com/dev/blocks.html) |
| Styles | Classic and Futuristic, plus any style you or the AI write | [Styles](https://docs.zvessa.com/dev/styles.html) |
| People | Roles you name, seven permission verbs, invites, friends, visibility and join rules | [People](https://docs.zvessa.com/dev/people.html) |
| A kit library | Dozens of optional features; none is on until you add it | [Kits](https://docs.zvessa.com/dev/kits.html) |
| Engines | Data, talk, levels, coins, games, calls, media, TV, bookings, counts, apps, connections, kids | [Engines](https://docs.zvessa.com/dev/engines.html) |
| Mini-apps | Any HTML game or tool, sandboxed, matching your style, paused when hidden, with a bridge to the phone and the space, 21 starters and a public library | [Mini-apps](https://docs.zvessa.com/dev/miniapps.html) |
| Device features | Native icons, video, audio, photos, files, push, calls; more as blocks need them | [Device](https://docs.zvessa.com/dev/device.html) |
| Build from anywhere | An MCP server and the same API the app uses, under the same permissions | [API](https://docs.zvessa.com/dev/api.html) |
| Templates | Business, Personal, Feeds, Journal and Photo book starters, or a purpose (team, family, kids…) with kits picked for it | [Templates](https://docs.zvessa.com/dev/templates.html) |
| Safety built in | Permissions, hidden data, kids rules, a key vault, safe outside calls and limits, all on the server | [Security](https://docs.zvessa.com/dev/security.html) |
Calls and live rooms, big videos, push notifications and the key vault run on services the
platform already provides ([docs/LIVE.md](https://docs.zvessa.com/more/live.html), [docs/CAPABILITIES.md](https://docs.zvessa.com/more/capabilities.html)).
## How to read these docs
- **Want to build something?** Read [Quick start](https://docs.zvessa.com/dev/quickstart.html), then the recipe closest to it.
- **Writing a block, kit or engine?** [Blocks](https://docs.zvessa.com/dev/blocks.html#adding-a-block), [Kits](https://docs.zvessa.com/dev/kits.html#writing-a-kit),
[Engines](https://docs.zvessa.com/dev/engines.html).
- **Writing a mini-app or game?** [Mini-apps](https://docs.zvessa.com/dev/miniapps.html), then [Performance](https://docs.zvessa.com/dev/performance.html).
- **Need the exact list of what exists?** [Catalog](https://docs.zvessa.com/reference/catalog.html) is generated from the code.
| Page | What's in it |
| --- | --- |
| [QUICKSTART.md](https://docs.zvessa.com/dev/quickstart.html) | Getting started, your first change three ways, recipes |
| [BLUEPRINT.md](https://docs.zvessa.com/dev/blueprint.html) | Every field of the blueprint, ids, changes (JSON Patch), checks, versions |
| [BLOCKS.md](https://docs.zvessa.com/dev/blocks.html) | Every block, how blocks are drawn, adding a block |
| [STYLES.md](https://docs.zvessa.com/dev/styles.html) | Styles as data, colour formulas, layers, recipes |
| [DATA.md](https://docs.zvessa.com/dev/data.html) | Collections, fields, records, events and analytics |
| [PEOPLE.md](https://docs.zvessa.com/dev/people.html) | Roles, permissions, visibility, invites, hidden pages and blocks |
| [KITS.md](https://docs.zvessa.com/dev/kits.html) | What a kit is, the library, writing a kit |
| [ENGINES.md](https://docs.zvessa.com/dev/engines.html) | The 14 engines underneath every kit, what each stores and does |
| [MINIAPPS.md](https://docs.zvessa.com/dev/miniapps.html) | The sandbox, the style, the bridge and every call, the public library, starter mini-apps |
| [AI.md](https://docs.zvessa.com/dev/ai.html) | The builder AI and its tools, the MCP server, keys and connections, bots |
| [NOTIFY.md](https://docs.zvessa.com/dev/notify.html) | Notifications: topics, each person's choices (on, digest, off, quiet hours, pause), space settings, blocks, tools, `notify()` for kits |
| [ACCOUNT.md](https://docs.zvessa.com/dev/account.html) | Account basics: change email or password, sign out elsewhere, forgot password, delete my account |
| [AI-JOBS.md](https://docs.zvessa.com/dev/ai-jobs.html) | How AI work runs: streaming replies, Stop, background builds, model sizes (`/ai/model`), AI notes, and starting jobs from bots |
| [AI-LIMITS.md](https://docs.zvessa.com/dev/ai-limits.html) | How much AI each person and space may use: server and space limits, `/ai/limits`, what counts, the limit message |
| [DEVICE.md](https://docs.zvessa.com/dev/device.html) | Native features: icons, haptics, sensors, media, push, and their permissions |
| [PERFORMANCE.md](https://docs.zvessa.com/dev/performance.html) | Budgets every block, kit and mini-app keeps: battery, heat, size |
| [API.md](https://docs.zvessa.com/dev/api.html) | Every server function, who may call it, what it returns |
| [TEMPLATES.md](https://docs.zvessa.com/dev/templates.html) | Starting points: the five starters, picker covers, onboarding purposes, making a template |
| [SECURITY.md](https://docs.zvessa.com/dev/security.html) | What the server guards, and its limits |
| [CATALOG.md](https://docs.zvessa.com/reference/catalog.html) | Generated: every engine, block, kit, permission and field type |
## Status words
Each page marks what exists today:
- **Ready**: works in the app now.
- **Being built**: designed and partly in the code, not usable yet.
- **Planned**: decided, not started. The shape here is the plan and may change.
## The rules that make it work
These hold for anything you build (from CLAUDE.md):
1. **Abstract, never one use case.** A `stat` is a number with a label, not a "streak counter".
2. **Every word and every look is a setting** in the blueprint.
3. **The AI proposes, a person approves, every version can be undone.**
4. **No user code inside the app.** Custom logic runs only in the mini-app sandbox.
5. **Looks come from the style.** Nothing writes its own colour, font or size.
6. **Features are kits on engines.** A new engine only when no engine can express it.
7. **Light on the device.** Nothing runs when it can't be seen (`useLive()`, [Performance](https://docs.zvessa.com/dev/performance.html)).
# Quick start
## Get started
Open the app and sign up. Then build by chat, in HQ, or from your own AI over MCP
([Connect](https://docs.zvessa.com/dev/ai.html#capabilities)).
Sign up and answer a few questions: your name, what the space is for (just me, a business, a
team, family, kids, a creator, a community, games, something else), its name, a look (Classic
or Futuristic) and the starter kits that purpose suggests, which you can untick. You land on the
dashboard: **HQ**, your **Home** page and the **AI** chat. The purposes and their kits are in
`convex/lib/onboarding.ts`; kits that aren't ready yet are skipped. Ready-made starting points
are on [Templates](https://docs.zvessa.com/dev/templates.html).
## Your first change, three ways
All three write the same blueprint and all three can be undone in HQ → Settings → History.
**1. Ask the AI.** Type "Make this page a link-in-bio with my Instagram and YouTube". The AI
proposes a change; you see its summary and tap **Approve**. Nothing changes before that.
**2. Tap in HQ.** HQ → Settings → Library → add **Boards and tasks**. HQ → Settings → Look → switch to Futuristic,
dark. Each tap is saved as its own version.
**3. Call the API.** Any signed-in client with the `manage` permission can send a change:
```ts
await convex.mutation(api.blueprint.change, {
summary: "Add a welcome line",
ops: [{ op: "add", path: "/pages/1/blocks/0",
value: { id: "welcome", kind: "text", props: { text: "Welcome!" } } }],
});
```
The server applies it to a copy, runs every check, and refuses it with a readable message if
anything is wrong, so a bad change never reaches the space. See [Blueprint](https://docs.zvessa.com/dev/blueprint.html#changes).
## Which piece do I need?
| You want | Use | Page |
| --- | --- | --- |
| Words, pictures, buttons, layout | Blocks | [Blocks](https://docs.zvessa.com/dev/blocks.html) |
| A list of things people add to (sign-ups, products, tasks, scores) | A collection + `records.*` blocks | [Data](https://docs.zvessa.com/dev/data.html) |
| A different look | A style, or a few changes on top of one | [Styles](https://docs.zvessa.com/dev/styles.html) |
| A ready feature (chat, polls, coins, bookings) | A kit | [Kits](https://docs.zvessa.com/dev/kits.html) |
| Who sees or does what | Roles, `visibleTo`, access | [People](https://docs.zvessa.com/dev/people.html) |
| A game, a calculator, anything blocks can't express | A mini-app | [Mini-apps](https://docs.zvessa.com/dev/miniapps.html) |
| An outside service (an API, an MCP server, web search) | A capability | [AI](https://docs.zvessa.com/dev/ai.html#capabilities) |
Rule of thumb: **filled in and placed** → block. **Needs the server to run rules** (data, games,
payments) → engine and kit. **Custom logic someone writes** → mini-app. **Outside service** →
capability.
## Recipes
Each recipe is what you'd ask the AI. The blueprint it ends up with is shown so a developer can
write it by hand too.
### Link-in-bio
> "Make my Home page a link-in-bio: my name, a short line, buttons for Instagram, YouTube and my shop."
```json
{ "id": "home", "title": "Home", "blocks": [
{ "id": "me", "kind": "heading", "props": { "text": "Ada Park" } },
{ "id": "line", "kind": "text", "props": { "text": "Ceramics and slow mornings." } },
{ "id": "links", "kind": "stack", "props": { "gap": 10 }, "children": [
{ "id": "ig", "kind": "button", "props": { "label": "Instagram", "href": "https://instagram.com/ada" } },
{ "id": "yt", "kind": "button", "props": { "label": "YouTube", "href": "https://youtube.com/@ada" } },
{ "id": "shop", "kind": "button", "props": { "label": "Shop", "href": "#catalog" } }
] }
] }
```
`#catalog` opens another page of the space (here the Catalog kit's page).
### Habit tracker
> "Build a habit tracker: I log a habit and a date, and see how many I've done."
A collection and three blocks:
```json
"collections": [{ "id": "habits", "name": "Habits", "fields": [
{ "id": "habit", "label": "Habit", "type": "choice", "options": ["Run", "Read", "Water"], "required": true },
{ "id": "day", "label": "Day", "type": "date", "required": true },
{ "id": "note", "label": "Note", "type": "text" }
] }]
```
```json
{ "id": "habits", "title": "Habits", "blocks": [
{ "id": "habits-count", "kind": "records.count", "props": { "collection": "habits", "label": "Done so far" } },
{ "id": "habits-form", "kind": "records.form", "props": { "collection": "habits", "submit": "Log it" } },
{ "id": "habits-board", "kind": "records.list", "props": { "collection": "habits", "groupBy": "habit" } }
] }
```
### Team space with a chat
> "Make this a team space: roles Coach and Player, a team chat only coaches can post in, and an invite link for players."
Roles in `/roles`, a stream in `/streams` with `mode: "broadcast"` and `postRoles: ["coach"]`,
a `stream` block, an `invite` block with `role: "player"`. Add **Teams and hierarchy** for squads,
each with its own room. See [People](https://docs.zvessa.com/dev/people.html).
### A small game
> "Make a tiny snake game that matches my style."
The AI adds a mini-app to `/apps` (one HTML document) and a `miniapp` block that shows it. The
game reads colours from CSS variables, so it matches any style. See [Mini-apps](https://docs.zvessa.com/dev/miniapps.html).
### A party game
> "Make a trivia night about 90s music, three rounds, teams, and show it on the TV."
The **Trivia night** kit adds a game to `blueprint.games`; the AI writes the questions. People
join with a code on their phones; **Show on TV** puts `game.screen` on the big screen.
### Make a games page
> "Make a games page like a real game app, with trivia, a quiz board and a survey game."
The full detail is in [docs/LOBBY.md](https://docs.zvessa.com/more/lobby.html). The page is built
only from lobby blocks: a `lobby.header`, a `showcase` of featured games, `game.live`, a
`posters` shelf of house games, small "Quick play" posters with links, `game.winners`,
`game.record`, and an optional `ledger.leaderboard`. In code, `gamesLobbyPage({ games,
featured?, quick?, leaderboard? })` in `convex/lib/lobby/page.ts` builds exactly that page from a
list of game ids.
```json
{ "id": "lobby-p-trivia", "kind": "poster", "props": { "game": "trivia-night", "art": "trophy", "palette": "sunset", "badge": "HOT" } }
```
- **Every new game gets a poster.** When the AI makes a game, it adds a poster for it on the
games page.
- **The look is props.** `art` is one of 20 scenes (`trophy`, `board`, `survey`, `mask`,
`sketch`, `dice`, `wheel`, `bingo`…), an emoji or an https picture. `palette` is one of 12
(`sunset`, `ocean`, `grape`…), or `colors` takes 2 or 3 of your own colours. Leave both out
and the poster picks from what the game does, the same way every time. Words are
`words["lobby."]`.
- A poster for a removed game isn't an error. Members don't see it, and managers see it greyed
out.
- `lobby.row` puts the games page in a chat list as a row with a mosaic of its posters.
### A shop
> "Add a catalog of my candles with prices."
The **Catalog** kit. People can buy levels (memberships, VIP) with the space's own coins today (**Coins**,
**Memberships**); card payments with Stripe come later.
## When something is refused
Every change is checked. The message says exactly what to fix, for example
`block "ig": "label" is required` or `dashboard: no page "shop"`. The AI reads the same messages
and fixes its own proposal, up to four tries.
# 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](https://docs.zvessa.com/dev/people.html#access) |
| `words` | Every word the shell shows, by key | [Words](https://docs.zvessa.com/dev/blueprint.html#words) |
| `look` | `{ style, scheme, changes? }`: the style in use, the scheme, small changes on top | [Styles](https://docs.zvessa.com/dev/styles.html) |
| `styles` | The space's own styles | [Styles](https://docs.zvessa.com/dev/styles.html) |
| `dashboard` | `{ panes, nav? }`: which panes show, in order, and how (`tabs` or `list`) | [Dashboard](https://docs.zvessa.com/dev/blueprint.html#dashboard) |
| `pages` | `[{ id, title, icon?, blocks, look?, visibleTo? }]` | [Pages](https://docs.zvessa.com/dev/blueprint.html#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](https://docs.zvessa.com/dev/miniapps.html) |
| `collections` | Shapes of the space's data | [Data](https://docs.zvessa.com/dev/data.html) |
| `roles` | `[{ id, name, can }]`: what each member may do | [People](https://docs.zvessa.com/dev/people.html) |
| `streams` | Conversations the space defines: rooms, channels, queues, with who sees, posts and answers | [Engines](https://docs.zvessa.com/dev/engines.html#spaces) |
| `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](https://docs.zvessa.com/dev/engines.html#spaces) |
| `structure` | Unit kinds and units (teams, classes, branches) as a tree | [Engines](https://docs.zvessa.com/dev/engines.html#structure) |
| `feeds` | Feeds people post in: replies, media, reactions, reposts, expiry, approval, outside sources | [docs/SOCIAL.md](https://docs.zvessa.com/more/social.html) |
| `games` | Game definitions for the Rounds engine: mechanic, items, decks, teams, timer, scoring | [Engines](https://docs.zvessa.com/dev/engines.html#rounds) |
| `ledger` | Units (coins, points), levels (tiers, VIP, memberships) and badges | [Engines](https://docs.zvessa.com/dev/engines.html#ledger) |
| `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 `""` or `"#"` | [Photo books](https://docs.zvessa.com/dev/blueprint.html#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](https://docs.zvessa.com/dev/engines.html#reviews) |
| `referrals` | Personal invite codes, rewards, `qualify`, `limit` and `milestones` | [People](https://docs.zvessa.com/dev/people.html#invites-and-referrals) |
| `notifications` | `{ topics?, defaults?, digestAt?, kidsQuiet?, inboxPage? }`: the space's own topics and default modes | [Notifications](https://docs.zvessa.com/dev/notify.html) |
| `safety` | Word filter (`words`, `presets`, `links`, `contacts`, `action`), report `reasons`, `escalate`, `modRoles`, `autoHide`, `notify`, `who` | [docs/SAFETY.md](https://docs.zvessa.com/more/safety.html) |
| `web` | `{ handle, domains?, home?, routes?, card?, data?, index?, embed?, appLink?, nav? }`: which pages open in a browser, at what address | [docs/WEB.md](https://docs.zvessa.com/more/web.html) |
| `bots`, `automations` | AI teammates (role, instructions, tools, approval) and rules (`when`, `if`, `do`) | [AI](https://docs.zvessa.com/dev/ai.html#bots-and-automations) |
| `connections` | Outside MCP servers and HTTPS APIs the AI may use; keys stay in the vault | [AI](https://docs.zvessa.com/dev/ai.html#capabilities) |
| `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](https://docs.zvessa.com/dev/ai-jobs.html#ai-settings), [AI limits](https://docs.zvessa.com/dev/ai-limits.html#space-settings-ailimits) |
| `audience` | `"everyone"` (default) or `"kids"` | [People](https://docs.zvessa.com/dev/people.html#kids-spaces) |
| `kits` | Kits this space added, and what each added | [Kits](https://docs.zvessa.com/dev/kits.html) |
| `fromLibrary` | Templates and kits added from the library, and what each added, so they can be taken out | [Templates](https://docs.zvessa.com/dev/templates.html#templates-and-kits-people-make) |
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
```json
{ "id": "menu", "title": "Menu", "blocks": [ … ], "look": { "colors": { "accent": "#c2410c" } }, "visibleTo": ["member"] }
```
- `blocks` is a tree: a block with `children` holds other blocks ([Blocks](https://docs.zvessa.com/dev/blocks.html)).
- `look` is a style layer for this page only ([Styles](https://docs.zvessa.com/dev/styles.html#layers)).
- `visibleTo` lists role ids (or `level:`, `level:+`, `badge:`); unset means everyone who can see the space ([People](https://docs.zvessa.com/dev/people.html#hiding-pages-and-blocks)).
- Every page opens at `/p/` with no new code; a button with `href: "#menu"` goes there.
- The `hq` page is core: it can be rearranged and added to, never removed.
## Dashboard
```json
"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 → Settings → 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](https://docs.zvessa.com/more/photobook.html). 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."]`.
```json
{ "op": "add", "path": "/books/-", "value": { "id": "italy", "name": "Italy", "tag": "italy", "fill": { "chapters": "day" } } }
```
## Words
Every word the shell shows is `words.`; 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](https://www.rfc-editor.org/rfc/rfc6902) 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. `~1` is a `/` inside a key, `~0` a `~`.
- `add` on an object key sets it; `add` on 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 `hq` page, a pane pointing at a page that doesn't exist;
- a `miniapp` block pointing at no app, a `stream` block at no stream, a `records.*` block at no
collection or field, a `groupBy` that 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 `version` and stores the whole blueprint in `versions` with its
summary and who made it (`person` or `ai`).
- 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 `proposals` as `pending` until a person approves (or discards) them.
## Three ways in
1. **HQ**: system blocks (`hq.*`) that edit parts of the blueprint.
2. **The AI**: proposes ops; a person approves.
3. **The API**: `blueprint.change` with ops, under the same permission (`manage`). See [API](https://docs.zvessa.com/dev/api.html).
# Blocks
A block is data: `{ id, kind, props?, children?, visibleTo? }`. It is filled in and placed,
never coded. Every kind is described **once** in `convex/lib/blocks.ts`, and that one list feeds
the renderer, the checks, HQ and the AI's prompt, so a new block shows up everywhere.
The full, current list with every prop is in the [Catalog](https://docs.zvessa.com/reference/catalog.html#blocks) (generated).
## The blocks by purpose
All are ready; each needs its engine's kit or setup only where noted.
| Purpose | Kinds |
| --- | --- |
| Layout ([docs/LAYOUT.md](https://docs.zvessa.com/more/layout.html)) | `header` (the page title with eyebrow, subtitle, mark and actions), `section` (a titled part with See all), `row` (side by side, wraps), `grid` (equal columns), `scroll` (a sideways shelf), `group` and `item` (grouped list rows that can open their own screen), `spacer`, `tabbar` and `tab` (a page's own bottom tab bar, like a phone app), `stack` (column or row), `card` (framed box), `divider`, `switcher` (one child at a time under segments, tabs or pills) |
| Words and media | `heading`, `text`, `list`, `image`, `stat`, `badge` (a small pill), `chips` (wrapping tags); text names a role, not a size ([text roles](https://docs.zvessa.com/more/layout.html#text-roles-tokens)) |
| Actions | `button` (opens a URL or `#page-id`) |
| Data ([Collections](https://docs.zvessa.com/dev/engines.html#collections)) | `records.list` (list or board), `records.form`, `records.count`, `split` (shared costs); record views `records.composer`, `records.timeline`, `records.calendar`, `records.streak`, `records.memories`, `records.prompt`, `records.stats` ([Data](https://docs.zvessa.com/dev/data.html#record-views)) |
| Talk ([Spaces](https://docs.zvessa.com/dev/engines.html#spaces)) | `inbox` (every conversation in one list), `stream` (a room, channel or feed), `direct`, `ask` and `queue` (both sides of a support desk or applications), `drafts` |
| People ([Spaces](https://docs.zvessa.com/dev/engines.html#spaces), [Structure](https://docs.zvessa.com/dev/engines.html#structure)) | `members`, `invite`, `units` (teams, classes, branches as a tree); invites and referrals `invites.code`, `invites.list`, `invites.redeem` and a general `qr` (any link, text, page, the space, its web address or your invite, in a designed look: [QR codes](https://docs.zvessa.com/more/qr.html)) ([People](https://docs.zvessa.com/dev/people.html#invites-and-referrals)) |
| Account ([Account](https://docs.zvessa.com/dev/account.html)) | `account.email`, `account.password`, `account.sessions`, `account.delete`: the viewer's own account, on any page |
| Notifications ([Notifications](https://docs.zvessa.com/dev/notify.html#blocks)) | `notify.settings` (your own controls), `notify.inbox` (tap opens the exact page), `notify.bell` (unread count), `notify.send` (managers send to members) |
| Safety ([docs/SAFETY.md](https://docs.zvessa.com/more/safety.html)) | `safety.queue` (moderators' review queue), `safety.filters` (word filter with a try-it box), `safety.blocked` (my blocked people), `safety.report`, `safety.block`, `safety.admin` (Zvessa's admins) |
| Web ([docs/WEB.md](https://docs.zvessa.com/more/web.html)) | `web.share` (the web address with Copy and Share; `qr: true` adds a designed QR code), `hq.web` (HQ's Web panel: handle, own domains, pages on the web, embeds) |
| Feeds ([Spaces](https://docs.zvessa.com/dev/engines.html#spaces)) | `feed.timeline` (list, cards, grid or stories), `feed.composer`, `feed.profile`, `feed.people`, `feed.activity`, `feed.search`, `feed.saved`, `feed.sources` |
| Reviews ([Spaces](https://docs.zvessa.com/dev/engines.html#reviews), [docs/REVIEWS.md](https://docs.zvessa.com/more/reviews.html)) | `reviews.summary` (average, count, a bar per star, aspects, Write a review), `reviews.list` (sort and filter), `reviews.stars` (a small ★ 4.6 (128) line that opens the reviews), `reviews.write` ("How was it?" stars to tap), `reviews.top` (subjects ranked), `reviews.queue` (waiting and reported, managers only) |
| Money, levels, badges ([Ledger](https://docs.zvessa.com/dev/engines.html#ledger)) | `ledger.balance`, `ledger.send`, `ledger.give`, `ledger.history`, `ledger.leaderboard`, `ledger.levels`, `ledger.marks`, `ledger.grants` |
| Play ([Rounds](https://docs.zvessa.com/dev/engines.html#rounds)) | `poll`, `wheel`, `game` (a live game from `blueprint.games`), `game.screen` (its big-screen view) |
| Game lobby ([Quick start](https://docs.zvessa.com/dev/quickstart.html#make-a-games-page), [docs/LOBBY.md](https://docs.zvessa.com/more/lobby.html)) | `poster` (one game or link card), `posters` (a shelf in three sizes), `showcase` (featured carousel), `game.live` (games you can join now), `game.winners`, `game.record` (your own plays and best), `lobby.header` (logo, line, coins), `lobby.row` (a chat-list row that opens the games page), `game.frame` (one game's play screen on a page) |
| Time ([Time](https://docs.zvessa.com/dev/engines.html#time)) | `booking` (book a time), `scheduled` (post later) |
| Numbers ([Events](https://docs.zvessa.com/dev/engines.html#events)) | `analytics` (charts per day, managers only) |
| Live ([Rooms](https://docs.zvessa.com/dev/engines.html#rooms), [Files](https://docs.zvessa.com/dev/engines.html#files), [Screens](https://docs.zvessa.com/dev/engines.html#screens)) | `room` (group call or stage), `call` (rings people), `media.gallery`, `media.player`, `screen.pair`, `screen.remote` |
| Starters ([Templates](https://docs.zvessa.com/dev/templates.html)) | `hero` (page header with picture and buttons), `tiles` / `tile` (app-style grid), `links` / `link` (services or links with price and action), `chats` (the chat list), `calls.recent` (call buttons and recent calls), `conversation` (one chat with its bar; on the `talk.screen` page it is every opened chat) |
| Photo books ([Blueprint](https://docs.zvessa.com/dev/blueprint.html#photo-books)) | `photobook.book` (cover and pages to swipe, read full screen, Edit book for managers), `photobook.shelf` (covers of the space's books, plus one per #tag album) |
| Custom logic | `miniapp` (a sandboxed app from `/apps`, with a `config` prop); the public library's `miniapps.library` (browse, try, add, remix), `miniapps.mine` (publish and update your apps) and `miniapps.review` (admins), from the App library kit ([Mini-apps](https://docs.zvessa.com/dev/miniapps.html#the-public-library)) |
| AI ([AI jobs](https://docs.zvessa.com/dev/ai-jobs.html#blocks), [AI limits](https://docs.zvessa.com/dev/ai-limits.html#blocks-and-tools)) | `ai.setup` ("Connect your AI": paste the Claude key), `ai.chat` (the builder chat; props `placeholder`, `model`, `steps`), `ai.model` (model size, streaming, finish pushes), `ai.notes` (what the AI remembers about you), `ai.jobs` (latest AI work with Stop), `ai.allowance` (what the viewer has left today, with a bar), `hq.aiUsage` (managers: today's use by person, space and bots, and whose key pays) |
| Sharing ([Templates](https://docs.zvessa.com/dev/templates.html#templates-and-kits-people-make)) | `share.save` (save pages or the space, with the list of what's taken out), `share.library` (browse, preview, add or switch), `share.mine` (your pieces: publish, update, take out), `share.review` (admins) |
| Search ([docs/SEARCH.md](https://docs.zvessa.com/more/search.html)) | `search` (one box across pages, records, messages, posts, people and files; `sources`, `collection` / `stream` / `feed`, `limit`; each person only finds what they could open) |
| Bots and automations ([AI](https://docs.zvessa.com/dev/ai.html#bots-and-automations)) | `bots.list` (bots and automations with an on/off switch), `bots.waiting` (actions waiting for you), `bots.runs` (what happened), `bots.run` (run one by hand), `bots.ask` (ask a bot), `bots.hooks` (webhook addresses, shown once) |
| HQ (system, [docs/HQ.md](https://docs.zvessa.com/more/hq.html)) | `apps` (an app home of everything the space has, built from the blueprint), `hq.status` (Needs you: proposals, reports, join requests), `hq.kit` (one added kit and Remove), `ai.approval` (Ask or Auto approval for the AI), `setup.steps` and `setup.step` (the Get ready card that ticks itself off), `hq.proposals`, `hq.pages`, `hq.library`, `hq.styles`, `hq.access`, `hq.connections`, `hq.history`, `hq.blueprint`; with the starters, `hq.overview` (KPIs), `hq.starters` (switch template) and `hq.navigation` (tabs or chat list) |
`room` and `call` need LiveKit on the server; big media needs R2 ([docs/LIVE.md](https://docs.zvessa.com/more/live.html)).
Planned: native form fields (date, slider, segmented choice), a calendar, a map, a
timer and a pay-by-card button.
`switcher` takes `labels` (one per child; a missing one uses the child's `title`), `look`
(`segments`, `tabs` or `pills`) and `start`. Only the open part is drawn, so the other parts'
live data isn't loaded until someone opens them. In Edit mode every part shows, so each can be changed.
## Props
- A prop is `string`, `number`, `boolean` or `string[]`. Richer values (objects) are passed as a
JSON string, as `records.form`'s `defaults` does, so every prop stays checkable.
- **Words go in props** (`label`, `empty`, `submit`, `done`), so every word is a setting.
- **Looks never go in props.** No colours, fonts or sizes; a different look is a style, a page
look or a part override ([Styles](https://docs.zvessa.com/dev/styles.html)). `stack`'s `gap` (px) is the one layout number a
block holds today.
- Links: `https://…` opens outside the app; `#page-id` opens a page of the space;
`chat:` opens a conversation, `ai` the AI chat, plus `mailto:` and `tel:`. Images must be `https://`.
## Hiding a block
`visibleTo: ["coach", "parent"]` shows the block only to those roles; managers always see
everything. The server leaves hidden blocks out of what it sends, so they never reach the
device ([People](https://docs.zvessa.com/dev/people.html#hiding-pages-and-blocks)).
## How blocks are drawn
`src/components/Renderer.tsx` holds one small function per kind in `RENDERERS`. It draws with
React Native, so the same code runs on iOS, Android and the web. Every renderer reads its look
from `useWorkspace()` and the helpers in `src/lib/workspace.tsx` (`surface`, `bodyText`,
`headingText`, `gap`, `color`) and never writes a colour, font or size of its own. An unknown
kind shows a quiet "Unknown block" line instead of crashing.
Kits keep their renderers in their own folder (`src/kits//`) in a `*_RENDERERS` table
(`SOCIAL_RENDERERS`, `LIVE_RENDERERS`, `LEDGER_RENDERERS`). `Renderer.tsx` looks a kind up in
`RENDERERS` first, then in the kit tables **when the block draws**; the tables are not spread in
when the file loads, because that made an import cycle that crashed the app on start. Kit block
descriptions are spread into `BLOCKS`. Blocks that show live data (records, posts, votes) read it
through Convex queries, which update on every device as soon as anything changes.
## Adding a block
1. **Describe it** in `BLOCKS` in `convex/lib/blocks.ts`: `kind`, a one-sentence `about` the AI
reads, its `props` (type, required, about) and whether it holds `children`.
2. **Draw it**: add a renderer to `RENDERERS` in `src/components/Renderer.tsx` (or to your kit's
own `*_RENDERERS` table, added to the lookup list in `kitRenderer`). Put anything bigger than a few lines in its
own component file. If it has an icon, use `` ([Device](https://docs.zvessa.com/dev/device.html#icons)).
3. **Cross-check it** if it points at another part of the blueprint (a collection, a stream, an
app): add the check to the `walk` in `checkBlueprint` (`convex/lib/blueprint.ts`), so a broken
reference is refused with a readable message.
4. **Regenerate the catalog**: `npm run docs`.
HQ and the AI pick the block up from the list; nothing else changes.
### Good blocks
- **Abstract.** Name it by what it is (`stat`, `records.list`), never by one use (`streakCounter`).
If two kits would want slightly different versions, it's one block with a prop.
- **Cheap.** Lists page their data (no more than they show); timers, sensors, media and animation
run only while `useLive()` is true; images are sized. See [Performance](https://docs.zvessa.com/dev/performance.html).
- **Native.** Use the platform's own controls and icons where they exist ([Device](https://docs.zvessa.com/dev/device.html)).
- **Safe.** A block can't run code, fetch a URL or read data its viewer isn't allowed to see. The
server checks permissions; the block only asks.
- **Accessible.** Real buttons with labels, images with `alt`, text that scales with the style.
# Styles
How everything looks is data: a **style**. Classic and Futuristic are built in and written the
same way anyone's own style is, so they are the worked examples (`convex/lib/styles.ts`). Every
setting, its options and ranges: [docs/STYLES.md](https://docs.zvessa.com/more/styles.html). This page is how to use them.
## The three places a look lives
```json
"look": { "style": "paper", "scheme": "system", "changes": { "colors": { "accent": "#15803d" } } },
"styles": [{ "id": "paper", "name": "Paper", "extends": "classic",
"type": { "body": "serif" }, "shape": { "radius": 20 } }],
"pages": [{ "id": "party", "title": "Party", "blocks": [], "look": { "backdrop": { "kind": "dots" } } }]
```
1. **`look.style`**: which style the space uses (`classic`, `futuristic` or an id from `styles`).
2. **`look.changes`**: small tweaks on top, without making a style ("make the accent green").
3. **`page.look`**: one page looks different (a party page with dots).
The ground behind everything is the style's `backdrop`: `plain`, `grid`, `dots` or `gradient`,
with `size` and moving `beams`. The lit scene adds `lines` (1 to 4 colours the grid sweeps
through), `glow` (0 to 3 drifting lights), `floor` (a lit floor of lines moving toward you) and
`streaks` (0 to 3 light streaks). It's drawn by `src/components/Scene.tsx` from
`convex/lib/scene.ts`, and the moving parts follow the [Performance](https://docs.zvessa.com/dev/performance.html) rules.
Later, a person's own look on their device sits on top of all of these.
## Layers
Lowest first, each the same shape, so any layer can change anything:
platform default → the style and what it `extends` → `look.changes` → `page.look` → (later) the
person's device look. A setting not given falls through to the layer below, so a style lists only
what it changes.
Every token, with its value in Classic, Futuristic and Tech: [Style tokens](https://docs.zvessa.com/more/tokens.html).
## Text roles and sizes
Blocks never hold a font size or colour. They name a **text role** (title, body, caption and so on),
a **size step** or a **colour role**, and the style sets what those look like, with different values
for Classic and Futuristic. The full list of roles and tokens is in
[Layout, text and tokens](https://docs.zvessa.com/more/layout.html#text-roles-tokens).
## Colours
- **Core names** every screen uses: `bg surface raised text text2 text3 line grid accent accent2
onAccent success warn danger`.
- **Any other name** is yours: `gold`, `win`, `felt`, `wheel-1`. Kits, blocks and mini-apps read
them; a style that doesn't set one falls back safely.
- **Values**: `#rrggbb`, `#rrggbbaa`, `white`, `black`, `transparent`, another colour's name,
`mix(a, b, 40%)`, `alpha(a, 20%)`, `contrast(a)`. They nest, so three colours can derive the rest:
```json
"schemes": { "dark": { "bg": "#0b1410", "text": "#e8f3ec", "accent": "#34d399" } },
"colors": { "surface": "mix(bg, text, 6%)", "line": "alpha(text, 12%)", "onAccent": "contrast(accent)" }
```
- **Schemes**: `light`, `dark` or any name (`night`, `contrast`), up to 8. `look.scheme: "system"`
follows the device.
## Recipes
| Ask | Change |
| --- | --- |
| "Make the accent orange" | `add /look/changes/colors/accent "#ea580c"` |
| "Rounder corners everywhere" | `add /look/changes/shape/radius 22` |
| "Dark mode always" | `replace /look/scheme "dark"` |
| "Buttons outlined, not filled" | `add /look/changes/parts/button { "treatment": "outline" }` |
| "A calm look with no animation" | `add /look/changes/motion { "speed": "off", "haptics": "off" }` |
| "Warm paper with a serif font" | new style in `/styles` extending `classic`, then `replace /look/style "paper"` |
A path's parents must exist: if `/look/changes` or `/look/changes/colors` isn't there yet, add
it first (`add /look/changes { "colors": {} }`), or add the whole object in one op.
## Rules for anything you build
- **Never write a colour, font or size.** Read the resolved style through `useWorkspace()` and the
helpers in `src/lib/workspace.tsx`:
`surface(theme, part)` for a box, `bodyText(theme, size)` and `headingText(theme, size)` for
text, `gap(theme, steps)` for spacing, `color(theme, name)` for a colour by name.
- **Need something new?** Add a colour name or a part, not a hard-coded value.
- **Parts** a style can restyle one by one: `pane card button input bubbleMine bubbleTheirs tabbar
header` (treatment, depth, radius, bg, text, line).
- **Motion and haptics are settings.** Respect `theme.motion.speed` (`off` means no animation) and
`theme.motion.haptics`. Planned: the phone's Reduce Motion setting counts as `off`.
- **Mini-apps get the style too** as CSS variables and `window.zvessa.style`
([Mini-apps](https://docs.zvessa.com/dev/miniapps.html#the-style)).
## Safe by design
Styles are checked on the server (`checkStyles`, `checkLayer`): unknown settings, values out of
range and anything that isn't a colour value are refused. A style can't run code, load a URL or
fake a screen. A colour that can't resolve falls back to Classic's, so a mistake never breaks a
screen.
# Data
Two kinds of live data, both kept out of the blueprint: **records** (things people add, from the
Collections engine) and **events** (things that happened, from the Events engine).
## Collections (ready)
A collection is the shape of a list of things, in `blueprint.collections`. Products, sign-ups,
tasks, bookings, scores and notes are all collections with different fields. No new database
table is ever needed. Code: `convex/lib/collections.ts`, `convex/records.ts`. Code that writes
records must also write `text: recordText(data)` (`convex/lib/search.ts`) so [search](https://docs.zvessa.com/more/search.html)
finds them; after upgrading a deployment, run `npx convex run kits/search/search:_backfill` once.
```json
{ "id": "orders", "name": "Orders", "fields": [
{ "id": "item", "label": "Item", "type": "text", "required": true },
{ "id": "qty", "label": "How many", "type": "number" },
{ "id": "status", "label": "Status", "type": "choice", "options": ["New", "Packed", "Sent"] }
] }
```
- 1 to 40 fields. Ids: lowercase, digits and dashes.
- Changing a collection's shape is a blueprint change, saved and undoable. Records already saved
keep their values.
- Removing a kit removes its collection shapes but **keeps the records**, so adding it back brings
them back.
### Field types
Every value is checked and cleaned on the server before it's saved.
| Type | Stored as | Rule |
| --- | --- | --- |
| `text` | string | up to 500 characters |
| `longtext` | string | up to 8,000 characters |
| `number` | number | any finite number; `1,200` is read as 1200 |
| `money` | number | like number; the currency comes with the Ledger engine |
| `date` | string | `2026-10-20` (a time may follow) |
| `choice` | string | one of `options` (at least two) |
| `checkbox` | boolean | |
| `email` | string | looks like an email, up to 200 |
| `phone` | string | up to 500 |
| `url` | string | must start with `https://` |
Planned: files and images, a person, a link to another record, a place, rich text.
### Showing and collecting records
| Block | Does |
| --- | --- |
| `records.form` | Adds a record. `fields` picks which to ask; `defaults` (a JSON string) sets values on every new one, like `{"status":"New"}`. |
| `records.list` | Lists records, newest first. With `groupBy` naming a choice field, it's a board with one column per option. |
| `records.count` | How many records there are. |
### Record views
More ways to show and write any collection: a journal, a gratitude log, a habit or workout
tracker, a reading list, a team check-in. Described in `convex/lib/records/views.ts`, drawn by
`src/kits/records`.
| Block | Does |
| --- | --- |
| `records.composer` | A question, the chip field's options to tap (a mood), and a button that opens the full-screen writing sheet |
| `records.timeline` | Records under month or day headers, with search (words and #tags) and Write |
| `records.calendar` | A month coloured by each day's chip; tap a day to read or write for it |
| `records.streak` | Days in a row, the best streak, the total, this week as dots, an optional goal, a nudge until today has one |
| `records.memories` | "On this day": a week, a month, six months and each year ago; hidden when there are none |
| `records.prompt` | A daily prompt (the same all day), "another one", and Write, which saves the prompt on the record |
| `records.stats` | Insights over the last N days: days written, records, words, best streak, a bar per chip option |
Props name which field plays which part, so they fit any collection: `title` (text), `body`
(longtext), `chip` (a choice field, with `icons` and `tones` per option), `date` (the record's
day; without one, the day it was written), `tags` (#tags; ones in the body count too), and
`field` (where the prompt is saved). `"none"` or `""` turns a part off, and a field the
collection lacks is simply not drawn. Every word is a prop (`question`, `note`, `write`,
`placeholder`, `saved` with `{streak}`, `label`, `shuffle`, `empty`, `heading`, `nudge`, `done`).
Tapping a record opens a reader with Share, Edit and Delete. The writing sheet is a native page
sheet: pick a day (never the future), the prompt, the chip options, a title, the body with a word
count, #tags and any other fields; Cancel asks before throwing work away.
Server reads in `kits.records.views`: `days`, `onDays`, `search` and `one`. They follow the same
rule as `records.list` (hidden behind every block means hidden on the server), so
`visibleTo: ["owner"]` on a page keeps a journal private. Each read looks at the newest 2,000
records; writing goes through `records.add`, `update` and `remove`.
### Who can do what
| Action | Needs |
| --- | --- |
| See records | `view` |
| Add a record, change or delete your own | `add` |
| Change or delete anyone's | `edit` |
Each record knows who added it. `records.list` returns `mine` and `canChange` for every row, so a
screen shows edit buttons only where they work. The server checks again on every write.
### Limits today
- A list returns up to 200 records by default, 500 at most.
- `records.count` counts up to 10,000.
- Planned: filters, sorting, search, paging and per-collection permissions.
## Events
The Events engine counts what happens in a space, per day and per person (`convex/tally.ts`,
`convex/events.ts`). It's on once the space has the **Analytics** or **Leaderboards** kit.
- **Counted for you:** `view` (page views, also per page), `post` (messages and posts), `record`
(records added), `join` (people who joined), `play` (votes, spins and moves).
- **Your own events:** `events.track({ name })` from a block or screen, with a short lowercase
name like `tap-buy` or `finished-level`. Anyone who can see the space may send them.
- **Totals:** `events.summary({ days })` gives daily totals per name for the last 1 to 90 days
(managers only); the `analytics` block draws them as charts.
- **Rankings:** `events.leaders({ name, days })` ranks people by how often they did something;
`ledger.leaderboard` with `event` shows it.
- Counts are kept as running totals, so reading them never scans every row.
Planned: the AI reads totals (never raw personal data) to suggest improvements; mini-apps send
events through the bridge; sums of values, not only counts. Analytics belong to each space's
owner; our own product analytics are separate and never mixed in.
# People, roles and access
One model fits a family, a team, a class, a business or a fan club: **roles the space names**,
**seven fixed permission verbs**, and **access rules** for who can find and join. Code:
`convex/lib/roles.ts`, `convex/access.ts`, `convex/members.ts`.
## Permissions
Kits, blocks and server functions check **verbs, never role names**, so renaming or adding a role
never breaks anything.
| Verb | Allows |
| --- | --- |
| `view` | See the space's pages |
| `post` | Write in chats and streams |
| `play` | Vote, play games and join rounds |
| `add` | Add records, and change their own |
| `edit` | Change or delete anyone's records |
| `invite` | Invite people, answer join requests |
| `manage` | Change the space (pages, styles, kits, roles), approve the AI's changes, use the AI chat |
## Roles
```json
"roles": [
{ "id": "owner", "name": "Owner", "can": ["view","post","play","add","edit","invite","manage"] },
{ "id": "coach", "name": "Coach", "can": ["view","post","play","add","edit","invite"] },
{ "id": "player", "name": "Player", "can": ["view","post","play","add"] },
{ "id": "visitor", "name": "Visitor", "can": ["view"] }
]
```
- Default roles: Owner, Member, Guest, Visitor ([Catalog](https://docs.zvessa.com/reference/catalog.html#default-roles)).
- **`owner` and `visitor` are core.** The owner keeps every permission; a space has one owner.
Visitors (people looking at a public, link or friends space they haven't joined) can never
`manage`, `invite` or `edit`, and may have no permissions at all.
- Only managers can hand out a role that can `manage` or `invite`.
- A member's role changes with `members.setRole`; anyone can leave with `members.removeMember`.
## Access
```json
"access": { "visibility": "private", "join": "invite" }
```
| `visibility` | Who can look |
| --- | --- |
| `private` | Members only |
| `link` | Anyone with the link |
| `friends` | The owner's friends |
| `public` | Anyone; listed in Explore |
| `join` | How people become members |
| --- | --- |
| `invite` | Only with an invite link |
| `request` | They ask; someone with `invite` accepts and picks their role |
| `open` | Anyone who can look can join as a member |
A new space is `private` and `invite`.
## Invites
`members.createInvite({ role, maxUses? })` makes a 10-letter code for a link. `members.peek`
shows the space's name before joining; `members.join` joins with the invite's role. Codes can be
used up (`maxUses`) or revoked. The `invite` block puts this on a page.
### Invites and referrals
The **Invites and referrals** kit (Grow) gives people their own invite code and QR,
records who invited whom, and rewards in any Ledger unit (coins; no real money). Code:
`convex/kits/invites/` (its `README.md` has the detail; `REFERRALS_GUIDE` in `model.ts` is what the AI
reads).
```json
"referrals": {
"codes": { "who": "view", "role": "member" },
"rewards": [{ "to": "inviter", "unit": "coins", "amount": 50 }, { "to": "invited", "unit": "coins", "amount": 25 }],
"qualify": { "hours": 24, "event": "post", "times": 1, "days": 30 },
"limit": { "count": 10, "every": "month" },
"milestones": [{ "at": 5, "badge": "verified" }, { "at": 10, "unit": "coins", "amount": 200 }]
}
```
- `codes`: who gets a personal code (a verb) and the role it gives. Without it, only roles that can
`invite` make links. Without `/referrals` at all, who invited whom is still recorded.
- `qualify`: a reward waits until the new person has stayed (24 hours by default) and, if set, done
something (`event`, `times`), within `days`. Checks are scheduled on the server, never polled.
- `rewards` go through the Ledger, paid once even on a retry, and the `refer` event is counted, so
`ledger.leaderboard` with `event: "refer"` ranks top inviters. `milestones` add badges or units.
- **Against farming:** a person counts once per space, ever; no self-referrals; the owner and TVs
never count; someone who held units here before counts as `void`; leaving first lapses it;
reward units are locked (spendable here, not sendable) unless `sendable: true`; each inviter has
a limit (10 rewarded a month by default); a code can't give a role that can manage, invite or
edit. In kids spaces only grown-ups (roles that can invite) get codes.
Blocks: `invites.code` (your code, QR and share), `invites.list` (who you invited and their
status), `invites.redeem` (enter a code), and a general `qr` block (any link, text or page, or `link: "invite"` for each person's own invite). `invites.code` and `qr` take the same `design` ([QR codes](https://docs.zvessa.com/more/qr.html)). AI and
MCP tools in the new `people` scope: `list_invites`, `create_invite`, `revoke_invite`,
`list_referrals`, `recheck_referrals`. Making and turning off links through tools needs a person's
token; the in-app AI can read and recheck, and changes the rules only by proposal.
## Hiding pages and blocks
```json
{ "id": "staff", "title": "Staff only", "visibleTo": ["coach"], "blocks": [] }
{ "id": "answers", "kind": "card", "visibleTo": ["coach"], "props": { "title": "Answers" } }
```
`visibleTo` on a page or block lists the role ids that see it; unset means everyone who can see
the space. **The server removes hidden parts before sending the blueprint** (`blueprintFor`), so
they never reach the person's device. Managers see everything.
**The data behind a hidden block is hidden too.** If every block that shows a collection, stream,
feed or mini-app is hidden from someone, the server also refuses to read or write it for them,
whatever client they use. Data no block points to stays reachable to anyone with the verb. See
[Security](https://docs.zvessa.com/dev/security.html#hidden-means-hidden-data-too).
## Several spaces
A person can own a space and be a member or visitor of others. `members.mySpaces` lists them,
`members.open` / `members.visit` switches, `members.home` goes back to their own. Every function
works on the space the person has open, through `getAccess`, so permissions live in one place.
## Friends
Friends are between accounts, across spaces (`friends.*`: ask, answer, remove, list, and the
spaces your friends let you see). They decide who sees `friends` spaces. Direct messages
(`dms.*`, the `direct` block) follow a person across spaces too.
## Teams, classes and branches
The Structure engine (Teams and hierarchy kit) puts people into units of any kind, named by the
space ("Class" led by a "Teacher", "Squad" led by a "Captain"), as a tree. Leads place people in
their units and set titles; open units can be joined. A stream can be for one unit only, so each
team gets its own room. See [Engines](https://docs.zvessa.com/dev/engines.html#structure).
## Levels and badges
With a Ledger kit (tiers, VIP, memberships, verified), `visibleTo` also takes `level:`,
`level:+` (that level or higher by rank) and `badge:`, so a page can be for VIPs or
verified members only. Hidden parts are removed on the server the same way.
## Kids spaces
`"audience": "kids"` (the Kids space kit) keeps the space private and invite-only, out of
Explore, and tells the AI to keep everything kid-safe. The check refuses any other access while
it's on. Kids (members whose role there can't invite or manage) only message, friend or ring
people who share one of their kids spaces, and only the owner can open the space up again
([Security](https://docs.zvessa.com/dev/security.html#kids-spaces)).
## Planned
- Kids spaces where grown-ups also set who kids can talk to and how long they play.
- Gates by password, and pay-by-card memberships (Stripe).
- **Sign-in**: email and password today; Apple and Google before release; email codes later.
# Kits
Everything beyond the dashboard is an optional **kit**. A new space has only HQ, its page and the
AI chat; people add a kit only if they want it, and taking it out is one tap and can be undone.
Kits are settings, never code: each one switches on [engines](https://docs.zvessa.com/dev/engines.html) and adds pages, blocks,
collections and settings to the blueprint. Code: `convex/lib/kits.ts`. Overview of the idea:
[docs/KITS.md](https://docs.zvessa.com/more/kits.html).
The full library with each kit's status: [Catalog](https://docs.zvessa.com/reference/catalog.html#kits) (generated).
## What's ready
Highlights by group (the [Catalog](https://docs.zvessa.com/reference/catalog.html#kits) has the full, current list and status):
| Group | Ready kits |
| --- | --- |
| Build | Forms, Connections, Web research, Media library, Gallery, Photo book (includes the Gallery), Templates and kits, AI helper (an owner-only AI page with model size, notes and jobs) |
| Talk | Group chat, Channels, Direct chat, Teamspace, Support desk, Posts and threads, Photo feed, Stories, Forum |
| People | Contacts, Teams and hierarchy, Tiers, VIP, Verified, Profile and followers |
| Money | Catalog, Coins, Memberships |
| Play | Polls, Wheel, Hot Potato, High Card, Leaderboards |
| Party | Trivia night, Quiz board, Survey showdown, Charades, Party pack, Game maker |
| Plan | Group plans, Secret Santa, Journal |
| Live | Calls, Live streams, Show on TV |
| Time | Appointments, Scheduler |
| Work | Boards and tasks, Drafts and approvals |
| Grow | Analytics, Reviews, Guestbook, Outside feeds, Invites and referrals, Public links |
| Safety | Moderation (a Safety page with the queue, filter and blocked list). Reporting and blocking are always on |
| Bots | Bots (a Bots page), with recipes Helper bot, Welcome message, Reply drafts, Daily digest and Coins for posts |
| Tools | Notifications and Announcements (pages for the inbox and settings, and a send form), Search (a Search page), App library (the public mini-app library), starter mini-apps: calculator, timer, focus, dice, countdown, flashcards, habits, weather, live info and more ([Mini-apps](https://docs.zvessa.com/dev/miniapps.html#starter-mini-apps)) |
| Safety | Kids space |
Coming: card payments and tips (Stripe), a bookable service page, events and tickets,
moderation, quests, docs and wiki, automations, bots, invites and referrals, public links and
auto-post. Ready-made starting points that combine kits are on [Templates](https://docs.zvessa.com/dev/templates.html).
## Adding and removing
| How | Call |
| --- | --- |
| HQ → Settings → Library | `blueprint.addKit({ kit })`, `blueprint.removeKit({ kit })` |
| Asking the AI | The AI's `add_kit` tool makes a proposal the person approves |
Adding a kit is one version. If a kit's collection id already exists in the space (the Journal
kit and the Personal starter both use `journal`), the space keeps it and its records, gets only
the missing fields, and keeps it when the kit is taken out. The space remembers what it added in `blueprint.kits.`
(its pages, collections, streams, roles, games, ledger units and levels, audience), so removing
takes out exactly that, plus any dashboard pane showing its pages, and any block on the pages
that stay that shows one of its feeds, collections or streams (`removeKitOps`), so a page never
points at something that's gone. **Records stay**, so adding the kit back brings the data
back. A kit can't be added twice, and one that waits for an engine can't be added yet.
After adding, everything a kit made is ordinary blueprint: rename it, restyle it, move its blocks,
hide it from some roles. A kit never hard-codes a use case: "Support desk" is a space with a queue
and assignment; "VIP" is a rank.
## Feed recipes
Ready settings for the kinds of feed people ask for, in `convex/kits/social/recipes.ts`: threads,
photos, stories, clips (short videos), updates (owner-only news), board (members post, managers
approve), moments (private family photos), questions (Q&A), forum, links (upvotes and
discussion), reviews, guestbook and portfolio. The AI uses one when someone names the kind
("add a clips feed") and changes what they ask. `feedRecipeOps(bp, recipe, { name?, id?, page?,
tab? })` returns the changes that add the feed and a page for it, with ids that never clash.
| Recipe | Makes |
| --- | --- |
| `threads` | Short posts, threads, reposts, quotes (like X or Threads) |
| `photos` | Photos and videos with captions, as cards (like Instagram) |
| `stories` | One photo or video, gone after a day |
| `clips` | Short videos only, several reactions (like TikTok or Reels) |
| `updates` | Owner-only news with titles and comments (a blog, a changelog) |
| `board` | Members post, managers approve first (a class or fan wall) |
| `moments` | Private family or friends photos, warm reactions |
| `questions` | Titles and threaded answers, most helpful on top (Q&A) |
| `forum` | Topics, long threads, most active on top |
| `links` | Links with titles, upvotes and discussion (like Reddit) |
| `reviews` | Star ratings with words and photos (the Reviews kit itself now uses [review boards](https://docs.zvessa.com/dev/engines.html#reviews)) |
| `guestbook` | Short notes, no pictures |
| `portfolio` | Owner-only grid, #tags as collections |
## Writing a kit
A kit is one entry in `KITS` in `convex/lib/kits.ts`, or in a kit list of its own spread into it
(`ROUNDS_KITS`, `LEDGER_KITS`, `LIVE_KITS`, the social kits). Besides `collections` and `pages`,
a kit can add `streams`, `feeds`, `roles`, `games`, ledger settings, `apps` (mini-apps) and
`connections`. The simplest shape:
```ts
{
id: "signups",
name: "Sign-ups",
about: "People sign up for something; you see the list.", // one line people read
group: "Grow", // a KIT_GROUPS entry
engines: ["collections"],
collections: [{ id: "signups-people", name: "People", fields: [
{ id: "name", label: "Name", type: "text", required: true },
{ id: "email", label: "Email", type: "email", required: true },
] }],
pages: [{ id: "signups", title: "Sign-ups", blocks: [
{ id: "signups-form", kind: "records.form", props: { collection: "signups-people", submit: "Sign up" } },
{ id: "signups-list", kind: "records.list", props: { collection: "signups-people", empty: "Be the first." } },
] }],
}
```
Rules:
1. **Ids start with the kit's id** (`signups-…`) for pages, blocks, collections, streams and games, so
kits never clash with each other or with what people made.
The one exception is a piece several kits mean to share: the talk kits all use one `chats`
page with the inbox, and Channels and Teamspace share `general` and `announcements`, so a
space with both has one of each. A shared piece is added with the first kit that lists it and
stays while any of them is in the space.
2. **Only blocks, collections and settings.** If the kit needs the server to run rules, that's an
engine; use one that exists, or add a new engine only when none can express it.
3. **Words in props, looks from the style.** No colours or fonts. If the kit needs its own colour,
name one (`gold`, `felt`) and let styles set it.
4. **A kit is ready** when every engine it uses is ready and it adds something: a page,
collection, stream, role, game, feed or ledger settings (`kitReady`). Until then the Library shows it as coming.
5. **Check it**: the result must pass `checkBlueprint` on a fresh space, and removing it must
leave the space as it was.
6. **Regenerate the catalog**: `npm run docs`.
Planned: kit settings that people change later (a currency name for Coins, quiet hours for
Notifications) live in `blueprint.kits.` next to what it added, checked like any other
setting.
## Kits people and their AI make
A kit is data, so the same shape can be saved from a space: "save my party page as a kit" turns
those pages and everything they point at into a kit, kept private until a person taps Publish and
an admin accepts it. Others then add it next to what they have, and can take it out again
([Templates and kits people make](https://docs.zvessa.com/dev/templates.html#templates-and-kits-people-make)).
The AI can also put together kits nobody has thought of from the same engines.
# Engines
Engines are the only server code a feature needs. Each is abstract (it knows records, members,
rounds, balances, never "orders" or "VIP"), so every kit is settings on top of one or more of
them. A new engine is added only when no engine can express a need. Ids and status:
`ENGINES` in `convex/lib/kits.ts` and the [Catalog](https://docs.zvessa.com/reference/catalog.html#engines).
Every engine follows the same pattern:
- **Shapes in the blueprint, live data in tables.** The blueprint says what exists (a collection,
a stream, a game, a unit); tables hold what people do with it (records, posts, moves, entries).
- **One permission check.** Every function goes through `getAccess` / `mustAccess` and checks a
verb ([People](https://docs.zvessa.com/dev/people.html)). The server reads block settings from the saved blueprint, never
from the device.
- **Live by default.** Convex queries update every open screen when data changes; no polling.
- **Bounded.** Every query reads through an index and takes a limit.
| Engine | Status | What it is | Deep doc |
| --- | --- | --- | --- |
| [Collections](https://docs.zvessa.com/dev/engines.html#collections) | Ready | Any records with fields the space defines | [Data](https://docs.zvessa.com/dev/data.html) |
| [Spaces](https://docs.zvessa.com/dev/engines.html#spaces) | Ready | Members, friends, every kind of conversation, feeds | `convex/kits/talk/README.md`, [docs/SOCIAL.md](https://docs.zvessa.com/more/social.html) |
| [Structure](https://docs.zvessa.com/dev/engines.html#structure) | Ready | Units of any kind (teams, classes, branches), levels and badges | `convex/kits/structure/README.md` |
| [Ledger](https://docs.zvessa.com/dev/engines.html#ledger) | Ready | Balances in any unit and moves between them | `convex/kits/ledger/README.md` |
| [Rounds](https://docs.zvessa.com/dev/engines.html#rounds) | Ready | Live games and quick rounds | `convex/lib/rounds/` |
| [Rooms](https://docs.zvessa.com/dev/engines.html#rooms) | Ready | Calls, group calls, live streams (LiveKit) | [docs/LIVE.md](https://docs.zvessa.com/more/live.html) |
| [Files](https://docs.zvessa.com/dev/engines.html#files) | Ready | The space's media library | [docs/LIVE.md](https://docs.zvessa.com/more/live.html) |
| [Screens](https://docs.zvessa.com/dev/engines.html#screens) | Ready | A TV or big screen joins and shows pages | [docs/LIVE.md](https://docs.zvessa.com/more/live.html) |
| [Time](https://docs.zvessa.com/dev/engines.html#time) | Ready | Bookings and posts scheduled for later | |
| [Events](https://docs.zvessa.com/dev/engines.html#events) | Ready | Counts of what happens, for analytics and leaderboards | [Data](https://docs.zvessa.com/dev/data.html#events) |
| [Apps](https://docs.zvessa.com/dev/engines.html#apps) | Ready | Sandboxed mini-apps | [Mini-apps](https://docs.zvessa.com/dev/miniapps.html) |
| [Capabilities](https://docs.zvessa.com/dev/engines.html#capabilities) | Ready | MCP server, key vault, outside connections | [docs/CAPABILITIES.md](https://docs.zvessa.com/more/capabilities.html) |
| [Audiences](https://docs.zvessa.com/dev/engines.html#audiences) | Ready | Kids spaces with safe rules | [People](https://docs.zvessa.com/dev/people.html#kids-spaces) |
| [Lifecycle](https://docs.zvessa.com/dev/engines.html#lifecycle) | Ready | Drafts, sending later, and posts held for approval | `convex/kits/talk/README.md` |
## Collections
Any records with fields the space defines: forms, contacts, boards, catalogs, notes. Blueprint
`collections`; table `records`; functions `records.*`; blocks `records.list`, `records.form`,
`records.count`, and `split` (totals a money field and settles up who owes whom).
## Spaces
A place with members, roles and conversation. Members, invites, join requests and friends live in
`members.*` and `friends.*`. **Every conversation is one engine** (`convex/kits/talk`), keyed by
a conversation key, so a new kind of conversation is a setting, not new code:
| Key | Is |
| --- | --- |
| `` | A stream from `blueprint.streams`: mode `room` (everyone talks), `broadcast` (a channel: few post, many follow) or `queue` (each person opens a private case; a team answers) |
| `dm::` | A direct chat; it follows the two people across spaces |
| `g:` | A group people made themselves |
| `c:` | One person's case in a queue (support request, application, order) |
A stream's settings say who sees, posts, moderates and answers it (`seeRoles`, `postRoles`,
`modRoles`, `teamRoles`), whether only one unit sees it (`unit`), replies inline or in threads,
reactions, slow mode, roles whose posts wait for review, and a queue's statuses.
`blueprint.talk` turns on direct chats, groups, emoji, read receipts, typing, edits and sending
later. Every word is `words["talk."]`. Blocks `stream`, `inbox`, `direct`, `queue`, `ask`,
`drafts`, `members`, `invite`; full screen at `/c/`. Calls can attach to any conversation.
Kits: group chat, channels, direct chat, teamspace, support desk, drafts and approvals.
**Feeds** (`blueprint.feeds`, `convex/kits/social`): a named place where people publish posts
with words, photos and videos, and reply, react, repost, quote, save and follow. Every behaviour
is a feed setting (replies off, flat or nested; media off, optional or required; length; titles;
star ratings; reactions; posts that expire; approval; outside sources from X, Bluesky, Mastodon,
Instagram, Threads, YouTube and RSS). The same code makes posts and threads, a photo feed,
gallery, stories, a forum, a guestbook and outside feeds. Blocks `feed.*`; screens
`/post/`, `/u/`, `/tag/`. Detail: [docs/SOCIAL.md](https://docs.zvessa.com/more/social.html).
### Reviews
**Review boards** (`blueprint.reviews`, `convex/kits/reviews`): star
ratings with words and photos about anything (services, products, places, games, mini-apps,
people, a collection's records). A board is a setting: `scale` (3 to 10 stars), `icon` (★, ❤️,
🔥 or an icon name), `subjects` or `subjectsFrom` a collection, up to 6 `aspects` rated on their
own, `writers` and `verified` (role ids, `member`, `level:`, `badge:`, and `booked` for people with
a booking), `managers` (may the owner review their own space), `text`, length, `titles`,
`photos` (0 to 10), `approval`, `replyRoles`, `helpful`, `reports` with `hideAfter`, `perDay`,
`links` and `edit`. Each person writes one review per subject; a subject is a listed id,
`record:`, `main`, or any short id a visible block names (`app:weather`, `game:trivia`,
`@handle`), so putting `reviews.stars` with a new subject on a page makes that thing reviewable.
Words are `words["reviews."]`. A summary reads one totals row however many reviews there are;
lists load a page at a time. Detail and recipes: [docs/REVIEWS.md](https://docs.zvessa.com/more/reviews.html).
```json
"reviews": {
"reviews": {
"name": "Reviews",
"subjects": [{ "id": "cut", "name": "Haircut" }, { "id": "colour", "name": "Colour" }],
"aspects": [{ "id": "value", "name": "Value" }, { "id": "friendly", "name": "Friendly" }],
"writers": ["member", "visitor"],
"verified": ["booked", "badge:verified"],
"managers": false,
"photos": 4
}
}
```
## Structure
Any shape a group of people has: teams and departments, years and classes, squads, branches,
ranks. `blueprint.structure` holds `kinds` (what units are called and what their leads are
called: "Class" with "Teacher") and `units` (a tree up to 8 deep, up to 500 units, open to join
or placed by leads). Who is where, with a title and a lead mark, is live data. Managers change
units; a unit's leads (and leads above it) place people; anyone joins or leaves an open unit. A
stream with `unit` set is a room only that unit and those under it see. Block `units`; functions
`kits.structure.structure.*`. Kit: teams and hierarchy.
Levels (tiers, VIP, memberships) and badges (verified) are kept with the Ledger
(`blueprint.ledger`). `visibleTo` can name `level:`, `level:+` or `badge:`. Blocks
`ledger.levels`, `ledger.marks`, `ledger.grants`. Kits: tiers, VIP, verified, memberships.
## Ledger
Balances in any unit (coins, points, credits) and moves between people, the space's purse
(`@space`) and issuing (`@issue`). One function moves units; amounts are whole steps; balances
never go below zero; every move is an entry that is never edited. A unit's `issue` permission
names a verb from the space's roles. Blocks `ledger.balance`, `send`, `give`, `history`,
`leaderboard`. Other engines pay prizes and charge fees through it (`award`, `charge`). Kits:
coins, leaderboards. **Card payments (Stripe)** are designed but not built yet; levels and units
can be bought with the space's own units today.
## Rounds
Live sessions with players, teams, turns, a timer, randomness, a result and a prize. Every game is
a definition in `blueprint.games`, built from nine mechanics:
| Mechanic | Is |
| --- | --- |
| `ask` | Everyone answers the same prompt: a choice, a typed answer or a number |
| `buzz` | Players race to buzz, the host judges; optionally a board of categories |
| `survey` | Hidden ranked answers a team uncovers, with strikes and a steal |
| `act` | One player sees a secret prompt and acts, draws or describes it |
| `spin` | A random pick from options, players or a deck |
| `draw` | Everyone draws a card; highest or lowest wins |
| `pass` | Pass a thing around until a hidden fuse runs out |
| `assign` | Each player privately gets a person or a role |
| `bingo` | Called items are marked on each grid; a full line wins |
People start a game, join with a code (visitors too) and play on their phones; `game.screen` and
the Screens engine show the big view. Randomness, timers and scores are decided **on the server**,
so everyone sees the same result and nobody can cheat; the big screen never shows secret words,
hidden answers or gift names. Quick rounds (`poll`, `wheel`) need no session. Functions
`rounds.sessions.*`, `rounds.quick.*`. Kits: polls, wheel, Hot Potato, High Card,
trivia, quiz board, survey showdown, charades, party pack, game maker, group plans, Secret Santa.
## Rooms
Live audio and video on self-hosted **LiveKit**. A `room` block is a group call (mode `talk`) or
a stage with hosts and an audience (mode `stage`), with screen share and recording; a `call`
block rings people, including a closed phone by push. Convex makes room tokens; media never
passes through Convex. Needs LiveKit set up on the server and a development build on phones.
## Files
The space's media library: photos, videos, audio and files. Small files go to Convex storage, big
ones to Cloudflare R2 when it's set up. Functions `media.*`; blocks `media.gallery`,
`media.player`. Other kits point at library items by id (a post's photos, a profile picture).
## Screens
A TV, projector or any browser opens `/tv`, shows a code, and someone with the `pair`
permission types it into a `screen.pair` block. The screen joins the space as its own member with
the block's role and shows a page; `screen.remote` switches what each screen shows. Because the
screen is a member, every block works on it, live.
## Time
Bookings (`booking` block: opening days and hours in the owner's time zone, minutes per booking;
functions `bookings.*`) and posts scheduled to go out later in a chat, channel or feed
(`scheduled` block; `streams.schedule`, checked again when it goes out). Kits: appointments,
scheduler.
## Events
Daily counts of what happens in a space: page views, posts, records, joins, plays, and custom
events from blocks. Functions `events.track`, `events.summary`, `events.leaders`; block
`analytics` (managers only); leaderboards rank by an event. [Data](https://docs.zvessa.com/dev/data.html#events).
## Automations
Bots and rules that run when something happens, on a schedule, by webhook or by hand, with
approval rules and safety caps. Blueprint `bots` and `automations`; blocks `bots.*`. Detail:
[AI](https://docs.zvessa.com/dev/ai.html#bots-and-automations) and [docs/BOTS.md](https://docs.zvessa.com/more/bots.html).
## Apps
Sandboxed mini-apps: games, calculators, anything blocks can't express. Blueprint `apps`; block
`miniapp`. [Mini-apps](https://docs.zvessa.com/dev/miniapps.html).
## Capabilities
The space reaches outside Zvessa, and anything outside can build it: an **MCP server** at
`/mcp` with access tokens, a **key vault** (AES-256-GCM; keys scoped to hosts; the AI sees names,
never values), and **connections** to outside MCP servers and HTTPS APIs in
`blueprint.connections`. Kits: connections, web research. [AI](https://docs.zvessa.com/dev/ai.html#capabilities) and
[docs/CAPABILITIES.md](https://docs.zvessa.com/more/capabilities.html).
## Audiences
`blueprint.audience: "kids"` keeps a space private and invite-only, out of Explore, and tells the
AI to keep everything kid-safe. Kit: kids space.
## Lifecycle
Drafts, posts scheduled to go out later (checked again when they go out; ones that can no longer
go out come back as drafts), and posts from chosen roles held until a moderator approves them.
Blocks `drafts`, `scheduled`. Kits: drafts and approvals, scheduler, support desk. Still coming
on it: moderation (reports, blocks, word filters).
## Settings by example
Every engine is driven by a blueprint field. These are small, valid examples to copy; the deep
docs list every option.
**Streams and talk** (`blueprint.streams`, `blueprint.talk`; a missing part of `talk` is off; all
options in `convex/kits/talk/README.md`):
```json
"streams": [
{ "id": "news", "name": "News", "icon": "📣", "mode": "broadcast", "postRoles": ["owner", "coach"], "replies": "threads" },
{ "id": "under-12", "name": "Under 12s", "unit": "u12", "slow": 10, "review": ["player"] },
{ "id": "help", "name": "Help", "mode": "queue", "teamRoles": ["staff"], "statuses": ["New", "Working", "Done"], "open": "anyone" }
],
"talk": { "direct": {}, "groups": { "who": ["owner", "coach", "player"], "max": 20 }, "emoji": ["👍", "❤️", "😂"],
"receipts": true, "typing": true, "edits": true, "schedule": true }
```
**A feed** (`blueprint.feeds`; options in [docs/SOCIAL.md](https://docs.zvessa.com/more/social.html#feed-settings)):
```json
"feeds": [{ "id": "reviews", "name": "Reviews", "replies": "flat", "media": "optional", "maxMedia": 4,
"maxLength": 1000, "rating": true, "reactions": ["❤️", "😂"], "approval": true }]
```
**Structure** (`blueprint.structure`):
```json
"structure": {
"kinds": [{ "id": "team", "name": "Team", "plural": "Teams", "lead": "Coach" }],
"units": [
{ "id": "club", "name": "Riverside FC" },
{ "id": "u12", "name": "Under 12s", "kind": "team", "parent": "club", "join": "open" }
]
}
```
**Ledger** (`blueprint.ledger`: units, levels, badges; rules in `convex/kits/ledger/README.md`):
```json
"ledger": {
"units": [{ "id": "coins", "name": "coin", "plural": "coins", "symbol": "🪙", "transfers": true, "startWith": 10 }],
"levels": [{ "id": "gold", "name": "Gold", "rank": 2, "mark": "💎", "color": "gold",
"perks": ["Members-only page"], "price": { "unit": "coins", "amount": 100, "every": "month" } }],
"badges": [{ "id": "verified", "name": "Verified", "mark": "✓", "grant": "manage" }]
}
```
A unit's `issue` and a level's or badge's `grant` name a permission verb (`manage` by default;
only `manage`, `edit` or `invite` are accepted). `decimals` (up to 4) makes amounts behave like
money. Welcome units (`startWith`) can be spent in the space but not sent.
**A game** (`blueprint.games`; every field in `convex/lib/rounds/types.ts`):
```json
"games": [{
"id": "pub-quiz", "name": "Pub quiz", "icon": "🧠", "players": { "min": 2 }, "teams": { "count": 2 },
"decks": [{ "id": "q", "name": "Questions", "items": [
{ "id": "q1", "prompt": "Largest planet?", "choices": ["Mars", "Jupiter", "Venus"], "correct": 1 },
{ "id": "q2", "prompt": "Capital of Japan?", "answer": "Tokyo" }
] }],
"rounds": [
{ "id": "r1", "mechanic": "ask", "deck": "q", "answer": "choice", "score": "correct", "timer": 20, "points": 10 }
],
"prize": { "text": "Bragging rights", "coins": 50 }
}]
```
Show it with `{ "kind": "game", "props": { "game": "pub-quiz" } }` and on a TV with
`game.screen`. Each mechanic reads its own options (`board` for a quiz board, `strikes` for a
survey, `sketch` for drawing, `fuse` for pass, `assign` and `avoid` for gift draws, `grid` for
bingo).
## Adding an engine
Only when no engine can express the need. An engine brings: its tables (with indexes for every
query) spread into `convex/schema.ts`, one Convex file of functions that all check a verb, its
blueprint field with a check in `checkBlueprint` and a default in `upgradeBlueprint`, its blocks
([Blocks](https://docs.zvessa.com/dev/blocks.html#adding-a-block)), an entry in `ENGINES`, its first kits, and tests
(`npm test`). Then regenerate the catalog.
# Templates
A template is a ready-made starting point: a blueprint (pages, kits, roles, a look) that a new
space starts from. Like everything else it is data, so after it's applied it's an ordinary space:
rename, restyle, add or remove kits, and undo back to before it.
## Purposes in onboarding (ready)
After sign-up, the person picks what the space is for. Each purpose names the space and ticks
the kits it suggests; the person can untick any of them, and kits that aren't ready yet are
skipped, so new kits show up here as they ship. Code: `PURPOSES` in `convex/lib/onboarding.ts`;
it's applied by `blueprint.onboard` as one version, so the first undo point is the finished
setup.
| Purpose | Space name | Kits it suggests |
| --- | --- | --- |
| Just me (`personal`) | My space | Boards and tasks, Gallery |
| A business (`business`) | My business | Catalog, Forms, Contacts, Appointments |
| A team or org (`team`) | Our team | Teamspace, Channels, Direct chat |
| Family and friends (`family`) | Our family | Group chat, Group plans, Polls |
| Kids (`kids`) | Kids' corner | Kids space |
| A creator (`creator`) | My studio | Posts and threads, Photo feed, Gallery, Profile and followers, Web research |
| A community (`community`) | Our community | Channels, Posts and threads, Forum, Direct chat |
| Games and parties (`games`) | Game night | Group chat, Trivia night, Polls, Wheel, Show on TV |
| Something else (`other`) | My space | None: start blank and build with the AI |
Each purpose also sets the AI chat's first hint ("Try: make a services page with prices and a
booking form").
Adding a purpose: one entry in `PURPOSES` with `id`, `label`, `about`, `spaceName`, `kits` (kit
ids from the [Catalog](https://docs.zvessa.com/reference/catalog.html#kits)) and `welcome`. Keep it abstract: a purpose is a set of
kits and words, never new code.
## Business and Personal starters (ready)
Full detail: [docs/TEMPLATES.md](https://docs.zvessa.com/more/templates.html). Onboarding now opens with **"How do you want
to start?"**: Business starter, Personal starter, or Build my own (the purposes above). A starter
skips the purpose, look and kit steps.
| | Business starter (`business`) | Personal starter (`personal`) |
| --- | --- | --- |
| Feels like | The Facia chat app: one chat list, no tab bar | A soft, personal home |
| Style | Messenger (built on Classic, light and dark) | Soft (cream and rose, rounded) |
| Navigation | `dashboard.nav: "list"`: home is the chat list, and HQ, AI, Calls, Games, Services and Apps are rows in it | Tabs: Home, Chats, Calls, Plans, Photos, with HQ and AI in the top bar |
| The open chat | The `chat-screen` page (`talk.screen`): a `conversation` with the chat colour behind it and a round send button | The standard chat screen |
| Kits | Direct chat, Group chat, Channels, Support desk, Calls, Trivia night, Quiz board, Survey showdown, Charades, Party pack, Game maker, Polls, Wheel, Show on TV, Coins, Leaderboards, VIP, Verified, Appointments, Analytics | Direct chat, Group chat, Calls, Gallery, Guestbook, Group plans, Polls, Analytics, plus its own `todo` and `journal` collections |
| HQ | KPIs over 7, 14 or 30 days, quick actions, proposals, analytics, four leaderboards, members, access, pages, library, styles, starters, history | Overview, quick actions, analytics, friends and family, invite, access, pages, starters, history |
House party games only: no slots or casino games. A kit that isn't ready is skipped.
Business also adds the Reviews kit. Its Services page gets a star line
under the header (hidden until the first review) and "What customers say" (a `reviews.summary`
and a `reviews.list`, 3 at a time) before Support ([review boards](https://docs.zvessa.com/dev/engines.html#reviews)).
**Applying one**
| Where | How |
| --- | --- |
| A new space | `blueprint.onboard({ name, space: { …, template: "business" } })` |
| An existing space | HQ's `hq.starters` block, or `kits.templates.templates.apply({ template })` (`manage`); one version, so it can be undone |
| The AI | Has no apply tool; it proposes the same parts itself or points people to HQ → Settings → Starters |
**Editing in place.** Every page has an **Edit** button for managers that turns the page into its
own editor: rename it, move, duplicate or remove parts, tap a header, tile or service to change
it, open a part's settings as a sheet (built from its block description, including who sees it),
add parts, then Save as one version or Cancel.
**The look, part by part.** Glossy game tiles with a red LIVE pill on what's on now; quick-play
and app tiles as soft icons with a status line ("6 to play", "Vote now"); a services header card
with the logo, a small-capitals line and Message us / Book; "This week" banners you swipe
sideways; section titles in small capitals over cards. All of it is block props, listed below.
**Native on phones.** System tab bar (expo-router native tabs) with SF Symbols and Material
icons and an unread badge; pages, chats and the AI slide in on a native stack and swipe back;
large titles; action sheets and page sheets; haptics from the style's `motion.haptics`; lists
virtualised and kit renderers loaded lazily. The web on a phone gets the same tabs drawn in the
style; wide screens get a sidebar.
**What it adds to the system** (for every space, not only starters):
- Blocks `hero`, `tiles` / `tile`, `links` / `link`, `chats`, `calls.recent`, `hq.overview`,
`hq.starters` ([Blocks](https://docs.zvessa.com/dev/blocks.html#the-blocks-by-purpose)).
- `Page.icon`: an icon name from `src/icons/icons.json`, used as the tab icon.
- `blueprint.dashboard.panes` is the app's pages in order, and `blueprint.dashboard.nav` picks how
they show: `"tabs"` (default; on phones the first five panes are tabs, the rest top-bar
buttons) or `"list"` (the chat app's way: no tab bar, home is the chat list, panes are rows in
it). The owner switches it in HQ → Settings → Navigation (`hq.navigation`), or asks the AI.
- `blueprint.talk.screen`: a page id. That page is the full-screen chat for every conversation
opened; put a `conversation` block on it (plus anything around it: rules, a pinned card, a poll)
to design every chat screen at once.
- Block options: `hero` `look` (band or card) and `eyebrow`; `tiles` `tone` (solid, soft,
glossy), `frame: "card"`, `heading: "label"`, a tile's `note` as a status line and
`badge: "Live"` as a LIVE pill; `links` `layout` (list, cards, grid, icons, banners), `heading`,
`tone` and a link `tint`; `chats` `search` (header, field, off), `filters`, `compose`, `rowsTop`
and `rowsUnder` (page ids or `ai` shown as rows); `conversation` `background` (a colour role
such as `chatBg`) and `send` (`text` or `icon`).
- Links (`href`) also take `chat:`, `ai`, `mailto:` and `tel:`. A link to a page
that doesn't exist is dimmed for managers and hidden for others, so removing a kit never breaks
a page.
- Functions `kits.templates.templates.apply`, `.overview({ days })` (HQ numbers, `manage`) and
`.recentCalls()`.
**Making another template.** Add a `TemplateDef` in `convex/lib/templates/` (shape in
`types.ts`) and list it in `TEMPLATES` in `index.ts`. `tests/templates.test.ts` checks every
template makes a valid space, its tabs have icons, its links resolve, every kit can still be
removed, switching twice is stable, and it uses only cataloged blocks and props, so the AI
can build the same starter, or a new one, from scratch.
## Feeds starter (ready)
A social app (Instagram, Threads, X), built only from general blocks the AI also uses, so anyone
can ask the AI for the same thing or any part of it.
| | Feeds starter |
| --- | --- |
| Style | Pulse (built on Classic: white by day, true black by night, hot pink and violet, bold type) |
| Phone tabs | Home, Explore, New, Activity, Profile |
| Top bar | Messages, HQ, AI |
| Kits | Posts and threads, Photo feed, Stories, Profile and followers, Gallery, Direct chat, Outside feeds, Polls, Analytics |
- **Home**: a stories row, a box to post, then **For you** (every feed, newest first) and
**Following** (people you follow, with Who to follow) in a switcher.
- **Explore**: search with trending #tags, tiles to Photos, Gallery, From outside and Polls, and
a grid of the week's top photos and videos.
- **New**: composers for Post, Photo and Story (gone in 24 hours) in a switcher, with your own
posts as a grid below.
- **Activity**: Activity (likes, replies, mentions, follows) and Saved.
- **Profile**: photo, @handle, bio, followers, Edit profile, then Posts, Photos and People.
- **Messages**: the chat list. **HQ**: overview, quick actions (Ask the AI, New post, Pull in
feeds, Gallery), analytics, people, invite, access, pages, starters, library, styles, history.
- Everything inside a post (replies and threads, reactions, reposts, quotes, edit, saves,
full-screen media) comes with the feed blocks. Taking out a kit, say Stories, takes its blocks
off these pages too.
## Journal starter (ready)
A private journal (id `journal`) in the Paper style: cream paper and ink, serif headings, sunrise
accents and a night-blue dark mode, with `tint1` to `tint4` as the moods. Every journal page is
`visibleTo: ["owner"]`, so entries stay private on the server too. Code:
`convex/lib/templates/journal.ts`; built from the Journal kit and the
[record views](https://docs.zvessa.com/dev/data.html#record-views).
| Tab | Shows |
| --- | --- |
| Today | A header, the streak, "How are you feeling?" with four moods, today's prompt, on this day, tiles to Entries, Calendar, Photos and "Reflect with AI" |
| Entries | The searchable timeline |
| Calendar | The mood calendar and a 30-day streak goal |
| Photos | The gallery feed, owner only |
| Insights | The last 30 days, this year, and a 100-day goal |
HQ and the AI are in the top bar. The Journal kit on its own (group Plan) adds the `journal`
collection (day, mood, title, entry, tags, prompt) and two owner-only pages: Journal and Mood
calendar.
## Photo book starter (ready)
Id `photobook`, in the Album style: warm paper and ink, serif titles, terracotta and sunflower,
with charcoal and amber at night. The tabs are Book (the book that fills itself, then tiles for
Add photos, Albums, Guestbook and New book), Photos (the Gallery grid), Albums (the shelf), Chats
and Guestbook. HQ and the AI are in the top bar. Its kits are Photo book (with the Gallery),
Guestbook, Direct chat, Group chat and Analytics. Code is in `convex/lib/templates/photobook.ts`,
and the books themselves are covered in [Blueprint](https://docs.zvessa.com/dev/blueprint.html#photo-books).
## Picker covers (`showcase`)
A template can carry `showcase: { tagline, gradient: [colour, colour], stickers: [emoji…],
shots: [{ icon, title, note, tint }] }`. The picker (onboarding and HQ → Settings → Starters) draws a bright
gradient cover with floating emoji, the tagline and a tilted phone showing the shots. They're
plain views, with no images to download. The emoji move only on the picked card, only while the
app is in front, and only if the person allows motion. Without `showcase` the picker shows the
plain card. `checkShowcase` keeps it in limits: a 60-character tagline, 6 stickers and 3 shots.
## Templates and kits people make
Any space can be saved as a **template** (the whole space: pages, tabs, look,
words and everything they use), and any pages as a **kit** (those pages plus the collections,
conversations, feeds, books, games, mini-apps, styles, connections, roles and ledger entries they
point at). Both are plain blueprint data, so whatever is added becomes the owner's to change. Kit
`templates-library` ("Templates and kits", group Build). Code: `convex/kits/sharing/`. Full detail:
[docs/SHARING.md](https://docs.zvessa.com/more/sharing.html).
| Step | Who | What happens |
| --- | --- | --- |
| Save | A person (`share.save`) or the AI (`save_template`, with `preview`) | Makes the piece, takes private things out, checks it makes a valid space, keeps it **private** to its author, who can add it to any space they manage |
| Publish | Only a person's tap | The version waits for a Zvessa admin |
| Review | Admins (`share.review`) | Accept, reject with a reason, take down, restore, handle reports |
| Add | Anyone (`share.library`) or the AI (`add_template`, a proposal a person approves) | `mode: "add"` puts its pages and parts next to what's there; `mode: "switch"` (templates) also brings tabs, look, words, chat settings and org kinds. One version either way, so it can be undone |
| Take out | Managers (`share.mine`) or the AI (`remove_template`) | Removes what an add added (records stay), using `blueprint.fromLibrary` |
**When ids clash** they get new ones (`shop` becomes `shop-2`) and every reference in the piece
follows (`collection`, `#page`, `level:vip`, `records.add:orders`). Identical parts and the space's
own roles, units, levels and badges are reused, built-in kits it uses are added first, and pages
that were tabs become tabs. Nothing is removed by a switch; a kids template makes the space a
private kids space.
**What's taken out before saving:**
- Keys and tokens.
- Emails and phone numbers, which are replaced with you@example.com and 555-0100.
- Links that carry a sign-in, key or signature.
- Links to this server's uploads or to private networks.
- Outside-feed accounts, photos in books, and connection headers.
Connections keep only their vault key name, so whoever adds one saves their own key. A block that
can't work without what was removed is left out. The person sees the full list before saving, and
admins see it while reviewing. Mini-apps from the mini-app library travel as a reference. Identity,
access and members never travel.
**Ratings and kids:** a piece can't be rated younger than its mini-apps; kids spaces only see and
add pieces rated for everyone, and anything saved from a kids space is rated for everyone.
Not yet: onboarding doesn't offer library templates. Start from a starter, then switch to one.
# Mini-apps
A mini-app is custom logic that blocks can't express: a game, a calculator, a drawing board, a
3D scene. It's **one self-contained HTML document** in `blueprint.apps`, shown by a `miniapp`
block. The AI writes them on request; developers can too.
```json
"apps": [{ "id": "snake", "name": "Snake", "html": "" }],
"blocks": [{ "id": "snake-game", "kind": "miniapp", "props": { "app": "snake", "height": 420 } }]
```
## The block's settings
A `miniapp` block's `config` prop (plain JSON, up to 16 KB) reaches the app as
`window.zvessa.config`, so one app placed twice can behave twice differently. `config.connections`
names the space [connections](https://docs.zvessa.com/dev/ai.html#capabilities) the app may read through the bridge's
`connections.call`, read-only (MCP tools the server marks read-only, HTTP `GET`). Big data
belongs in a [collection](https://docs.zvessa.com/dev/data.html), not in config.
## Starter mini-apps
Ready-made mini-apps people add from HQ → Settings → Library, each a kit with the same
id (group Tools). Code: `convex/lib/miniapps/`, built from `miniapps/*.html` with
`npm run miniapps`.
| Kit | What it is | Settings (`config`) |
| --- | --- | --- |
| `calculator` | Calculator, keyboard too | `decimals` |
| `drawing` | Drawing board with the style's colours, undo, eraser | `colors`, `background` (plain, grid, dots), `shared` |
| `timer` | Countdown timer with presets; can run cycles | `minutes`, `presets`, `cycle` ([{ label, minutes }]), `rounds`, `sound` |
| `focus` | The timer set to 25 min focus / 5 min break, four rounds | (as timer) |
| `stopwatch` | Stopwatch with laps | |
| `scorekeeper` | Scores for any game | `players`, `steps`, `start`, `target`, `lowWins`, `title`, `shared` |
| `points` | Points for members (Ledger unit `points`, weekly and all-time rankings, give points) | (blocks) |
| `dice` | Dice, coin, random number | `modes`, `dice`, `sides`, `min`, `max` |
| `converter` | Unit converter (8 kinds) | `categories`, `category` |
| `split` | Tip and bill splitter | `tips`, `tip`, `people`, `currency` |
| `countdown` | Days, hours and minutes to a date | `date`, `title`, `editable` |
| `flashcards` | Flip cards, again or got it, edit the deck | `cards` ([[front, back]]), `editable` |
| `metronome` | Metronome with accent and tap tempo | `bpm`, `beats` |
| `habits` | Habit tracker with streaks | `habits`, `days` |
| `colors` | Colour picker: HEX, RGB, HSL, shades, the space's colours | `color` |
| `qr` | QR codes for links, text or Wi-Fi | `text` |
| `puzzles` | Tic-tac-toe, sliding puzzle, memory match | `games`, `size`, `symbols` |
| `compass` | Compass that can point out a direction | `mark` ({ label, bearing }), `hz` |
| `weather` | Weather now, by hour and for the week (Open-Meteo, free) | `place`, `lat`, `lon`, `units`, `refresh`, `connections` |
| `liveinfo` | Live information from any MCP server or API of the space, with an AI summary | `connection`, `tool`, `arguments`, `path`, `query`, `items`, `fields`, `limit`, `summarize`, `refresh`, `title` |
| `toolbox` | Pocket tools: calculator, converter, splitter, timer, stopwatch | |
Every starter app also takes `words` (change any word it shows) and `save` (a name, so two
copies keep separate saved state). Weather and Live info need the bridge; the compass needs it
on phones.
**Reuse, don't copy.** Once an app is in `/apps`, place more `miniapp` blocks pointing at it with
another `config`: a second countdown, a golf scorekeeper (`lowWins: true`). Kits share one copy
too (Focus uses the Timer's). To **remix**, read the app's `html` from `/apps`, change it, and
add it under a new id.
```json
{ "id": "trip", "kind": "miniapp", "props": { "app": "countdown", "height": 320,
"config": { "date": "2027-06-01", "title": "Lisbon trip", "words": { "days": "sleeps" } } } }
```
## The sandbox
Nothing anyone makes runs as code inside Zvessa itself. A mini-app runs:
- **on phones** in a WebView that can't navigate anywhere (`src/components/MiniAppFrame.tsx`);
- **on the web** in an iframe with `sandbox="allow-scripts"` and no same-origin access
(`MiniAppFrame.web.tsx`). `src/components/MiniApp.tsx` is the same on both.
Its content policy (`src/components/miniAppDoc.ts`):
```
default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; media-src data: blob:; font-src data:;
```
So a mini-app **can** run inline scripts and styles and show `data:` and `blob:` images, sounds and
fonts. It **can't** load other scripts or images from the internet, fetch anything, open sockets,
read the app's sign-in, keys or data, or navigate away. (An `https:` image would let a mini-app send
what people type to any server inside the image's address, so those are out.) Everything else goes
through the bridge.
## The style
A mini-app matches every style, including ones made after it, because it gets the resolved style
twice:
- **CSS variables** on `:root`: every colour by name (`--bg`, `--surface`, `--raised`, `--text`,
`--text2`, `--text3`, `--line`, `--accent`, `--accent2`, `--onAccent`, `--success`, `--warn`,
`--danger`, and any named colour such as `--gold`, `--win`), plus `--radius`, `--radius-small`
and `--line-width` (`--line` is the line colour). `color-scheme` is set to light or dark; the body already uses `--bg` and `--text`.
- **`window.zvessa.style`**: `{ id, scheme, dark, colors, shape, surface, type }` for scripts
(canvas games read `colors.accent` and so on).
```html
```
Never write a colour of your own; name one (`var(--felt, var(--surface))`) so styles can set it.
## The bridge
The bridge is the one way a mini-app reaches the space, the phone and the outside world. The
mini-app asks; the host checks, then answers. Code: the list of calls is
`convex/lib/miniapps/bridge.ts` (the AI's guide is built from it), the host is
`src/kits/miniapps/host.tsx`, the server side is `convex/miniapps/`.
```js
const me = await zvessa.call("me");
await zvessa.call("storage.set", { key: "best", value: 42 });
const off = zvessa.on("sensors.compass", (v) => needle(v.heading), { hz: 15 });
if (zvessa.can("location.get")) { /* show the "use my location" button */ }
```
- `zvessa.call(name, args)` answers once (a Promise). `zvessa.on(name, handler, args)` streams
until you call the function it returns.
- A failed call rejects with an `Error` whose `code` is one of: `denied` (not listed in `needs`, the
role can't, or the person said no), `missing` (no such call), `limit` (too many calls; try again
shortly), `unsupported` (this device or browser can't), `conflict` (someone changed it first),
`invalid` (bad arguments), `failed` (the message says why).
- `zvessa.can(name)` says whether this mini-app may use a call here, so it can hide a button
instead of failing. `zvessa.allowed` is the full list.
- `window.zvessa` also has `app` (`{ id, name }`), `config`, `style` and `preview` (true while
someone tries it from the library: saved state lives in memory and space data is off).
- Mini-apps written for the old name keep working: `window.myaibio` is the same object as
`window.zvessa`.
### What a mini-app declares: `needs`
Basic calls (saved state, haptics, sound, share, clipboard copy, open, resize…) need nothing. Every
other call must be listed in the app's `needs`, and a call on a collection names it:
```json
{ "id": "quiz", "name": "Quiz", "html": "…", "needs": ["records.add:scores", "records.list:scores", "ai.ask", "location.get"],
"about": "A pub quiz", "icon": "trophy", "age": "everyone" }
```
Then the host and the server both check, in order: the call exists, it's in `needs`, the viewer's
role allows it (`records.add` needs `add`, a shared `storage.set` needs `play`…), the person has
allowed it on this device when it touches the phone (location, camera, microphone, contacts,
calendar, motion; asked once per app, in the app's own words), and the rate limit (bursts of 60,
30 a second, 12 at a time). The server checks again from the saved blueprint, so a changed page
can't skip a check.
### Outside information
- `connections.call` reads from the space's own [connections](https://docs.zvessa.com/dev/ai.html#capabilities): a read-only MCP
tool, or a `GET` on an HTTP API. Only the connections the block lists in `config.connections` (a
list of ids, or an object such as `{ "forecast": "weather-forecast" }`). Keys stay on the server.
It answers `{ text, json?, status? }`.
- `ai.ask` asks the space's AI (`{ prompt, context?, system?, format: "text" | "json" }`) on the
space's own key.
- A **published** mini-app added to another space uses only that space's connections, the ones its
owner lists in the block's config. Never the author's.
### Every call
**Who and where**
| Call | Does | Args → gives | Needs |
| --- | --- | --- | --- |
| `me` | The viewer | – → { id, name, role, member, can: { view, post, play, add, edit, invite, manage } } | – |
| `space` | The space it runs in | – → { id, name, kids } | – |
| `env` | The device | – → { platform: 'ios'\|'android'\|'web', width, height, language, dark, reduceMotion, preview } | – |
| `members.list` | People in the space | { limit? } → [{ id, name, role }] | in `needs`, role: `view` |
**Saved state**
| Call | Does | Args → gives | Needs |
| --- | --- | --- | --- |
| `storage.get` | Read a saved value. scope 'me' (default) is this person's own; 'space' is shared by everyone | { key, scope? } → { value, version } or null | – |
| `storage.set` | Save a JSON value (up to 64 KB). On scope 'space' it needs the play permission. Pass ifVersion to save only if nobody changed it since you read it (turns in a game); otherwise fails with code 'conflict' | { key, value, scope?, ifVersion? } → { version } | – |
| `storage.delete` | Delete a saved value | { key, scope? } | – |
| `storage.list` | Saved keys | { scope?, prefix? } → [{ key, version, at }] | – |
| on("storage.watch") | Live value of a key: fires now and whenever anyone changes it (shared boards, turn-based games) | { key, scope? } → { value, version } or null | – |
**Space data**
| Call | Does | Args → gives | Needs |
| --- | --- | --- | --- |
| `records.list` | Records of a collection, newest first | { collection, limit? } → [{ id, data, at, mine }] | in `needs`, role: `view` |
| on("records.watch") | Live records of a collection | { collection, limit? } → [{ id, data, at, mine }] | in `needs`, role: `view` |
| `records.add` | Add a record (fields are checked against the collection) | { collection, data } → { id } | in `needs`, role: `add` |
| `records.update` | Change a record: your own with add, anyone's with edit | { collection, id, data } | in `needs`, role: `add` |
| `records.delete` | Delete a record: your own with add, anyone's with edit | { collection, id } | in `needs`, role: `add` |
**Scores and events**
| Call | Does | Args → gives | Needs |
| --- | --- | --- | --- |
| `scores.submit` | Submit a score to a board; each person keeps their best | { board, value, lower? (true when lower is better) } → { best, rank } | in `needs`, role: `play` |
| `scores.top` | A board's best scores | { board, limit?, lower? } → [{ rank, id, name, value, me }] | in `needs`, role: `view` |
| `events.record` | Count something that happened (shows in the space's analytics when that kit is on) | { name } | – |
| `ledger.balance` | The viewer's balances (coins, points…) | – → [{ unit, name, amount }] | in `needs`, role: `view` |
| `ledger.give` | Give units to a member (needs the unit's issue permission, manage by default) | { unit, to, amount, note? } | in `needs` |
| `ledger.send` | Send the viewer's own units to a member, when the unit allows transfers | { unit, to, amount, note? } | in `needs` |
**Together**
| Call | Does | Args → gives | Needs |
| --- | --- | --- | --- |
| `channel.send` | Send a small JSON message (up to 4 KB) to everyone with this mini-app open in the space | { data } | in `needs`, role: `play` |
| on("channel") | Messages others send with channel.send (the last minute) | – → { from: { id, name }, data, at } | in `needs`, role: `view` |
| on("presence") | Who has this mini-app open right now (the viewer counts as here while it's open) | – → [{ id, name }] | in `needs`, role: `view` |
| `notify.members` | Push a notification to members (10 an hour per mini-app) | { title, body?, to?: 'all' \| [memberId], page? } → { sent } (sent on topic `apps.`) | in `needs`, role: `post` |
**Outside information**
| Call | Does | Args → gives | Needs |
| --- | --- | --- | --- |
| `connections.describe` | What a connection offers: an MCP server's tools, or an HTTP API. Only connections the block lists in config.connections (a list of ids, or an object whose values are ids) | { connection } → { id, name, kind, tools? } | in `needs` |
| `connections.call` | Read from a connection: run a read-only MCP tool, or GET an HTTP path. Keys stay on the server. Only connections the block lists in config.connections | { connection, tool?, arguments?, path?, query? } → { text (HTTP answers start with the status line, like "200\n…"), json? (parsed when it's JSON), status? } | in `needs` |
| `ai.ask` | Ask the space's AI. format 'json' answers with JSON. Counts toward the space's AI use | { prompt (or question), context? (text it should use), system?, format?: 'text'\|'json', maxTokens? } → { text, json? } | in `needs` |
**Feel and sound**
| Call | Does | Args → gives | Needs |
| --- | --- | --- | --- |
| `haptics.tap` | A tap the person feels. Follows the style's haptics setting (phones) | { style: 'light'\|'medium'\|'heavy'\|'success'\|'warning'\|'error'\|'select' } | – |
| `audio.play` | Play a sound from a data: URL (inline it). Follows silent mode | { src, volume?, loop? } → { id } | – |
| `audio.stop` | Stop a sound, or all of them | { id? } | – |
| `speech.say` | Read text aloud | { text, language?, rate?, pitch? } | – |
| `speech.stop` | Stop reading aloud | – | – |
| on("speech.listen") | Speech to text while on | { language?, interim? } → { text, final } | in `needs`, person allows (speech) |
| `mic.record` | Record a clip (up to 60 s) | { seconds } → { dataUrl, seconds } | in `needs`, person allows (microphone) |
| on("mic.level") | Live loudness while on (voice games) (phones) | { hz? } → { level: 0..1 } | in `needs`, person allows (microphone) |
**Sensors**
| Call | Does | Args → gives | Needs |
| --- | --- | --- | --- |
| on("sensors.motion") | Tilt and movement | { hz? (default 30, max 60) } → { rotation: { alpha, beta, gamma }, acceleration: { x, y, z } } | in `needs`, person allows (motion) |
| on("sensors.accelerometer") | Acceleration in g | { hz? } → { x, y, z } | in `needs`, person allows (motion) |
| on("sensors.gyroscope") | Rotation rate | { hz? } → { x, y, z } | in `needs`, person allows (motion) |
| on("sensors.magnetometer") | Magnetic field in µT (phones) | { hz? } → { x, y, z } | in `needs`, person allows (motion) |
| on("sensors.barometer") | Air pressure (phones) | { hz? } → { pressure, relativeAltitude? } | in `needs`, person allows (motion) |
| on("sensors.light") | Light level (Android) (phones) | { hz? } → { illuminance } | in `needs`, person allows (motion) |
| on("sensors.pedometer") | Steps since it was turned on (phones) | – → { steps } | in `needs`, person allows (motion) |
| on("sensors.compass") | Which way the phone points | { hz? } → { heading (degrees from north), accuracy } | in `needs`, person allows (location) |
| `location.get` | Where the person is (rounded to about 1 km unless precise) | { precise? } → { lat, lon, accuracy } | in `needs`, person allows (location) |
| on("location.watch") | Where the person is, as they move | { precise?, meters? } → { lat, lon, accuracy, speed?, heading? } | in `needs`, person allows (location) |
**Camera and files**
| Call | Does | Args → gives | Needs |
| --- | --- | --- | --- |
| `camera.take` | Take a photo (or a short video) | { video?, maxSize? (px, default 1280) } → { dataUrl, width, height } or null | in `needs`, person allows (camera) |
| `camera.scan` | Scan a QR code or barcode | – → { type, data } or null | in `needs`, person allows (camera) |
| `media.pick` | Pick photos or videos | { multiple?, kind?: 'image'\|'video'\|'any', maxSize? } → [{ dataUrl, width, height, kind }] | in `needs`, person allows (photos) |
| `files.pick` | Pick a file (up to 5 MB) | { types? (like ['application/pdf']) } → { name, type, size, dataUrl } or null | in `needs`, person allows (files) |
| `files.save` | Save or share a file the mini-app made | { name, data (text or data: URL) } | – |
| `share` | Open the share sheet with text, a link or an image | { text?, url?, title?, image? (data: URL) } | – |
| `clipboard.copy` | Copy text | { text } | – |
| `clipboard.read` | Read copied text | – → { text } | in `needs`, person allows (clipboard) |
**Calendar and contacts**
| Call | Does | Args → gives | Needs |
| --- | --- | --- | --- |
| `calendar.add` | Open the calendar's add-event screen, filled in (phones) | { title, start, end, location?, notes? } (times as ms or ISO) | in `needs`, person allows (calendar) |
| `contacts.pick` | The person picks one contact (phones) | – → { name, phones, emails } or null | in `needs`, person allows (contacts) |
| `notify.me.later` | A reminder on this phone at a time (timers, alarms) (phones) | { at (ms or ISO), title, body? } → { id } | in `needs`, person allows (notifications) |
| `notify.me.cancel` | Cancel a reminder made with notify.me.later (phones) | { id } | – |
**Screen**
| Call | Does | Args → gives | Needs |
| --- | --- | --- | --- |
| `screen.keepAwake` | Keep the screen on while the mini-app shows (games, timers, recipes) | { on } | – |
| `screen.orientation` | Lock the screen while the mini-app shows (phones) | { lock: 'portrait'\|'landscape'\|'any' } | – |
| `screen.brightness` | Brighten the screen while it shows (tickets, QR codes); goes back after (phones) | { level: 0..1 } | – |
| `resize` | Change the mini-app's height | { height (px, 120 to 2000) } | – |
| `open.page` | Open a page of the space | { page } | – |
| `open` | Open a button-style href: "#page-id" opens a page of the space, an https link opens outside (the person confirms) | { href } | – |
| `open.link` | Open an https link (the person confirms) | { url } | – |
Later (needs store setup first): in-app purchases through Apple and Google, and reading steps and
workouts from Health.
### Pausing
When the app is in the background or the mini-app is off screen, the host sends a pause: animation
frames, timers and media stop, events stop arriving, sensors and sounds turn off, and
`myaibio:pause` / `myaibio:resume` fire. Live values (`storage.watch`, `records.watch`, `channel`,
`presence`) catch up on resume.
## The public library
Mini-apps can be shared with everyone. Nothing is public until its owner taps **Publish**.
- **Publish** (people who manage the space, from the **Apps** page: kit `app-library`, blocks
`miniapps.library`, `miniapps.mine`, `miniapps.review`). Pick a category and an age rating
(`everyone`, `teen`, `mature`), write what it does, and send it. The AI can't publish.
- **Review.** Every version waits for a Zvessa admin, who
can try it, then accept or reject with a reason. Only accepted versions can be added anywhere
else. Admins can also take a listing down, which stops it in every space, and put it back.
- **Browse and add.** Anyone signed in can search, read the details and the calls it asks for, and
try it in a preview. **Add to my space** puts it in the space with its own page; **Remix** makes a
copy the space owns and can change. Kids spaces only see and add apps rated `everyone`.
- **Updates.** An added app remembers where it came from. When a newer version is accepted, the
space's managers see **Update**, with any new calls it asks for listed first.
- **Ratings and reports.** One rating per person; reports go to the admins.
- Free: publishing, adding and using cost nothing.
### The library over MCP
Any MCP client can browse the public library: `POST {CONVEX_SITE_URL}/mcp/library` needs no
account or token and offers `search_miniapps` and `view_miniapp` (`source: true` includes the
HTML), plus `search_templates` and `view_template` for templates and kits. It's read-only and lists only accepted pieces. A space's own `/mcp` server and the
in-app AI get the same two tools plus `add_miniapp({ id, remix? })`, which is a proposal a
person approves. Publishing is still only a person's tap in the app.
## Writing a good mini-app
- **One file, no network.** Inline everything; images as `data:` URLs. Outside information comes
through `connections.call`.
- **List what you use.** Put every non-basic call in `needs`, and use `zvessa.can()` to hide what
isn't allowed here.
- **Fit any width.** Phones first: 320 px wide and up, touch first, then mouse and keys.
- **Light on the device.** Draw with `requestAnimationFrame` only while something moves; stop when
the game is paused or over. Ask sensors for the lowest rate that works (30 Hz is enough for
tilt games). More in [Performance](https://docs.zvessa.com/dev/performance.html).
- **Paused when hidden.** When the app is in the background or the mini-app is off
screen, the host calls `window.zvessa.setPaused(true)`: `requestAnimationFrame`, timers and
media stop, and timers never fire faster than every 16 ms. Listen for the `myaibio:pause` and
`myaibio:resume` events (or read `window.zvessa.paused`) to save state or stop sounds.
- **No dialogs or forms.** `alert`, `confirm` and form submits are blocked in the sandbox; use a
second tap to confirm, and a button (plus Enter) to send an input row.
- **No browser storage.** It's blocked in the sandbox. Use the bridge's `storage.get` (answers
`{ value, version }` or `null`) and `storage.set`; `scope: "space"` shares a value with everyone
and `storage.watch` keeps it live. Check `typeof zvessa.call === "function"` so the app still
works outside Zvessa.
- **Small.** Aim for well under 200 KB of HTML; the whole blueprint goes to every device.
- **Readable words.** Put the app's words near the top of the script so people (and the AI) can
change them.
- **Accessible.** Real buttons, labels, enough contrast (use `--text` on `--bg`).
## Planned
Developer apps hosted elsewhere behind the same bridge; in-app purchases and Health once the
store setup is done; Skia and GL for native-speed games.
# The AI, bots and capabilities
## The builder AI (ready)
The AI chat on the dashboard builds the space. It can do anything HQ can: **it proposes, a person
approves (or Auto approval applies it at once), every version can be undone.** Code:
`convex/ai.ts`; how to build, in words for any AI, is `convex/lib/guide.ts`; its tools are
described once in `convex/lib/capabilities/tools.ts` and shared with the MCP server.
1. Someone with `manage` sends a message (`ai.send`).
2. The server sends Claude the build guide (blocks, kits, collections, games, ledger, styles,
rules), the current blueprint and recent messages. The guide part stays the same between
requests, so it's cached and cheap.
3. Claude calls tools. Changes are applied to a copy and run through every
[check](https://docs.zvessa.com/dev/blueprint.html#checks); errors go back to Claude to fix, up to 6 rounds.
4. A valid change is stored as a pending proposal; the person taps Approve in the chat or in HQ.
In [Auto mode](https://docs.zvessa.com/dev/ai.html#auto-approval) it is applied right away instead, saved as a version.
| Tool | Does |
| --- | --- |
| `propose_change({ summary, ops })` | Proposes JSON Patch ops on the blueprint |
| `add_kit` / `remove_kit` | Proposes adding or taking out a kit |
| `undo_change({ version? })` / `list_versions` | Proposes going back to an earlier version (default: before the latest change); the history |
| `list_records` / `add_record` / `update_record` | Reads a collection's records; adds or changes records as the person who asked (deleting stays with people) |
| `list_connections` / `call_connection` | Uses the space's outside services ([Capabilities](https://docs.zvessa.com/dev/ai.html#capabilities)) |
| `list_photos({ feed?, tag?, limit? })` | Reads photos (ref, caption, #tags, shape, date) to build photo books; read-only, `manage` (MCP scope `data.read`) |
| `search_miniapps` / `view_miniapp` / `add_miniapp` | Searches the public mini-app library, reads one (with its HTML if asked), and proposes adding or remixing it ([Mini-apps](https://docs.zvessa.com/dev/miniapps.html#the-library-over-mcp)) |
| `list_reviews` / `answer_review` / `moderate_review` | Reads a board's or subject's reviews (`data.read`), writes the space's public answer, and approves, hides or keeps a review (`data.write`); all need `manage`. No tool writes a review, so an AI or a key can never fake one ([Engines](https://docs.zvessa.com/dev/engines.html#reviews)) |
| `search_docs({ query, in?, limit? })` / `read_doc({ page?, section? })` | Searches and reads the public docs (this site), with links; read-only. Also on the open `/mcp/library` server with no account ([What the AI knows](https://docs.zvessa.com/dev/ai.html#what-the-ai-knows)) |
| `get_ai_usage({ day? })` | Today's AI use by person, the space and bots, against the limits, and whose key pays; `manage` ([AI limits](https://docs.zvessa.com/dev/ai-limits.html#blocks-and-tools)) |
| `search_templates` / `view_template` / `add_template` / `remove_template` / `save_template` | Searches the template and kit library (`mine` for your own), reads one (`source`), proposes adding or taking one out, and saves pages or the space privately (`preview` first). No tool publishes ([Templates](https://docs.zvessa.com/dev/templates.html#templates-and-kits-people-make)) |
| `search({ query, sources?, collection?, stream?, feed?, limit? })` | Searches the space (pages, records, messages, posts, people, files) and says where each result lives; read-only, `data.read`. Over MCP it searches as the token's person; the in-app AI never sees direct chats or private groups ([docs/SEARCH.md](https://docs.zvessa.com/more/search.html)) |
| `list_notes` / `remember_note` / `forget_note` | The caller's AI notes in this space (only where `/ai/remember` is on) ([AI jobs](https://docs.zvessa.com/dev/ai-jobs.html#ai-notes)) |
| `list_ai_jobs` / `ask_space_ai` / `cancel_ai_job` | MCP only: the space's latest AI jobs, hand a request to the space's own AI, stop a job ([AI jobs](https://docs.zvessa.com/dev/ai-jobs.html#tools-mcp-and-the-in-app-ai)) |
| `list_invites` / `create_invite` / `revoke_invite` / `list_referrals` / `recheck_referrals` | Scope `people`: invite links and who invited whom ([People](https://docs.zvessa.com/dev/people.html#invites-and-referrals)) |
| `my_notifications` / `change_my_notifications` / `undo_my_notifications` / `my_inbox` / `send_notification` | Scope `me`: the caller's own notification choices and inbox; `send_notification` (scope `notify`) is managers only, 10 an hour, outside AIs only ([Notifications](https://docs.zvessa.com/dev/notify.html#ai-and-mcp)) |
| `list_reports` / `check_text` / `act_on_report` / `report` / `block_person` / `list_blocked` / `safety_admin` | The review queue, the word filter, acting on a report (new scope `moderate`), reporting and blocking. The in-app AI reads the queue and tries the filter, but only a person moderates ([docs/SAFETY.md](https://docs.zvessa.com/more/safety.html)) |
| `preview_qr` / `make_qr` | What a QR code would hold and how it would look (read), and proposing a designed `qr` block on a page (propose) ([QR codes](https://docs.zvessa.com/more/qr.html)) |
| `get_web` / `check_handle` / `check_web_domain` / `preview_web_page` | The space's web address and pages, whether a handle is free, an own domain's DNS, and what a visitor sees at a path ([docs/WEB.md](https://docs.zvessa.com/more/web.html)) |
| `account_overview` / `account_request_deletion` / `account_confirm_deletion` | Scope `account`: the emailed code is the person's approval; email and password changes stay in the app ([Account](https://docs.zvessa.com/dev/account.html#ai-and-mcp)) |
| `read_conversation` / `post_message` / `notify` / `give` / `run_automation` / `ask_bot` / `list_runs` / `list_waiting` / `decide_action` | Bots and automations: read and post in a conversation, notify, give Ledger units, start an automation, ask a bot, see runs and what's waiting, decide a waiting action ([Bots and automations](https://docs.zvessa.com/dev/ai.html#bots-and-automations)) |
| `web_search` / `web_fetch` | Searches the web and reads pages, only in spaces with the **Web research** kit |
It never approves its own proposals (Auto mode is the owner's choice, below) and never deletes records.
### Auto approval
`/ai/approval = { mode: "ask" | "auto", roles?, mcp?, ask? }` (`convex/lib/ai/approval.ts`,
`convex/kits/approval/auto.ts`). Default `ask`. In `auto`, a proposal from the in-app AI or an MCP
token applies the moment it's made, through the same checks as Approve, saved as a version
("… (applied automatically)"). The chat card says **Applied automatically** with **Undo** while it's
the latest change; after that, HQ → Settings → History. HQ → Settings → AI has the switch (`ai.approval`).
- `roles` (default `["owner"]`): whose requests apply at once; anyone else's still wait.
- `mcp` (default true): the person's MCP tokens too. Bots never auto-apply; they keep their own `/bots` approval.
- `ask`: paths that still wait, default `/access`, `/audience`, `/roles`, `/connections`, `/web`,
`/bots`, `/automations`, `/ai/limits` (their effect reaches past Undo). `/ai/approval` always waits.
- Only the owner turns Auto on or widens it; anyone who manages can switch back to Ask. Kids spaces always ask.
- Things that aren't blueprint changes keep their own confirms: deleting the account (emailed code),
removing members and changing roles (people only, in HQ), and giving coins (its own tool and rules).
Which key pays: each space connects its own Claude key, saved in the vault with use `ai`, from
the `ai.setup` block at the top of HQ or the Connections page. Until then the AI chat asks the
owner to connect one. The space's own limits per person and role still apply
([AI limits](https://docs.zvessa.com/dev/ai-limits.html)).
How it runs: every reply is an **AI job** that streams in, shows a progress line, can be stopped,
and keeps going in the background for long builds; `/ai/model` picks `auto`, `small` (fast) or
`large` (careful), and the AI can keep short notes on how each person likes things built. Detail:
[AI jobs](https://docs.zvessa.com/dev/ai-jobs.html). Keys never reach a device. Safety detail:
[Security](https://docs.zvessa.com/dev/security.html#the-ai).
What it knows about feeds: `FEEDS_GUIDE` (`convex/kits/social/guide.ts`, part of the kits
section of the guide) covers the `/feeds` settings, the feed blocks and the switcher, the
`feed.` words, the [feed recipes](https://docs.zvessa.com/dev/kits.html#feed-recipes), how to add a feed and its page,
and how a whole social app is put together.
What it knows about mini-apps: the guide's "apps" part (`convex/lib/miniapps/guide.ts`, built
from the bridge's call list) covers the `/apps` shape, `needs`, the sandbox and every
[bridge call](https://docs.zvessa.com/dev/miniapps.html#the-bridge). It can write, place and remix mini-apps through proposals,
but it **can't publish** one to the public library; only a manager can, and an admin reviews it.
### What the AI knows
Everything about Zvessa, without looking it up first:
- **How to build** (cached in its instructions): the rules, every block and its props, every kit,
collections, people and roles, styles, games, connections, mini-apps and bots. Built from the code
(`convex/lib/guide.ts`), so it changes when the code does.
- **How people use the app** (cached too): the whole [user guide](https://docs.zvessa.com/guide/start.html), so "how do I
invite someone?" gets the app's own answer.
- **Everything else on this site** on demand: `search_docs` and `read_doc` reach every developer and
deep docs page. The AI links the page it used.
The docs come from the same Markdown as this site through one list of public pages
(`docs/build/sources.mjs`). `node docs/build/knowledge.mjs` (part of `npm run docs`) writes them to
`convex/lib/docs/packData.ts`, and a test fails when that file is older than the docs. Owner-only
setup never goes in.
**Other AIs.** Both MCP servers offer the docs as resources (`zvessa://docs/`, and the
building guide as `zvessa://guide/`) and prompts (`zvessa_build`, `zvessa_help`). The open
library server, `POST {CONVEX_SITE_URL}/mcp/library`, answers `search_docs`, `read_doc` and
`describe_library` with no account. The site has [`llms.txt`](https://docs.zvessa.com/llms.txt) (an index),
[`llms-full.txt`](https://docs.zvessa.com/llms-full.txt) (every page in one file), and a Markdown copy of
each page (swap `.html` for `.md`).
What the AI is told it must do: build abstract, reusable pieces; keep looks in styles; give every
block a unique id; never remove the HQ page; put games in game definitions or mini-apps with no
network calls; keep proposals small; keep kids spaces kid-safe.
### How to ask well
- Say what it's for, not how to build it: "a page where my class signs up for a time slot".
- One thing at a time; approve, then ask for the next.
- Name the look in words ("warm, paper, serif") and let it write the style.
- If a proposal is wrong, say what's wrong; it changes the proposal. Undo is always there.
### Planned for the builder AI
- One tool per engine (pages, data, apps, media, notifications, automations), all still through
propose and approve.
- Analytics totals (never raw personal data) so it can suggest improvements.
- Making images and video (needs a second provider, added as a capability).
## Bots and automations
Bots are AI teammates in the space and automations are rules: when this
happens, if these checks pass, do these steps. Both are blueprint entries (`/bots`,
`/automations`) made by chat, by hand or over MCP, and every change is a proposal a person
approves. Full detail: [docs/BOTS.md](https://docs.zvessa.com/more/bots.html); the engine is `convex/kits/bots/` (its
`README.md`, rules in `model.ts`).
```json
{
"bots": [{ "id": "helper", "name": "Helper", "role": "bot", "instructions": "Answer briefly.",
"tools": ["read_conversation", "post_message", "list_records"], "approval": "ask" }],
"automations": [{ "id": "helper-answers", "name": "Helper answers mentions",
"when": { "event": "post", "mentions": "helper" },
"do": [{ "do": "ask", "bot": "helper", "prompt": "{{person.name}} asked: {{text}}" },
{ "do": "post", "reply": true, "text": "{{last}}", "as": "helper" }] }]
}
```
- **Bot fields:** `id`, `name`, `about`, `icon`, `role`, `instructions`, `tools`, `approval`
(`ask` or `auto`), `askFor`, `approvers`, `model` (`small` or `large`), `perDay`, `on`. The
`builder` entry is the AI chat itself: only its name, instructions and model can be set.
- **Tools:** any tool from the one tool list (the same tools MCP clients and the AI chat use),
except approving. A tool runs as the bot's own account, so its role applies. Tools that change
something follow the approval rules, and proposals always wait for a person. In kids spaces bots
always ask a grown-up.
- **Triggers (`when.event`):** `record`, `record.changed`, `post`, `join`, `booking`, `play`,
`review`, any event a kit reports through `count()`, plus `schedule` (every 15m, 30m, hour, day,
week or month, with `at`, `day` and `timezone`), `webhook` and `manual`. Other keys filter on the
event's fields (`collection`, `feed`, `board`…); `mentions` names a bot; `fromBots` hears bots
too. Kit authors: pass the event's fields as `count()`'s optional `about` argument so automations
can filter on them.
- **Checks (`if`):** `[{ path, is | not | contains | gt | lt | exists }]`.
- **Steps (`do`):** `post`, `add_record`, `update_record`, `notify`, `give` (Ledger units, never
money), `ask` (a bot thinks; its answer is `{{last}}`), `wait`, `call` (a connection), `tool`
(any tool, as a bot). Text is a template: `{{text}}`, `{{person.name}}`, `{{data.x}}`,
`{{last}}`, `{{date}}`, `{{space.name}}`.
- **Approval:** words written into an automation go as written; words a bot wrote follow its
approval; `as: "approver"` makes a draft the approver sends as themselves; `approval: "ask"` on
an automation makes every step wait.
- **Safety limits:** an automation never starts itself again, chains stop after 3 rounds, each bot
has a daily cap (50 by default) and each automation one too (200), and webhooks take 60 a minute
and 16 KB.
- **Webhook:** `POST {CONVEX_SITE_URL}/hooks/` with a JSON body starts an automation; it
answers 202, 404, 409, 413 or 429. The address is made with a `bots.hooks` block and shown once.
Blocks: `bots.list`, `bots.waiting`, `bots.runs`, `bots.run`, `bots.ask`, `bots.hooks`. The
**Bots** kit adds a Bots page; recipes: Helper bot, Welcome message, Reply drafts, Daily digest,
Coins for posts. Bot jobs run on the [AI jobs](https://docs.zvessa.com/dev/ai-jobs.html#for-engines-bots-automations) engine and
count against the space's [AI limits](https://docs.zvessa.com/dev/ai-limits.html).
## Capabilities
Ready. Full detail and setup: [docs/CAPABILITIES.md](https://docs.zvessa.com/more/capabilities.html). People get the screens
with the **Connections** kit (`hq.connections`).
**Build from anywhere.** Zvessa is an MCP server at `{CONVEX_SITE_URL}/mcp`. Any MCP client
(Claude Desktop, Claude Code, Cursor, another app's AI) builds a space with the same tools as the
in-app AI, using an access token made on the Connections page:
```sh
claude mcp add --transport http zvessa https://your-backend/mcp --header "Authorization: Bearer mbk_…"
```
A token belongs to one person in one space and can do what both its scopes (`read`, `propose`,
`approve`, `data.read`, `data.write`, `connections`) and the person's current role allow. New
tokens get `read` + `propose`: the client proposes, a person approves in the app.
**Your own keys.** A space's keys live in a vault on the server, sealed with AES-256-GCM, each
limited to the hosts it may go to. No query returns a value; the AI, bots and mini-apps see a
key's name, never the key.
**Outside services.** `blueprint.connections` lists remote MCP servers (`kind: "mcp"`) and HTTPS
APIs (`kind: "http"`) with the vault key each uses. The AI can propose one; a person approves.
Only read-only tools or `GET` calls run unless the person lists more. Addresses must be public
HTTPS, so a connection can't reach the server's own network. Local (stdio) MCP servers can't run
on the backend; connect the remote version.
Planned: auto-post to social accounts, scheduled jobs that call connections, and the Automations
kit.
# AI jobs
Streaming replies, background builds, model sizes and notes.
Every piece of AI work in a space is an **AI job**: a chat reply, a bot's thought, a request an
outside AI hands over through MCP. Jobs are general, so anything that needs Claude with tools runs
on the same engine (code: `convex/aiJobs.ts`, `convex/lib/ai/`).
## What people see
- **Replies stream in** as the AI writes them. The server writes the growing text a few times a
second (every 250 ms by default), not per word, so phones redraw little and stay cool.
- **A progress line** under the reply says what the AI is doing ("Checking the change…",
"Searching the web…", or the model's own short note between tool calls).
- **Stop** replaces Send while the AI works. It stops between words; proposals already made stay.
- **Messages wait in line.** A second message sent while the AI works shows "Waiting for the AI…"
and runs next.
- **A push when a long build finishes** (over 30 seconds) while the person is away, unless
`/ai/notify` is false. It opens the AI chat.
## How a job runs
```
start → queued | running → one model turn per background step → done | failed | cancelled
```
- Each model turn is its own scheduled action, so a long build never hits the action time limit,
and Stop works between turns as well as between words.
- The job keeps its conversation append-only (thinking blocks included) and snapshots the
blueprint and notes at its first turn, so every turn sends the same prefix and the stable build
guide stays cached.
- Jobs in one **lane** run one at a time: the space's chat is one lane, each bot is its own.
- A cron ends jobs whose step stopped answering for 12 minutes (a crash or deploy mid-turn).
- Tokens per response are counted by the AI limits kit (`_addTokens`), and limits are checked
before a job starts (`takeAiTurn`). See [AI-LIMITS.md](https://docs.zvessa.com/dev/ai-limits.html).
## Model sizes
Spaces never name a model; they pick a size at `/ai/model`:
| Setting | What runs |
| --- | --- |
| `auto` (default) | Small edits and short questions go to the fast model; building, anything new, web research and long asks go to the careful one. |
| `small` | Always the fast model (`claude-haiku-5-5`), low effort. |
| `large` | Always the careful model (`claude-opus-5-5`), medium effort. |
If the fast model gets stuck (it declines, runs out of room, or keeps making invalid changes), the
job starts over once on the careful model. The chat block can also ask for a size per message
(`ai.chat` prop `model`).
## `/ai` settings
All optional, changed like any setting (HQ, `propose_change`, MCP):
```json
{ "ai": { "model": "auto", "stream": true, "remember": false, "notify": true, "maxTurns": 10 } }
```
- `stream`: false shows the reply only when it's done (fewer redraws; the progress line still shows).
- `remember`: the AI keeps notes (below). The **AI helper** kit turns it on.
- `maxTurns`: 1 to 20 model turns per chat job.
- `limits`: belongs to the AI limits kit.
## AI notes
Short notes on how each person likes things built ("Prefers dark looks", "Calls members crew").
One list per person per space, at most 30, each under 200 characters. They go into the AI's
instructions for that person's jobs. Notes are about style: anything that looks like an email, a
phone number, a key or a password is refused, and in kids spaces the AI is told never to note
anything about a child. People read, add and delete their notes on the `ai.notes` block.
## Blocks
| Block | What it is |
| --- | --- |
| `ai.chat` | The builder chat itself (the dashboard's AI pane is this block). Props: `placeholder`, `model`, `steps`. |
| `ai.model` | Model size, streaming and finish pushes (writes `/ai`). |
| `ai.notes` | What the AI remembers about the viewer; add, delete, notes on or off. |
| `ai.jobs` | The space's latest AI work with Stop for running jobs. Props: `title`, `limit`. |
Kit: **AI helper** (`ai-helper`, group Build) adds an owner-only "AI" page with `ai.model`,
`ai.notes` and `ai.jobs`, and sets `/ai/remember` to true.
## Tools (MCP and the in-app AI)
| Tool | Scope | What it does |
| --- | --- | --- |
| `list_notes` | read | The caller's notes in this space. |
| `remember_note` | propose | Keep a note (refused when notes are off). |
| `forget_note` | propose | Remove a note by id or exact text. |
| `list_ai_jobs` | read | Latest jobs: status, step, size, turns, tokens. MCP only. |
| `ask_space_ai` | propose | Hand a request to the space's own AI, as if typed in the chat. MCP only. |
| `cancel_ai_job` | propose | Stop a queued or running job. MCP only. |
The in-app AI gets the note tools only where notes are on.
## For engines (bots, automations)
```ts
const turn = await ctx.runMutation(internal.kits.ailimits.limits.take, { workspaceId, userId: null, source: "bot" });
if (!turn.ok) return; // turn.limit.message says why
const jobId = await ctx.runMutation(internal.aiJobs.start, {
workspaceId, kind: "bot", bot: "greeter", ref: runId,
prompt: "A new member joined: say hi in #lobby",
system: "You greet new members warmly, in one line.",
tools: ["post_message"], // narrows the tool list; approve tools are never given
tier: "small", maxTurns: 6, key: turn.key,
callback: "kits/bots/engine:_thought", // called with { ref, jobId, text, status, proposalIds }
});
```
`internal.aiJobs.cancel({ jobId })` stops it; `internal.aiJobs._get({ jobId })` reads it.
Tools run through `runTool` with `caller.for` (the person) and `caller.bot` set.
# AI limits
How much AI each person and each space may use, so nobody uses up a space's day and the server's
Claude key can't run up a bill. Code: `convex/kits/ailimits/` (rules in `model.ts`, counting in
`limits.ts`). Always on; there is nothing to add.
## Two layers
| Layer | Set by | Where | Applies |
| --- | --- | --- | --- |
| Platform limits | the platform | its own settings | only if the platform offers its own Claude key (off for now) |
| Space limits | the space (owner or their AI) | blueprint `/ai/limits` | on any key |
On the platform's key the tighter of the two wins: a space can lower the platform's limits, never raise
them. With the space's own key (a vault key with use `ai`, on the Connections page) only the
space's limits apply. The per-minute limit for each person always applies.
Days are UTC days. Counters are small rows in `aiUsage` (one per person, person-in-space, space and
bots per day), swept after 35 days.
### Platform limits
| Default | What |
| --- | --- |
| 100 | messages one person may send per day, across every space on the platform's key |
| 300 | messages one space may send per day |
| 200 | runs a space's bots start on their own per day |
| 6 | messages per person per minute, on any key |
| off | tokens one person, or one space, may use per day on the platform's key |
### Space settings: `/ai/limits`
```json
{ "ai": { "limits": {
"personDaily": 20,
"byRole": { "member": 10, "staff": 200 },
"spaceDaily": 500,
"botDaily": 50,
"personDailyTokens": 200000,
"exempt": ["owner"]
} } }
```
Every field is optional; leaving one out means no limit of the space's own, and 0 means none
allowed. `byRole` wins over `personDaily`. `exempt` roles skip the space's limits (default
`["owner"]`), never the server's. Changed like any setting: propose → approve → undo, from the
chat, HQ or MCP (`propose_change` on `/ai/limits`).
## No key yet
Each space connects its own Claude key for now ("+W+"). With no space key and no platform key,
`takeAiTurn` returns `{ ok: false, limit: { code: "ai_no_key", scope: "no-key", message } }` and
nothing is counted. People who manage the space see "Connect your AI to start…" and members see
"Ask the owner…" (words `aiNoKeyManager`, `aiNoKeyMember`). The platform limits only apply if the
platform ever offers its own key.
The `ai.setup` block ("Connect your AI") lets a manager paste the Claude key right there; it hides
once connected. Props `title`, `about`, `hideWhenReady`; words `aiSetupTitle`, `aiSetupAbout`,
`aiSetupReady`, `aiSetupServer`, `aiSetupSave`. It sits at the top of the default HQ and above the
AI chat until the space is connected.
## What counts
- A chat message: one turn for the person who sent it, and one for the space.
- A bot or automation a person started (an @mention): one turn for that person and the space.
- A bot or automation nobody started (a schedule, an event): one bot run and one turn for the space.
- Tokens: input + output + a tenth of cached input, after each Claude response.
- MCP clients run their own AI, so their tool calls don't count here (they have their own 120
calls a minute per token).
## When a limit is hit
`ai.send` returns `{ ok: false, limit }` instead of throwing, and the chat shows `limit.message`
in one line above the composer, keeping what the person typed. `limit` is
`{ code: "ai_limit", scope, message, limit, resetsAt, server }`; `scope` is one of
`person-minute`, `person-day`, `member-day`, `space-day`, `bot-day`, `person-tokens`,
`member-tokens`, `space-tokens`. `server: true` means the space's own key would lift it, so people
who manage the space (not in kids spaces) also get the "add your own key" hint.
Every message is a word the space can change: `aiLimitPerson`, `aiLimitSpace`, `aiLimitBots`,
`aiLimitMinute`, `aiLimitTokens`, `aiLimitOwnKey`, `aiLimitLeft`, `aiLimitNone`, and the HQ
report's `aiUsageTitle`, `aiUsageOwnKey`, `aiUsageServerKey`, `aiUsageSpace`, `aiUsageBots`,
`aiUsageNobody`. `{limit}`, `{left}` and `{when}` ("in about 3 hours") are filled in.
## Blocks and tools
| | What |
| --- | --- |
| `ai.setup` block | "Connect your AI": a manager pastes the Claude key; hides once connected (props `title`, `about`, `hideWhenReady`) |
| `ai.allowance` block | what the viewer has left today, with a bar (props `label`, `meter`) |
| `hq.aiUsage` block | for managers: today's use by person, the space and bots, against the limits, and whose key pays (prop `limit`) |
| `get_ai_usage` tool | the same report for MCP and the in-app AI (`day` = YYYY-MM-DD, default today); permission manage |
| `api.kits.ailimits.limits.allowance` | query: the signed-in person's allowance in the open space, with `key` (`none`, `own` or `server`), `needsKey` and `canManage` |
| `api.kits.ailimits.limits.usage` | query: the HQ report (null unless the person manages the space) |
## For code that calls Claude
```ts
import { takeAiTurn } from "./kits/ailimits/limits";
// In a mutation, before the AI runs:
const turn = await takeAiTurn(ctx, { workspaceId, userId /* null for a bot nobody started */, source: "chat" | "bot" });
if (!turn.ok) return { ok: false, limit: turn.limit };
// From an action: ctx.runMutation(internal.kits.ailimits.limits.take, { workspaceId, userId, source })
// After each Claude response:
await ctx.runMutation(internal.kits.ailimits.limits._addTokens, { workspaceId, userId, key: turn.key, input, output, cacheRead });
```
It checks every limit and counts the turn in one mutation, so two quick sends can't both slip past.
A turned-away turn isn't counted.
# Notifications
The Notify engine.
Every notification in Zvessa has a **topic** and goes through the person's own settings.
## Topics
| Topic | What | Starts |
| --- | --- | --- |
| talk.message, talk.reply, talk.mention | Chats | on |
| calls.ring (urgent), calls.missed | Calls | on |
| social.reply, social.mention | Feeds | on |
| social.like, social.follow, social.repost | Feeds | digest |
| reviews.new, reviews.reply, reviews.report | Reviews | on |
| apps. | One mini-app's notes | on |
| space.announce | Managers' announcements | on |
| your own, e.g. orders.new | `blueprint.notifications.topics` | on |
A topic's group is the part before the dot. A rule for `talk` covers every talk topic; `*` covers all.
## What a person chooses
- **Mode per topic or group**: on (right away), digest (one summary a day), off; for every space or one space.
The most specific rule wins: this space beats every space, the exact topic beats its group, the group beats `*`.
- **Quiet hours** (`22:00`–`07:00`): held and sent as one summary when they end. Calls still ring unless "Hold calls too".
- **Pause** for 1 h, 8 h or a day.
- **Digest time** (default 18:00, or the space's `digestAt`), in the person's own time zone.
## What a space sets (blueprint, propose → approve → undo)
```json
"notifications": {
"topics": [{ "id": "orders.new", "name": "New orders", "about": "…", "mode": "on" }],
"defaults": { "social": "digest", "orders.new": "on" },
"digestAt": "18:00",
"kidsQuiet": { "from": "20:00", "to": "07:00" },
"inboxPage": "notifications"
}
```
## Blocks
- `notify.settings` — the viewer's own controls. Props: `title`, `sections` (pause, quiet, digest, topics, spaces), `topics`.
- `notify.inbox` — newest first; tap opens the exact page, switching space. Props: `title`, `scope` (all, space), `limit`, `topics`.
- `notify.bell` — unread count, opens the inbox. Props: `label`, `page`, `scope`.
- `notify.send` — managers send to members. Props: `title`, `topic`, `to` (role ids), `link`.
Kits: **Notifications** (a page with inbox + settings), **Announcements** (a page with the send form).
Without either, the bell and summaries open the built-in `/notifications` screen.
## Where a tap goes
`link` is an app path: `/p/`, `/c/?thread=`, `/dm/`, `/post/`, `/u/`,
`/tag/`, `/s/`, `/ai`, `/notifications`, or the short form `page:`. Anything else is dropped.
## AI and MCP
| Tool | Scope | Does |
| --- | --- | --- |
| `my_notifications` | me | Topics here with their mode and where it comes from; quiet hours, pause, digest, spaces |
| `change_my_notifications` | me | `{ rules: [{ topic, mode, space? }], quiet, digestAt, pause }`; applies now, undoable |
| `undo_my_notifications` | me | Back to before the last change |
| `my_inbox` | me | Recent notifications |
| `send_notification` | notify | Managers only, 10 an hour per space; outside AIs only |
The in-app AI has `me`. Space settings change with `propose_change` on `/notifications`.
## From a kit (server)
```ts
import { notify } from "convex/notify";
await notify(ctx, { userIds, topic: "orders.new", workspaceId, title, body, link: "page:orders", group: `order:${id}`, from });
```
Mini-apps: `notify.members({ title, body?, to?, page? })` sends on `apps.`.
# Account
Delete my account, forgot password, change email, change password, and sign out on other
devices. It's part of the core: always on, with no kit to add. Code:
`convex/account/`, `convex/lib/account/`, `src/kits/account/`, tests in `tests/account.test.ts`.
## Blocks
| Block | What it does |
| --- | --- |
| `account.email` | Change email: the password, then a code sent to the new address; the old address gets a notice |
| `account.password` | Change password; signs out the person's other devices |
| `account.sessions` | Sign out on other devices |
| `account.delete` | Delete my account. It names each space the person owns and how many people are in it, because those spaces go too |
They act on the viewer's own account, work on any page for anyone, and take every word as a prop.
The Spaces screen shows all four in an "Account" section, reachable from "Spaces" on every space's
bar, so deleting is always easy to find (App Store rule 5.1.1(v)). "Forgot password?" is on the
sign-in screen.
## Codes
8 digits, stored hashed, good for 15 minutes, 5 tries, 45 seconds between sends. Password checks
use Convex Auth's own rate limit.
## What deleting does
- Spaces the person owns are removed whole, with everything in them.
- Their posts in other people's spaces stay in place but empty, like a deleted post. Ledger
movements say "gone".
- Everything else that points at the person is removed or cleared. Sign-ins, sessions and the user
row go first, so the email is free at once.
- It can't be undone. A goodbye email is sent.
The erase plan (`convex/lib/account/plan.ts`) reads the schema at run time, so new tables are
covered on their own, and a test fails if a table holds a person the plan misses.
## AI and MCP
New scope `account`:
| Tool | What it does |
| --- | --- |
| `account_overview` | The account and the spaces it owns |
| `account_request_deletion` | Emails the person a code |
| `account_confirm_deletion({ code })` | Deletes the account; the emailed code is the person's approval |
Changing email or password is only in the app, because those need the password and an AI is never
handed one.
# API
The app, HQ, the AI, outside AIs and any other client use the **same Convex functions under the
same permissions**. There is no back door: a call can do exactly what the caller could do by
tapping. The generated list of every public function, with what each does, is in the
[Catalog](https://docs.zvessa.com/reference/catalog.html#server-functions).
Every function works on the space the caller has open (see [People](https://docs.zvessa.com/dev/people.html#several-spaces))
and checks a permission verb. A mutation a role can't use throws `Your role in this space can't
do that`; a query returns empty instead.
```ts
import { useMutation, useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
const space = useQuery(api.blueprint.get);
const add = useMutation(api.records.add);
await add({ collection: "orders", data: { item: "Mug", qty: 2 } });
```
## Three ways in
| Way | For | How |
| --- | --- | --- |
| Convex functions | The app, and your own app on the same backend | `useQuery` / `useMutation` with a signed-in person |
| MCP server | Any AI (Claude Desktop, Claude Code, Cursor…) | `{CONVEX_SITE_URL}/mcp` with an access token ([docs/CAPABILITIES.md](https://docs.zvessa.com/more/capabilities.html)) |
| Proposals | Anything that changes the blueprint | `blueprint.change` (people) or `propose_change` (AIs), then approve |
## Sign-in
Convex Auth (`convex/auth.ts`), email and password: `signIn("password", { email, password,
flow: "signUp" | "signIn" })`, `signOut()`. A new person goes through `blueprint.me` /
`blueprint.onboard` (their name and their own space). Apple and Google come before release.
TVs sign in as their own account through pairing (`screens.*`).
## The core functions
**The space** (`blueprint.*`)
| Function | Needs | Does |
| --- | --- | --- |
| `get` | signed in | The open space as this person sees it: `{ blueprint, workspaceId, version, role, can, member, me, own }`. Hidden pages and blocks are already removed. |
| `me` / `onboard` | signed in | Where the person is in onboarding; finish it in one step (name, purpose, space name, look, kits, or a starter `template`) |
| `ensure({ style? })` | signed in | Makes the person's own space on first sign-in (or returns it) |
| `change({ summary, ops })` | `manage` | Applies JSON Patch ops at once, checked, saved as a version |
| `history` / `restore({ version })` | `manage` | The last 30 versions; undo by saving an old one again |
| `pending` / `approve` / `discard` | `manage` | The AI's proposals waiting for approval |
| `addKit({ kit })` / `removeKit({ kit })` | `manage` | Adds or takes out a kit, as one version |
**The AI** (`ai.*`): `messages` and `send({ text })`, both `manage`.
**Records** (`records.*`)
| Function | Needs | Does |
| --- | --- | --- |
| `list({ collection, limit? })` | `view` | Newest first, 200 by default, 500 at most; each with `mine` and `canChange` |
| `count({ collection })` | `view` | How many (up to 10,000) |
| `add({ collection, data })` | `add` | Checks and cleans every field, then saves |
| `update({ id, data })` / `remove({ id })` | `add` (own) or `edit` | Changes or deletes |
**People** (`members.*`): `list`, `mySpaces`, `open`, `visit`, `home`, `explore`, `setName`;
`setRole` and `removeMember` (`manage`, or yourself to leave); `createInvite`, `invites`,
`revokeInvite`, `requests`, `answerRequest` (`invite`); `peek` and `join` (an invite code);
`joinOpen` (a visitor joins an open space or asks to join).
## The engines' functions
| Module | Engine | Functions |
| --- | --- | --- |
| `kits.talk.talk` | Spaces, Lifecycle | Reading: `conversation`, `posts`, `post`, `inbox`, `typing`, `reactors`, `people`. Writing: `send`, `edit`, `remove`, `react`, `pin`, `read`, `setSeat` (mute, pin), `setTyping`, `review` (held posts). Direct and groups: `openDirect`, `createGroup`, `addToGroup`, `leaveGroup`, `renameGroup`, `setGroupAdmin`. Queues: `openCase`, `cases`, `myCases`, `setCase` (status, assignee), `team`. Drafts and later: `draft`, `drafts`, `saveDraft`, `schedule`, `cancelScheduled`, `sendNow` |
| `kits.structure.structure` | Structure | `placements`, `place`, `unplace`, `join`, `mine` |
| `friends` | Spaces | `list`, `status`, `ask`, `answer`, `remove`, `spaces` |
| `dms` | Spaces | `list`, `send`, `remove`, `conversations` |
| `streams` | Spaces, Time | `list`, `send`, `remove`, `schedule`, `cancelScheduled`, `scheduledList`, `writable` |
| `kits.ledger.ledger` | Ledger, Structure | `balances`, `history`, `leaderboard`, `levels`, `marks`, `rights`, `holders`, `open`, `send`, `issue`, `take`, `buyLevel`, `grant`, `revoke` |
| `kits.social.posts` | Spaces (feeds) | `timeline`, `thread`, `pending`, `create`, `edit`, `remove`, `moderate` (approve, hide, show) |
| `kits.social.reactions` | Spaces (feeds) | `react`, `reactors`, `repost`, `save`, `saved` |
| `kits.social.people` | Spaces (feeds) | `me`, `byHandle`, `view`, `handleFree`, `saveProfile`, `follow`, `list`, `suggested` |
| `kits.social.activity`, `.search`, `.sources` | Spaces (feeds) | `list`, `unseen`, `markSeen`; `posts`, `trending`; `status`, `pullNow` (outside feeds) |
| `rounds.quick` | Rounds | `vote`, `poll`, `spin`, `wheel` |
| `rounds.sessions` | Rounds | `create`, `join`, `leave`, `setPlayer`, `setOpen`, `act` (every move), `view`, `screen({ sessionId })`, `live({ gameId })`, `current` |
| `bookings` | Time | `slots`, `days`, `book`, `cancel`, `upcoming` |
| `events` | Events | `track`, `summary`, `leaders` |
| `calls` | Rooms | `targets`, `start` (ring), `incoming`, `current`, `answer`, `decline`, `hangUp` |
| `rooms`, `livekit` | Rooms | `rooms.status`; `livekit.join`, `joinCall` (passes), `moderate`, `record` |
| `media` | Files | `list`, `latest`, `uploadTarget`, `save`, `remove`, `rename`, `can`, `byIds` |
| `screens` | Screens | `open`, `pending`, `pair`, `mine`, `ping`, `list`, `show`, `disconnect` |
| `kits.templates.templates` | Templates | `apply({ template })`, `overview({ days })` (`manage`), `recentCalls` ([Templates](https://docs.zvessa.com/dev/templates.html)) |
| `kits.photobook.books` | Photo books | `read({ block, book?, album?, upTo?, all? })`, `photos({ feed?, tag?, before? })` (`manage`), `shelf({ block })`. Each read names the block showing the book, so a book is exactly as private as its page |
| `kits.reviews.reviews` | Spaces (reviews) | Reading: `summary`, `stars`, `list`, `top`, `subjects`, `queue`. Writing: `write`, `remove`, `vote`, `reply`, `report`, `moderate` ([Engines](https://docs.zvessa.com/dev/engines.html#reviews)) |
| `kits.search.search` | Search | `everywhere({ q, sources?, collection?, stream?, feed?, limit? })` ([docs/SEARCH.md](https://docs.zvessa.com/more/search.html)) |
| `kits.ailimits.limits` | AI limits | `allowance` (the signed-in person's), `usage` (HQ report, managers) ([AI limits](https://docs.zvessa.com/dev/ai-limits.html)) |
| `kits.sharing.library` | Templates and kits | `preview`, `saveMine`, `publish`, `unpublish`, `forget`, `mine`, `browse`, `details`, `add`, `added`, `takeOut`, `rate`, `report`; admins `queue`, `source`, `accept`, `reject`, `remove`, `restore`, `dismissReports` ([Templates](https://docs.zvessa.com/dev/templates.html#templates-and-kits-people-make)) |
| `push` | | `register`, `unregister` (a phone's push token) |
| `capabilities.vault` | Capabilities | `list` (names only), `save`, `setScopes`, `remove` |
| `capabilities.tokens` | Capabilities | `list`, `create` (shown once), `revoke` |
| `capabilities.connections` | Capabilities | `test` |
Each engine's deep doc says who may call what: [docs/LIVE.md](https://docs.zvessa.com/more/live.html), [docs/SOCIAL.md](https://docs.zvessa.com/more/social.html),
[docs/CAPABILITIES.md](https://docs.zvessa.com/more/capabilities.html), `convex/kits/talk/README.md`,
`convex/kits/structure/README.md`, `convex/kits/ledger/README.md`.
## Planned
- Webhooks in and out ("when a record is added, call this URL"), through the Automations kit.
- Stripe Checkout and its webhook for card payments.
# Device features
Zvessa draws real native screens (Expo, React Native), so blocks, kits and mini-apps can use the
phone itself: icons, haptics, sensors, camera, push and more. Each feature is **one shared piece
used by many blocks**, never wired into one screen, and each asks for permission only when first
needed, in words from the blueprint. Package choices and versions: the "myaibio packages and
dependencies" plan in the project.
**When** column: *Ready* is in the app today. *Next* is in the first install list (Expo SDK 58,
`npx expo install`). *Later* is added when the block or kit that needs it starts, so the app stays
small until then.
## Icons
Icons are named **by meaning**, never by an SF Symbol or Material name: `send`, `delete`, `ai`,
`coin`, `game` and about 120 more in `src/icons/icons.json`. A blueprint stores the name as a
string (an `icon` prop), so it works the same on every platform.
```tsx
import { Icon, ICON_NAMES, isIconName, type IconName } from "src/icons";
```
- **iOS** draws the matching SF Symbol (`expo-symbols`). **Android and web** draw it from an 18 KB
Material Symbols subset font, so only the icons we use ship.
- `color` is a style colour name, and `size` scales with the style's type. An unknown name draws
a circle instead of breaking.
- **Adding an icon:** add `"name": ["sf.symbol.name", "material_name"]` to `icons.json`, run
`python3 src/icons/build-font.py`, then the typecheck.
More in [docs/PERFORMANCE.md](https://docs.zvessa.com/more/performance.html) (Icons).
## Look and feel
| Feature | Used for | Package | When |
| --- | --- | --- | --- |
| Haptics | Taps, approve and undo, game feedback; follows the style's `motion.haptics` | `expo-haptics` | Next |
| Animation | Smooth motion on the UI thread; follows the style's `motion.speed` | `react-native-reanimated` | Next |
| Gestures | Swipes, drags, reorder, pinch-zoom | `react-native-gesture-handler` | Next |
| Frosted glass | Glass panes, sheets and tab bars (style `treatment: glass`) | `expo-blur`, `expo-glass-effect` | Next |
| Gradients | Gradient backdrops and buttons | `expo-linear-gradient`, `expo-mesh-gradient` | Next |
| Native controls | Real switches, pickers, sliders, date fields, sheets | `@expo/ui`, community pickers | Next |
| Native tabs | The real iOS tab bar and Android's bar; scrollable top tabs | `expo-router` native tabs | Next |
| Fast lists | Long chats, versions, collections | `@shopify/flash-list` | Next |
| Fast images | Cached images with placeholders | `expo-image` | Next |
| Animated stickers | Lottie celebrations and loaders | `lottie-react-native` | Next |
| Charts and shapes | Chart, progress and vector blocks | `react-native-svg` | Next |
## Sensors
`expo-sensors` (Next): accelerometer, gyroscope, device motion, magnetometer (compass),
barometer and pedometer. Used for tilt and shake games, step trackers and motion effects, and
passed to mini-apps through the bridge ([Mini-apps](https://docs.zvessa.com/dev/miniapps.html#the-bridge)).
Rules, because sensors drain the battery fastest:
- Subscribe only while the block or app is **on screen and the app is in the foreground**
(`useLive()`, [Performance](https://docs.zvessa.com/dev/performance.html#the-hooks)); unsubscribe when it turns
false and on unmount.
- Ask for the **lowest rate that works** (`setUpdateInterval`): 30 Hz for tilt, 10 Hz for a
compass, the pedometer's own counts for steps. Never above 60 Hz.
- One shared subscription per sensor, fanned out to every block that wants it.
- Location (`expo-location`, Later) uses the coarsest accuracy that works and never runs in the
background unless the person turned that on for a kit.
## Media
| Feature | Package | When |
| --- | --- | --- |
| Pick or take photos and video (HEIC converted on the phone) | `expo-image-picker` | Ready |
| Resize and compress before upload | `expo-image-manipulator` | Next |
| Background upload | `expo-file-system` | Next |
| Native video player, picture-in-picture | `expo-video` | Ready |
| Play and record audio, voice notes | `expo-audio` | Ready |
| Camera and QR scanning | `expo-camera` | Later |
| Save to the photo library | `expo-media-library` | Later |
Photos and small files go to Convex file storage; video and large files to Cloudflare R2 through
the Convex R2 component, so the app never holds storage keys.
## Calls and live
Ready: LiveKit (`@livekit/react-native`, `livekit-client` on the web) for voice and video calls,
group rooms, stages, screen share (iPhone through a broadcast extension) and recording,
self-hosted next to Convex. Needs a development build. Setup: [docs/LIVE.md](https://docs.zvessa.com/more/live.html).
## Notifications
Ready: `expo-notifications` on the phone, Expo's push service from Convex (`convex/push.ts`).
Calls ring a closed phone with Answer and Decline buttons. People are asked to allow
notifications the first time they need them, not at sign-in. People choose on, digest or off
per kind and per space, quiet hours and pause, and a tap opens the exact page
([Notifications](https://docs.zvessa.com/dev/notify.html)).
## Everything else
| Feature | Package | When |
| --- | --- | --- |
| Keep the sign-in token in the Keychain / Keystore | `expo-secure-store` | Ready |
| Sandboxed mini-apps | `react-native-webview` | Ready |
| Copy and paste | `expo-clipboard` | Next |
| Language and date format | `expo-localization` | Next |
| Random ids and hashes | `expo-crypto` | Next |
| Pick files | `expo-document-picker` | Ready |
| Share a page or export | `expo-sharing` | Later |
| In-app browser and OAuth | `expo-web-browser` | Later |
| Face ID / fingerprint to unlock or approve | `expo-local-authentication` | Later |
| Maps with pins | `expo-maps` | Later |
| Add to calendar, invite from contacts | `expo-calendar`, `expo-contacts` | Later |
| Read aloud | `expo-speech` | Later |
| Keep the screen on (games, recipes, timers) | `expo-keep-awake` | Later |
| Landscape for games and video | `expo-screen-orientation` | Later |
| Print or save as PDF; a page as an image | `expo-print`, `react-native-view-shot` | Later |
| Home-screen widgets; Siri and Shortcuts | `expo-widgets`, `expo-app-intents` | Later |
| Offline pages and drafts | `expo-sqlite` | Later |
## Permissions
One hook asks for camera, microphone, photos, location, motion and push **only when first
needed**, explains why in words from the blueprint, and handles "no" without breaking the screen.
A mini-app gets a device permission only after the person allows it for that app.
## Limits
- A brand-new native module needs an app update through the stores, so native power comes from
this shipped kit of features. Blocks, kits, styles and mini-apps never need one.
- Claude can preview the web build; native screens are tested in a development build on a phone.
## Links into the app
Links that open the app use `zvessa://` (for example an invite link when no web address is set).
Older `myaibio://` links still open the app.
# Performance for builders
Zvessa must feel instant and **never drain the battery or heat the phone**, on a five-year-old
Android as much as a new iPhone. These are the budgets every block, kit, engine and mini-app keeps.
The app-wide rules and how the core meets them: [docs/PERFORMANCE.md](https://docs.zvessa.com/more/performance.html).
## The hooks
`src/lib/activity.ts` tells a block when it may do work:
| Hook | True when |
| --- | --- |
| `useLive()` | The screen is showing and the app is in the foreground |
| `useDecorativeMotion()` | `useLive()` and the phone's Reduce Motion is off |
| `useReduceMotion()` | The phone's Reduce Motion is on |
| `useAppActive()` | The app is in the foreground |
Timers, sensors, media and game loops start only while `useLive()` is true and stop when it
turns false. Decorative animation (beams, shimmer) runs only while `useDecorativeMotion()` is true.
## Budgets
Targets every change is measured against:
| What | Budget |
| --- | --- |
| Frames | 60 fps while scrolling and animating; no work over 16 ms on the JS thread |
| Start | The dashboard shows from cache before the network answers |
| Idle | Zero timers, animations, sensors or network when nothing on screen changes |
| A page | Draws only what's on screen; long lists are virtualised |
| A query | Reads through an index with a limit; returns only what the screen shows |
| Images | Sized to their box before upload; cached; no full-size photos in lists |
| A mini-app | Well under 200 KB of HTML; sleeps when hidden |
| App size | Only what's used ships: packages are added when their block starts |
## Rules
**Nothing runs that can't be seen.**
- Stop animations, timers, sensors and media when a block scrolls off screen, its tab is hidden or
the app goes to the background. Resume on return.
- Mini-apps are paused for you when hidden: `requestAnimationFrame`, timers and media stop, the
page gets `myaibio:pause` / `myaibio:resume` events, and `window.zvessa.paused` says which.
Draw only while something moves.
**Do the least work.**
- Animate with Reanimated on the UI thread, never by setting state every frame. Animate transform
and opacity, not layout.
- Respect `motion.speed: off` and `haptics: off`.
- Lists longer than a screen use FlashList, with stable keys (block or record ids).
- Memoise renderers that take big props; never rebuild the theme per block (read it from
`useWorkspace()`).
- Sensors at the lowest rate that works, one shared subscription ([Device](https://docs.zvessa.com/dev/device.html#sensors)).
**Ask the server for less.**
- Convex queries are live: subscribe once, never poll.
- Take a limit on every read (`take(n)`); page long lists.
- Counts and totals on big data come from aggregates (Events engine), not by reading every row.
- Heavy or outside work (AI, HTTP, MCP) runs in background jobs and streams results back.
- Keep the blueprint small: data goes in collections, big files in storage, never inline.
**Network and battery.**
- Upload compressed media; download thumbnails first.
- Batch pushes on the server; never wake the phone for something it will show anyway.
- Nothing runs in the background unless the person turned it on for a kit (a step counter, a
call).
## Checking
- Profile in a release-like build, not development mode.
- Watch the JS and UI frame rates while scrolling the longest page and running the heaviest
mini-app.
- Leave the app idle on a page for a minute: CPU should sit near zero.
# Security and safety
What the system already guards, so a builder doesn't have to. Every rule here is enforced **on
the server**; the app, HQ, the AI, an MCP client and your own code all hit the same checks. Tests:
`tests/launch-review.test.ts`, `tests/capabilities.test.ts` and each engine's test file.
## The ground rules
| Rule | What it means for you |
| --- | --- |
| Nothing people make runs as code on the server | Blocks, kits, styles, games and bots are data. Custom logic is a [mini-app](https://docs.zvessa.com/dev/miniapps.html) in a sandbox. |
| The AI proposes, a person approves | Neither the in-app AI nor an MCP client changes a space on its own (unless a person gives a token `approve`). Every change is a version that can be undone. |
| Every change is checked | `checkBlueprint` refuses a broken or unsafe blueprint with a readable message ([Blueprint](https://docs.zvessa.com/dev/blueprint.html#checks)). Replacing the whole blueprint in one op is refused. |
| One permission check | Every function goes through `getAccess` / `mustAccess` and checks a verb, never a role name ([People](https://docs.zvessa.com/dev/people.html#permissions)). |
| Keys stay on the server | No query returns a key; devices, the AI, bots and mini-apps see key names only. |
## Hidden means hidden, data too
`visibleTo` on a page or block (roles, `level:`, `level:+`, `badge:`) does two
things:
1. **The layout:** `blueprintFor` removes hidden pages and blocks before the blueprint is sent,
so they never reach the device.
2. **The data:** `hiddenFrom` (`convex/lib/blueprint.ts`) works out which collections, streams,
feeds and mini-apps a person can't reach because every block that shows them is hidden from
them. Records, posts and apps behind a staff-only page can't be read or written through the
API either. A collection, stream or app that **no block points to** stays reachable to anyone
with the verb, so put private data behind a hidden block, or give the role no `view`.
HQ blocks (`hq.*`) are only for people who manage the space. Managers see everything.
## Roles and invites
- `owner` keeps every permission; `visitor` can never `manage`, `invite` or `edit`.
- Only managers hand out a role that can `manage` or `invite`; someone who can only invite can't
accept a join request with a managing role.
- Invite codes are 10 letters, can be used up (`maxUses`) or revoked.
- Changing someone's role changes what their MCP tokens can do at once.
## Kids spaces
`audience: "kids"` (Kids space kit):
- The space must stay private and invite-only and is out of Explore; only the owner can open it
up again.
- A **kid** is a member of a kids space whose role there can't invite or manage. Kids only
message, friend or ring people who share one of their kids spaces; strangers can't reach them,
even from another open space the kid is in, and kids can't walk into other open spaces.
- Kids spaces can't have MCP tokens that approve changes. The AI is told to keep every word, page
and mini-app kid-safe and to give kids only `view`, `post` and `play`.
## The AI
- It only acts through its tools ([AI](https://docs.zvessa.com/dev/ai.html)); its changes are proposals. It never approves,
never writes records, can't remove the HQ page, and only reads through connections (`GET`,
read-only MCP tools).
- What comes back from the web or a connection is marked as outside data, so a page or a tool
result can't make it send anything out.
- Each space runs the AI on its own Claude key (an `ai` key in the vault). Per-person, bot, burst (6 a minute)
and token limits, and the space's own `/ai/limits`, are in [AI limits](https://docs.zvessa.com/dev/ai-limits.html). Messages are capped at 8,000
characters.
## MCP tokens
`mbk_…` tokens ([docs/CAPABILITIES.md](https://docs.zvessa.com/more/capabilities.html)): one person in one space; can do
what both its scopes and the person's current role allow; only a hash is stored and it's shown
once; 90 days by default (1 to 365); 120 calls a minute; batches of at most 20; at most 20 live tokens
per space. URL tokens (for clients that only take a URL) can only read and propose,
because URLs end up in logs. Revoking stops a token at once.
## The key vault
AES-256-GCM under the platform's vault key, tied to the space and the key's name. Each key has uses (`ai`,
`connections`) and hosts (`api.example.com`, `*.example.com`; not shared suffixes such as
`*.co.uk` or `*.vercel.app`). A key is refused for any other host. Changes can only narrow a key;
widening needs it pasted again. Keys are scrubbed from results and errors. Only managers add,
change or remove keys.
## Calls to the outside
Every outside call (connections, web fetch, outside feeds) leaves through one Node action,
`convex/capabilities/egress.ts`, using the rules in `convex/lib/capabilities/net.ts`:
- Public HTTPS on port 443 only. The host is resolved and **every address must be public**: no
loopback, private, link-local or cloud metadata, carrier-grade NAT, NAT64, 6to4, Teredo,
IPv4-compatible, documentation or reserved ranges.
- The request is pinned to the checked address, so a name can't switch to an inside address
between the check and the call. No redirects; 2 MB and 30 s caps; results trimmed.
- Header values in the blueprint can't look like keys (`Authorization`, `X-API-Key`…); those go
in the vault. `http` connections stay under their `url` and only `GET` unless `methods` lists
more; `mcp` connections only call read-only tools unless `tools` lists more.
- For more depth, also firewall the backend's outbound traffic away from your private network.
## Mini-apps
A mini-app is HTML in a sandbox (WebView on phones, iframe on the web) with
`default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; media-src data: blob:; font-src data:`
(no `https:` images, so typed text can't leak out in an image address). It can't fetch, open
sockets, load scripts or read the app, cookies or keys. It reaches the space only through the
[bridge](https://docs.zvessa.com/dev/miniapps.html#the-bridge): each call must be in the app's `needs`, allowed by the viewer's
role, allowed by the person for anything on the phone, and within the rate limit (bursts of 60,
30 a second), and the server checks again from the saved blueprint. Published apps are reviewed
by an admin, use only the adding space's own connections, and kids spaces only get apps rated
`everyone`. It gets the style as
CSS variables and is paused when it's off screen ([Mini-apps](https://docs.zvessa.com/dev/miniapps.html)).
## Limits against flooding and cheating
| Where | Limit |
| --- | --- |
| Chats and streams | 4,000 characters a message; slow mode per stream (moderators exempt); groups capped by `talk.groups.max`; a later send at least a minute ahead |
| Feeds | 60 posts or reactions per person per 10 minutes; outside sources pulled at most once a minute |
| Records | 40 fields per collection; every field checked and cleaned on add; lists 500 at most |
| Ledger | Whole steps only; balances never go below zero; every move is an entry that's never edited; issuing needs the unit's `issue` verb |
| Games | Randomness, timers, scores and secrets decided on the server; the big screen never shows secret words, hidden answers or gift names; 200 players; 8 wrong join codes in 15 minutes, then a pause |
| Calls and rooms | 3 calls a minute and 20 an hour per person; 16 people a call; room passes last 10 minutes; a host's remove holds 2 hours |
| Screens | Pairing needs `invite`; 10 wrong codes a minute; codes last an hour; a TV only gets a role that views and plays |
| Media | Upload links take one exact size and type for 10 minutes; 4 GB a file; 20 GB a space (`MEDIA_QUOTA_BYTES`); unsaved uploads deleted hourly; private-space links run out after an hour |
| Events | Short lowercase names only; summaries for managers; up to 90 days |
LiveKit's webhook is signature-checked; anything else is refused. Media and room detail:
[docs/LIVE.md](https://docs.zvessa.com/more/live.html). Feeds: [docs/SOCIAL.md](https://docs.zvessa.com/more/social.html).
## Reports, blocks and filters
Reporting and blocking are always on in every space and direct chat. Reports go to the
space's moderators; serious reasons, anything in a kids space and anything outside a space also
reach Zvessa's admins. A word filter (`blueprint.safety`) masks, holds or refuses words on the
server, and kids spaces never go below its strict settings. Moderators mute and ban; admins
suspend accounts and lock spaces. Detail: [docs/SAFETY.md](https://docs.zvessa.com/more/safety.html).
## Accounts and email
People can delete their account from the Spaces screen (an App Store
requirement); email codes are 8 digits, hashed, last 15 minutes and allow 5 tries. Changing email
or password is never offered to an AI. Detail: [Account](https://docs.zvessa.com/dev/account.html).
## Planned
Grown-up controls for who kids talk to and how long they play.
# Catalog
Generated from the code by `node --no-warnings docs/build/catalog.mjs`. Don't edit by hand.
Last generated 2026-10-21.
**Status words.** *Ready*: works in the app today. *Described*: the server and the AI know it, the
screen for it is being built. *Coming*: listed in the library, waits for its engine.
## Engines
| Engine | Id | Status |
| --- | --- | --- |
| Collections | `collections` | Ready |
| Spaces | `spaces` | Ready |
| Structure | `structure` | Ready |
| Ledger | `ledger` | Ready |
| Rounds | `rounds` | Ready |
| Rooms | `rooms` | Ready |
| Time | `time` | Ready |
| Lifecycle | `lifecycle` | Ready |
| Events | `events` | Ready |
| Apps | `apps` | Ready |
| Capabilities | `capabilities` | Ready |
| Screens | `screens` | Ready |
| Files | `files` | Ready |
| Audiences | `audiences` | Ready |
| Automations | `automations` | Ready |
| Notify | `notify` | Ready |
| Web | `web` | Ready |
| AI Jobs | `ai` | Ready |
| Search | `search` | Ready |
## Blocks
156 kinds.
Props marked * are required. A block with *children* holds other blocks.
| Kind | What it is | Props | Status |
| --- | --- | --- | --- |
| `bots.list` | The space's bots (the AI chat first) and automations: what each does, what starts it, today's use, the next scheduled run, and an on/off switch. For people who manage the space. | `title` (string): Heading, default none `show` (string): 'all' (default), 'bots' or 'automations' | Ready |
| `bots.waiting` | Things bots and automations want to do that wait for a person: posts (reply drafts), records, notifications, coins. Approvers can edit the words, approve or discard. Shows only what this person may approve. | `title` (string): Heading `empty` (string): Shown when nothing waits | Ready |
| `bots.runs` | Recent runs of automations and bots: what started each, every step's result and any failure. For people who manage the space. | `automation` (string): Only this automation's runs `bot` (string): Only this bot's runs `limit` (number): How many, default 10 `title` (string): Heading | Ready |
| `bots.run` | A button that starts an automation whose trigger is manual (send the weekly report now, close the day, deal a new round). Shows only to roles in its when.who and people who manage. | `automation`* (string): The /automations id `label` (string): Button words, default the automation's name `done` (string): Words after it starts | Ready |
| `bots.ask` | Give one of the space's bots a task in words and see its answer (for people who manage the space). People talk with bots in conversations by @mentioning them. | `bot`* (string): A /bots id (not the builder) `placeholder` (string): Words in the empty box `title` (string): Heading | Ready |
| `bots.hooks` | Webhook addresses that start an automation whose trigger is webhook, for outside services (forms, shops, scripts). Make one, copy it once, revoke it. For people who manage the space. | `automation`* (string): The /automations id `title` (string): Heading | Ready |
| `header` | The page's header (app bar), first on a page: a small line over the title (eyebrow), the title, a subtitle, an optional mark or logo, and its action buttons (children: button blocks, icon-only when they have an icon and no label). size 'large' is the phone's big title, 'inline' a compact bar title, 'hero' a tall picture band with the title on it. When it is the first block it replaces the page's own title, and the bar shows its title as you scroll. *Holds children.* | `title` (string): Title. Empty uses the page's name `subtitle` (string): A line under the title `eyebrow` (string): A small line over the title (the space's name, a date, a section) `size` (string): 'large' (default), 'inline' or 'hero' `align` (string): 'start' (default) or 'center' `mark` (string): A letter, emoji or icon name in a square before the title (like an app's logo) `image` (string): https URL: the picture behind a 'hero' header, or the logo square otherwise `tint` (string): Colour of the mark square or the hero band without a picture: a style colour role. Default accent `titleVariant` (string): The title's text role (display, title, headline, subtitle, body, callout, caption, label, overline, number, quote, code). Default display (large), subtitle (inline), title (hero) `titleTone` (string): The title's colour: a style colour role or a colour value. 'gradient' paints it from accent to accent2 `font` (string): The title's font: 'body', 'display' or 'mono' (the style's), or a font name (system, rounded, serif, mono…) `weight` (string): The title's weight: 400, 500, 600, 700 or 800 `background` (string): Behind the header: 'none' (default, the page shows through), 'surface', 'tint' (a soft wash of tint), 'gradient' (tint to accent2), 'glass' (frosted, see-through) or 'image' (the picture in image) `height` (string): 'compact', 'regular' (default) or 'tall' `divider` (boolean): A thin line under the header | Ready |
| `heading` | A title for a part of a page, with an optional small line over it and a line under it. | `text`* (string): The words `level` (number): 1 (title), 2 (headline), 3 (subtitle) or 4 (small label). Default 1 `subtitle` (string): A quieter line under it `eyebrow` (string): A small line over it `align` (string): 'start' (default), 'center' or 'end' | Ready |
| `text` | Words in one of the style's text roles. variant picks the role: display, title, headline, subtitle, body, callout, caption, label, overline, number, quote, code. body is a paragraph, callout a bigger quiet intro, caption small print, label and overline small capitals, number a big figure, quote a pulled quote, code fixed-width. | `text`* (string): The words. New lines are kept `variant` (string): The text role: display, title, headline, subtitle, body, callout, caption, label, overline, number, quote, code. Default body `align` (string): 'start' (default), 'center' or 'end' `tone` (string): A colour role from the style (accent, accent2, success, warn, danger, gold, text2…). Default depends on the block. Default the role's own `lines` (number): Show at most this many lines, then … | Ready |
| `list` | A list of short lines: bullets, numbers or ticks. | `items`* (string[]): One string per item `variant` (string): 'bullet' (default), 'number' or 'check' | Ready |
| `badge` | A small pill with a word or a count: New, Live, 3, Beta, Sold out. | `text`* (string): The words `tone` (string): A colour role from the style (accent, accent2, success, warn, danger, gold, text2…). Default depends on the block. Default accent `icon` (string): An icon name (home, chat, calendar, star, settings, users, bolt, game, coin…) or an emoji `variant` (string): 'soft' (default: a light tint), 'solid' or 'outline' | Ready |
| `chips` | A row of small tags that wraps: topics, skills, opening days, filters to read. | `items`* (string[]): One tag per item `tone` (string): A colour role from the style (accent, accent2, success, warn, danger, gold, text2…). Default depends on the block. Default neutral | Ready |
| `card` | A framed box: a picture or icon, a small line over the title, the title, a line under it, text, a badge, and child blocks. size 'sm' (a small square for grids and shelves), 'md' (default), 'lg' (big, a picture on top), 'wide' (picture or icon on the left, words on the right) or 'hero' (tall, the picture fills it with the words on top). With href the whole card opens it. *Holds children.* | `title` (string): Title `text` (string): Text `subtitle` (string): A line under the title `eyebrow` (string): A small line over the title `size` (string): 'sm', 'md' (default), 'lg', 'wide' or 'hero' `variant` (string): 'default' (the style's card), 'outline', 'filled' (a tint of tint), 'accent' (solid tint), 'glass' or 'plain' (no frame) `image` (string): https URL of a picture `icon` (string): An icon name (home, chat, calendar, star, settings, users, bolt, game, coin…) or an emoji `tint` (string): Colour of the icon square and of 'filled' and 'accent' cards: A colour role from the style (accent, accent2, success, warn, danger, gold, text2…). Default depends on the block `badge` (string): A small label in the corner (New, 2, Live) `href` (string): '#page-id' opens a page, 'https://…' a website, 'chat:' a conversation (e.g. chat:support, chat:general), 'ai' the AI chat (people who manage), 'mailto:' or 'tel:' a mail or a phone call `action` (string): Words of a button at the bottom that opens href (Book, Open, Play) | Ready |
| `stat` | A big number with a label, and optionally its change (+12%) and a note. | `label`* (string): What the number is `value`* (string): The number or short value `change` (string): The change, as people should read it: +12%, -3, New `trend` (string): 'up', 'down' or 'flat': colours the change (up green, down red) `note` (string): A short line under it `icon` (string): An icon name (home, chat, calendar, star, settings, users, bolt, game, coin…) or an emoji `href` (string): '#page-id' opens a page, 'https://…' a website, 'chat:' a conversation (e.g. chat:support, chat:general), 'ai' the AI chat (people who manage), 'mailto:' or 'tel:' a mail or a phone call | Ready |
| `image` | A picture from a URL, in a shape you pick, with an optional caption. | `src`* (string): https URL `alt` (string): What the picture shows `ratio` (string): Shape: 1:1, 4:3, 3:2, 16:9, 21:9, 3:4, 9:16. Default 16:9 `caption` (string): Small words under it `round` (boolean): A circle (a face, a logo) | Ready |
| `button` | A button that opens a page, a conversation, a link or the AI. With an icon and no label it is a round icon button (header actions). | `label` (string): Button words `href` (string): '#page-id' opens a page, 'https://…' a website, 'chat:' a conversation (e.g. chat:support, chat:general), 'ai' the AI chat (people who manage), 'mailto:' or 'tel:' a mail or a phone call `variant` (string): 'primary' (default), 'secondary', 'plain' or 'danger' `icon` (string): An icon name (home, chat, calendar, star, settings, users, bolt, game, coin…) or an emoji `size` (string): 'sm', 'md' (default) or 'lg' `full` (boolean): Fill the width | Ready |
| `section` | A part of a page with a small title, an optional line under it and a link on the right (See all), holding child blocks. variant 'card' puts the children on one card. With open true it shows as one row that opens its children as their own screen (a settings page inside a page). *Holds children.* | `title` (string): Small title over the section `subtitle` (string): A line under the title `action` (string): Words of the link on the right (See all, Edit) `href` (string): Where the link goes: '#page-id' opens a page, 'https://…' a website, 'chat:' a conversation (e.g. chat:support, chat:general), 'ai' the AI chat (people who manage), 'mailto:' or 'tel:' a mail or a phone call `variant` (string): 'plain' (default) or 'card' `open` (boolean): Show as one row that opens the children as their own screen `icon` (string): With open: the row's icon. An icon name (home, chat, calendar, star, settings, users, bolt, game, coin…) or an emoji | Ready |
| `stack` | Lays out child blocks in a column (or a row). *Holds children.* | `direction` (string): 'column' (default) or 'row' `gap` (number): Space between children in px, 0-64, default 12 (row and grid take size steps instead) `align` (string): 'stretch' (default), 'start', 'center' or 'end' | Ready |
| `row` | Child blocks side by side, wrapping onto the next line when they don't fit: buttons, badges, small cards, stats. *Holds children.* | `gap` (string): Space between: xs, sm, md, lg, xl. Default sm `align` (string): Up and down: 'start', 'center' (default), 'end' or 'stretch' `justify` (string): Across: 'start' (default), 'center', 'end' or 'between' `equal` (boolean): Every child gets the same width (stats, cards) | Ready |
| `grid` | Child blocks in columns of equal width: cards, stats, pictures. A child can span more columns with props.span. *Holds children.* | `columns` (number): Columns on a phone, 1 to 6. Default 2 `columnsWide` (number): Columns on a tablet or the web, 1 to 8. Default twice columns, up to 4 `gap` (string): Space between: xs, sm, md, lg, xl. Default sm | Ready |
| `scroll` | A shelf of child blocks people swipe sideways: cards, pictures, stats. Each child is as wide as its size (cards) or itemSize. *Holds children.* | `itemSize` (string): Width of children that don't say: 'sm', 'md' (default), 'lg' or 'full' `gap` (string): Space between: xs, sm, md, lg, xl. Default sm `snap` (boolean): Stop on each child. Default true | Ready |
| `group` | Grouped list rows on one card with thin lines between them (a settings list, a menu, a contact card). Its children are usually `item` blocks. *Holds children.* | `title` (string): Small title over the group `footer` (string): A small note under it `variant` (string): 'inset' (default: on a card) or 'plain' (rows on the page) | Ready |
| `item` | One list row: an icon or picture, a title, a line under it, a value on the right, a badge, and an arrow when it opens something. Opens href, or, when it holds child blocks, opens them as their own screen titled by the row. *Holds children.* | `title`* (string): Title `subtitle` (string): A line under the title `icon` (string): An icon name (home, chat, calendar, star, settings, users, bolt, game, coin…) or an emoji `tint` (string): Colour of the icon square: A colour role from the style (accent, accent2, success, warn, danger, gold, text2…). Default depends on the block. Default accent `tone` (string): How the icon square is painted: 'soft' (default), 'solid' or 'glossy' `image` (string): https URL of a round picture in place of the icon (a face, a logo) `value` (string): Short words on the right (On, 3 members, $40) `badge` (string): A small pill on the right (New, 2) `href` (string): '#page-id' opens a page, 'https://…' a website, 'chat:' a conversation (e.g. chat:support, chat:general), 'ai' the AI chat (people who manage), 'mailto:' or 'tel:' a mail or a phone call `danger` (boolean): Red title (Delete, Leave) `chevron` (boolean): The arrow on the right. Default: shown when it opens something | Ready |
| `divider` | A thin line between parts, optionally with a word in the middle. | `label` (string): A word in the middle (or, Today) `inset` (boolean): Start the line after the space of an icon, like between list rows | Ready |
| `spacer` | Empty space between parts. | `size` (string): xs, sm, md, lg, xl. Default md | Ready |
| `miniapp` | Runs one of the workspace's mini-apps (apps[].id) in a sandbox. Use for games and anything blocks can't express. props.config is a JSON object the mini-app reads as window.zvessa.config (its settings; config.connections lists connection ids it may read from). | `app`* (string): The id of an entry in blueprint.apps `height` (number): Height in px, default 360 `config` (object): Settings for this copy of the app, handed to it as window.zvessa.config (a starter app lists its keys). Plain JSON, up to 16 KB. config.connections names the space connections it may read through the bridge | Ready |
| `records.list` | Shows a collection's records as a list, or as a board with one column per option when groupBy names a choice field. | `collection`* (string): A blueprint.collections id `fields` (string[]): Which fields to show, in order; default all `groupBy` (string): A choice field id: show a board, one column per option `empty` (string): Words when there are no records | Ready |
| `records.form` | A form that adds a record to a collection. | `collection`* (string): A blueprint.collections id `fields` (string[]): Which fields to ask for; default all `submit` (string): Button words, default 'Save' `done` (string): Words shown after saving `defaults` (string): JSON object of values set on every new record, e.g. {"status":"To do"} | Ready |
| `records.count` | A big number: how many records a collection has. | `collection`* (string): A blueprint.collections id `label` (string): What the number is | Ready |
| `stream` | One live conversation from blueprint.streams: a room, a channel, a feed. Who sees, writes and moderates is set on the stream. look 'feed' shows posts as cards, newest first. | `stream`* (string): A blueprint.streams id `look` (string): 'chat' (default, bubbles) or 'feed' (cards) `height` (number): Height in px of the chat look, default 520 `placeholder` (string): Words in the empty message box (overrides the stream's) | Ready |
| `conversation` | One conversation with its header: back, picture, name and people count, voice and video call buttons (direct and group chats, with the calls kit), a details button (people, mute, leave), then the messages and the box to write in. Replies, reactions, edits, read receipts and typing follow the conversation's settings (/streams, /talk). Without a stream it shows the conversation that was opened: put it on the page /talk/screen names to design the full-screen chat. | `stream` (string): A blueprint.streams id. Leave it out on the /talk/screen page to show whichever chat was opened `header` (boolean): Show the header row (default true) `calls` (boolean): Show voice and video call buttons when calls are on (default true) `details` (boolean): Show the details button (default true) `look` (string): 'chat' (default, bubbles) or 'feed' (cards) `height` (number): Height in px when it sits inside a page, default 520 `placeholder` (string): Words in the empty message box `background` (string): The colour behind the messages: a style colour role (chatBg, bg, surface…). Default chatBg on the full screen `send` (string): The send button: 'text' (default, a Send button) or 'icon' (a round button with an arrow, like messaging apps) | Ready |
| `inbox` | A person's conversations in one list: direct chats, groups, the space's streams and their cases, with unread counts, search and New chat. Opens a conversation (beside the list on wide screens). | `title` (string): Heading `show` (string[]): Which kinds to list: direct, group, stream, case; default all `height` (number): Height in px on wide screens, default 600 | Ready |
| `direct` | The person's direct (one-to-one) conversations. Same as an inbox showing only direct chats. | `title` (string): Heading, default 'Messages' | Ready |
| `queue` | The team's side of a queue stream (support desk, applications, orders): every case by status and who it's given to, to answer them. Only the stream's team sees it. | `stream`* (string): A blueprint.streams id whose mode is 'queue' `title` (string): Heading | Ready |
| `ask` | The asking side of a queue stream: a person opens a case (asks for help, applies, orders) and follows their own. | `stream`* (string): A blueprint.streams id whose mode is 'queue' `subject` (string): Label for the short title, default 'Subject' `button` (string): Button words, default 'Send' `done` (string): Words shown after sending | Ready |
| `drafts` | A person's unsent drafts and scheduled posts, and, for moderators, posts waiting for approval. | `title` (string): Heading | Ready |
| `units` | The space's units (teams, departments, classes, branches…) as a tree with who is in each, their titles and leads. Leads and managers place people; open units can be joined; managers add and rename units. | `unit` (string): Start at this unit (blueprint.structure.units id); default the whole tree `people` (boolean): Show who is in each unit, default true | Ready |
| `members` | The space's members and their roles, with a button to message each one directly. | `title` (string): Heading, default 'People' | Ready |
| `invite` | Makes an invite link people open to join the space with a role. | `role` (string): Role new people get, default 'member' `label` (string): Button words | Ready |
| `booking` | People book a time (appointments, classes, a table). Props are the opening hours, in the owner's time zone; people who manage the space see every booking. | `title` (string): Heading, default 'Book a time' `duration` (number): Minutes per booking, 5-480 (default 30) `days` (string[]): Open days: sun, mon, tue, wed, thu, fri, sat (default mon-fri) `from` (string): Opening time, HH:MM 24-hour (default 09:00) `to` (string): Closing time, HH:MM 24-hour (default 17:00) `tz` (string): IANA time zone like America/New_York (default UTC; ask the owner) `ahead` (number): How many days ahead people can book (default 14) | Ready |
| `scheduled` | Write a post now and have it go out in a chat, channel or feed later; lists what's waiting. | `stream` (string): Only post into this blueprint.streams id | Ready |
| `analytics` | Charts of what happens in the space per day: page views, posts, records, joins, plays. Only people who manage the space see it. | `days` (number): Days to show first: 7, 14, 30 or 90 | Ready |
| `poll` | A question members vote on, with live results. | `question`* (string): The question `options`* (string[]): At least two answers `multiple` (boolean): Allow picking more than one | Ready |
| `wheel` | A wheel anyone can spin for a random pick; everyone sees the same result. | `options`* (string[]): At least two slices `label` (string): Button words, default 'Spin' | Ready |
| `game` | A live game from blueprint.games (trivia, quiz board, charades, bingo, a gift draw…). People start one, join with a code and play on their phones; on a TV paired with Show on TV it shows the big screen. | `game`* (string): A blueprint.games id `title` (string): Heading, default the game's name | Ready |
| `game.screen` | The big-screen view (question, board, timer, scores) of the game in progress, for a tablet or a screen on the wall. | `game`* (string): A blueprint.games id | Ready |
| `split` | Totals a collection's money field, shows each person's share and the fewest payments to settle up. Trip costs, a shared gift, a picnic. | `collection`* (string): A blueprint.collections id `amount`* (string): The money or number field to add up `payer`* (string): The text field with who paid `people` (string[]): Names sharing the cost; default everyone who paid `title` (string): Heading | Ready |
| `room` | A live room for voice, video and screen sharing. mode 'talk': everyone allowed to speak joins as equals (calls, group calls, huddles, a study room). mode 'stage': hosts are seen and heard and everyone else watches and reacts (live streams, classes, town halls, a show). | `title` (string): Name shown on the room `mode` (string): 'talk' (default) or 'stage' `video` (boolean): Allow cameras, default true; false makes it voice only `screen` (boolean): Allow screen sharing, default true `speak` (string): Permission needed to speak in 'talk' mode (view, post, play, add, edit, invite, manage), default 'post' `watch` (string): Permission needed to watch a 'stage', default 'view' `host` (string): Permission needed to host: be on stage, invite people up, mute, remove and record. Default 'manage' `max` (number): Most people in the room at once, default 50 for talk, 1000 for stage `record` (boolean): Let hosts record to the media library (needs LiveKit Egress and R2) `join` (string): Join button words | Ready |
| `call` | A button that rings people for a voice or video call. Use it on a contact page, a service page or a members list. | `to` (string): Who it rings: 'owner' (default), 'pick' (choose from the space's members) or a role id (everyone with that role) `video` (boolean): Start with cameras on, default false `label` (string): Button words | Ready |
| `media.gallery` | A grid of photos, videos, audio and files from the space's media library, with an upload button for people allowed to add. Tap an item to open it. | `folder` (string): Which folder of the library, default 'main'. Any short name; recordings go to 'recordings' `kinds` (string[]): Only these kinds: image, video, audio, file. Default all `upload` (boolean): Show the upload button to people with 'add', default true `columns` (number): Columns on a phone, default 3 `empty` (string): Words when the folder is empty `label` (string): Upload button words | Ready |
| `media.player` | Plays the newest video or audio (or shows the newest photo) from a media folder. Good for a latest episode, a welcome video or a song. | `folder`* (string): Which folder of the media library `kind` (string): 'video' (default), 'audio' or 'image' `title` (string): Words above the player | Ready |
| `screen.pair` | Connects a TV, projector or any big screen. Open the TV link on the screen (any browser, a smart TV, or a laptop with AirPlay or Chromecast), then type the code it shows here. The screen joins the space with the role you pick and shows a page. | `page` (string): Page id the screen opens on, default this page's first other page `role` (string): Role the screen gets in the space, default 'guest' `pair` (string): Permission needed to connect screens, default 'manage' `label` (string): Button words | Ready |
| `screen.remote` | A remote for the space's connected screens: pick what each one shows. Phones become controllers. | `pages` (string[]): Page ids people can put on the screen, default every page except HQ `control` (string): Permission needed to use the remote, default 'play' `title` (string): Heading | Ready |
| `hq.pages` | HQ: every page of the space, to open one. | none | Ready |
| `hq.library` | HQ: the kit library, to add or remove kits (optional features). | none | Ready |
| `hq.history` | HQ: the list of saved versions with undo. | none | Ready |
| `hq.proposals` | HQ: changes the AI proposed, waiting for Approve. | `quiet` (boolean): Show nothing while no change waits (HQ's Overview) | Ready |
| `hq.styles` | HQ: the style library (Classic, Futuristic and the space's own) and the scheme, to switch the look. | none | Ready |
| `hq.access` | HQ: who can find and join the space (visibility, join rule), join requests, and the roles. | none | Ready |
| `hq.connections` | HQ: the space's own AI key, its key vault, access tokens for MCP clients, and connections to outside MCP servers and APIs. | `part` (string): Show one part: 'ai' (the AI key), 'servers' (MCP servers, APIs and their keys) or 'tokens' (access tokens). Default: all | Ready |
| `hq.blueprint` | HQ: the whole blueprint as readable JSON. | none | Ready |
| `ledger.balance` | The viewer's balance in one unit (coins, points, credits…) or in every unit of the space. | `unit` (string): A unit id; leave out to show every unit `label` (string): Words under the number, e.g. 'Your coins' | Ready |
| `ledger.send` | A form to send some of a unit to another person by email. Only works for units with transfers on. | `unit`* (string): A unit id from ledger.units, e.g. 'coins' `title` (string): Heading, e.g. 'Send coins' `button` (string): Button words, e.g. 'Send' `done` (string): Words shown after sending | Ready |
| `ledger.give` | For whoever may issue a unit: create some and give it to a person by email, or take some back. | `unit`* (string): A unit id from ledger.units, e.g. 'coins' `title` (string): Heading, e.g. 'Give coins' | Ready |
| `ledger.history` | The viewer's latest movements in and out (received, sent, spent, rewards). | `unit` (string): A unit id; leave out for every unit `limit` (number): How many, default 20 `empty` (string): Words when there's nothing yet | Ready |
| `ledger.leaderboard` | A ranking of people: by a unit (who holds the most, received the most ever, or received the most this day/week/month/year), or by how often they did something (an event: post, view, play, record, join or a custom one). Give either unit or event. | `unit` (string): A unit id to rank by, e.g. 'coins' `event` (string): An event name to rank by instead, e.g. 'post' or 'play' (needs the Leaderboards kit) `by` (string): 'balance' (default) or 'earned' `window` (string): 'all' (default), 'day', 'week', 'month' or 'year'; ranks by what was received in that time `limit` (number): How many places, default 10 `title` (string): Heading `empty` (string): Words when nobody is ranked yet | Ready |
| `ledger.levels` | The space's levels (tiers, VIP, memberships, plans) with their perks and price, and a button to get or renew one. | `only` (string[]): Level ids to show; leave out for all `get` (string): Button words to get a level, e.g. 'Join' `renew` (string): Button words to renew a level that runs out, e.g. 'Renew' `held` (string): Words on a level the viewer holds, e.g. 'You're in' `byHand` (string): Words on a level that can't be bought, e.g. 'By invite' | Ready |
| `ledger.marks` | The viewer's marks in this space: their top level and badges (verified, VIP…). | `empty` (string): Words when they have none | Ready |
| `ledger.grants` | For whoever may give a level or badge: who holds it, give it to someone by email (optionally for some days), or take it away. Use for verified, VIP, staff and the like. | `kind`* (string): 'level' or 'badge' `key`* (string): The level or badge id `title` (string): Heading | Ready |
| `invites.code` | The viewer's own invite code for this space, as a link, a QR code and a share button, with what inviting earns. Needs /referrals/codes. | `title` (string): Heading, e.g. 'Invite friends' `qr` (boolean): Show the QR code (default true) `design` (object): How the QR code looks: the same settings as the qr block's design (look, dots, eyes, shape, ink, paper, logo…) `message` (string): Share words; {space}, {code} and {link} are filled in. Default 'Join me in {space}: {link}' `button` (string): Share button words, default 'Share' `empty` (string): Words for people who can't get a code | Ready |
| `invites.list` | Who the viewer invited and whether each counted, with totals; or, for people who can invite or manage, who invited whom in the whole space. | `who` (string): 'mine' (default) or 'everyone' (only shows to roles that can invite or manage) `title` (string): Heading `limit` (number): How many, default 50 `empty` (string): Words when nobody has joined yet | Ready |
| `invites.redeem` | A box to type or paste an invite code or link and join that space. | `title` (string): Heading, e.g. 'Have a code?' `button` (string): Button words, default 'Join' `scan` (boolean): Also offer to scan a QR code with the camera (default true on phones) | Ready |
| `qr` | A QR code for any link or text: a page of this space, a website, wifi details, a ticket number. Phones open it with their camera. | `value` (string): The text or full link to encode (up to 300 characters) `page` (string): Or a page id of this space: makes a link that opens that page `label` (string): Words under the code `size` (number): Width in points, 96-320 (default 200) | Ready |
| `qr` | A designed QR code that phones open with their camera: any link or text, a page of this space, the space itself, its public web address or the viewer's own invite link. Looks from square to round dots to a round badge like Apple's, in the style's colours or its own, with a logo and caption; people can share, save and print it. | `value` (string): The text or full link to encode (up to 600 characters). Wi-Fi: WIFI:T:WPA;S:;P:;; `page` (string): Or a page id of this space: makes a link that opens that page (with link web: that page's public web address) `link` (string): Or what to link to: space (opens this space in Zvessa), web (its public web address, needs the Public links kit) or invite (the viewer's own invite link, needs /referrals/codes) `label` (string): Words printed under the code, up to 60 characters (same as design.caption) `title` (string): Heading above the code `size` (number): Width in points, 96-360 (default 220) `design` (object): How it looks, every setting optional. look: A ready look: classic, rounded, round, dots, circle (circle is a round badge, like Apple's; default classic). The settings below change it. dots: Module shape: square, soft, round, dot, diamond. eyes: The three corner squares: square, round, circle, leaf. shape: The paper's outline: square, rounded, circle. ink: Colour of the code: a style colour name (accent, text, qrInk…), #hex or mix()/alpha(). Default qrInk if the style has it, else black. Kept dark enough to scan. ink2: Second colour for a gradient. gradient: none, linear, radial (needs ink2). angle: Gradient angle in degrees, 0 = left to right (default 45). eyeInk: Colour of the corner squares (default the ink). paper: Background colour: default qrPaper if the style has it, else white. Kept light so cameras read it. border: Colour of a thin edge around the paper; leave out for none. fill: For the circle shape: fill the ring around the code with decorative dots (default true). logo: In the middle: none, zvessa, initial, image. zvessa is the Zvessa mark; initial is a letter (initial); image is logoImage. A logo raises error correction to H. logoImage: https:// address of a square picture for logo image. initial: One to two letters for logo initial (default the first letter of the space). caption: Words printed under the code, inside the design (shared and printed with it), up to 60 characters. ecc: Error correction L, M, Q or H: how much can be covered and still read (7%, 15%, 25%, 30%). Default M, or H with a logo. margin: Blank space around the code in modules, 2-6 (default 3) `actions` (boolean): Show Share (and Save/Print on phones), Copy link and Print (web) buttons (default true) `share` (string): Share button words, default 'Share' `copy` (string): Copy button words, default 'Copy link' `print` (string): Print button words (web), default 'Print' | Ready |
| `ai.chat` | The builder AI chat: people ask, the reply streams in word by word with a progress line and a Stop button, and proposals show Approve and Discard. Only people who can manage the space see it. | `placeholder` (string): Words in the empty box (default: words.chatPlaceholder) `model` (string): Model size for messages sent here: auto (default, follows /ai/model), small or large `steps` (boolean): Show what the AI is doing while it works (default true) | Ready |
| `ai.model` | HQ: choose the AI's model size (auto, small and fast, large and careful), and switch streaming and finish pushes on or off. Writes /ai. | `title` (string): Heading, default 'AI' | Ready |
| `ai.approval` | HQ: how the AI's changes are approved. Ask (default): each change waits for Approve. Auto: changes apply at once with an Undo note, except who can come in, connections, bots and the web, which still ask. Only the owner turns Auto on. Writes /ai/approval. | `title` (string): Heading, default 'Approving changes' | Ready |
| `ai.notes` | The notes the AI keeps on how the viewer likes things built: read, add and delete them, and switch notes on or off (/ai/remember). | `title` (string): Heading, default 'What the AI remembers' | Ready |
| `ai.jobs` | The space's latest AI work (chat replies, bots): what each is doing now, how it ended, and Stop for running ones. | `title` (string): Heading, default 'AI activity' `limit` (number): How many to show, default 10 | Ready |
| `ai.setup` | The 'Connect your AI' step: people who manage the space paste their Claude API key right here (sealed in the vault, never shown again); others see who to ask. Once the AI is connected, managers see the key here with Replace and Remove (and a new one is asked for if Claude refuses it); hideWhenReady true hides it instead. Good at the top of HQ, a welcome page or an onboarding page. | `title` (string): Heading, default the word aiSetupTitle ('Connect your AI') `about` (string): Words under the heading, default the word aiSetupAbout `hideWhenReady` (boolean): Hide once a key is connected, default false (managers see Replace and Remove) | Ready |
| `ai.allowance` | How many AI messages the viewer has left today in this space (or that there's no limit), and when they come back. Put it near an AI chat or on a profile page. | `label` (string): Words above the number, e.g. 'Your AI today' `meter` (boolean): Show a bar of used vs. allowed, default true | Ready |
| `hq.aiUsage` | HQ, for people who manage the space: today's AI use by person, the space and its bots, the limits that apply (the space's /ai/limits and, on the server's key, the server's), and whose key pays. | `limit` (number): How many people to list, default 20 | Ready |
| `setup.steps` | A calm 'Get ready' card: a few first steps with a progress bar, for people who manage the space (others never see it). Each step ticks itself when it's done; the card goes away when all are done or the person taps ×. Its children are setup.step blocks; with none it shows: connect your AI, ask for a first change, make it yours, invite someone. Good at the top of the home list (a child of the chats block), HQ or a welcome page. *Holds children.* | `title` (string): Heading, default 'Get ready' `hideable` (boolean): Show × to hide it on this device, default true | Ready |
| `setup.step` | One step in a setup.steps card. It ticks itself when its check is true. | `check`* (string): What makes it done: 'ai' (The space's AI is connected (a key works)), 'ask' (Someone asked the AI something), 'change' (The space was changed after it was made (a page edit, an approved AI change, a kit)), 'invite' (Someone else joined, or an invite link was made), 'public' (The space is open to more than its members (link, friends or public)), 'kit' (A kit was added after the space was made) `label`* (string): What to do, a few words (Connect your AI) `hint` (string): One short line under it `icon` (string): An icon name `href` (string): Where tapping it goes: '#page-id', 'ai', 'chat:' or a web link | Ready |
| `feed.timeline` | Posts of a feed (blueprint.feeds) with likes, replies, reposts and a box to post. Layouts: list (X/Threads style), cards (Instagram style), grid (gallery, profile grid), stories (a row of recent posts that disappear). | `feed`* (string): A blueprint.feeds id, or '*' for every feed of the space `layout` (string): 'list' (default), 'cards', 'grid' or 'stories' `sort` (string): 'new' (default), 'active' (latest reply first, for forums) or 'top' (most liked this week) `scope` (string): 'all' (default), 'following' (people I follow), 'mine', or 'person' with the person prop `person` (string): A @handle, when scope is 'person' `tag` (string): Only posts with this #tag (an album, a topic) `mediaOnly` (boolean): Only posts with photos or videos `composer` (boolean): Show the box to post at the top, default true `placeholder` (string): Words in the empty post box `empty` (string): Words when there are no posts `columns` (number): Grid columns, 2 to 5, default 3 `pageSize` (number): Posts per page, 5 to 50, default 20 `tabs` (boolean): 'For you' and 'Following' tabs over the posts, like X | Ready |
| `feed.composer` | Just the box to write a post in a feed, with photos and videos when the feed takes them. | `feed`* (string): A blueprint.feeds id `placeholder` (string): Words in the empty post box | Ready |
| `feed.profile` | A person's profile: photo, name, @handle, bio, link, follower counts, a follow button and their posts. | `person` (string): 'owner' (the space's owner, default), 'me' (whoever looks), or a @handle `feed` (string): Which feed's posts to show: a feed id or '*' (default) `layout` (string): 'grid' (default) or 'list' `posts` (boolean): Show their posts under the profile, default true | Ready |
| `feed.people` | A list of people: who to follow in this space, the person's followers or following, or their friends. | `list` (string): 'suggested' (default), 'followers', 'following' (of the person looking) or 'friends' `title` (string): Heading `limit` (number): How many, default 10 | Ready |
| `feed.activity` | The person's activity: friend requests to answer, then likes, replies, mentions, reposts and new followers, grouped like Instagram. | `title` (string): Heading | Ready |
| `feed.search` | Search posts by words and #tags, with the week's trending tags. | `feed` (string): A feed id to search in, or '*' (default) for all `trending` (boolean): Show trending tags, default true | Ready |
| `feed.saved` | Posts the person saved for later. Private to them. | `title` (string): Heading | Ready |
| `feed.sources` | For people who manage the space: the outside feeds (X, Bluesky, Mastodon, Instagram, Threads, YouTube, RSS) a feed pulls in, how the last pull went, and adding, pausing or removing them. | `feed` (string): A feed id, or '*' (default) for all | Ready |
| `reviews.summary` | The rating at a glance: the average in big stars, how many reviews, a bar for each star (tap one to read those), the average of each aspect, and a Write a review button. Put it at the top of a services, product, place, game or profile page. | `reviews`* (string): A blueprint.reviews id (a review board) `subject` (string): What the reviews are about: a subject id of the board, 'record:' (a record of the board's subjectsFrom collection), '@handle' (a person), any short id (app:weather, deep-clean), 'main' for the board as a whole, or '*' for every subject. Default '*' when reading; writing then asks which one (or uses 'main' when the board has no list) `heading` (string): Heading over it, e.g. 'What people say' `bars` (boolean): Show the bar for each star. Default true `aspects` (boolean): Show each aspect's average. Default true `write` (string): Words on the write button. Default 'Write a review'; empty = no button `look` (string): 'card' (default) or 'plain' | Ready |
| `reviews.list` | Reviews with stars, words, photos, the verified mark, helpful votes, answers from the owner, report, and edit or delete your own. Chips to sort (most helpful, newest, highest, lowest) and filter (stars, with photos, verified). People who manage can hide, show and answer. Your own review shows first. | `reviews`* (string): A blueprint.reviews id (a review board) `subject` (string): What the reviews are about: a subject id of the board, 'record:' (a record of the board's subjectsFrom collection), '@handle' (a person), any short id (app:weather, deep-clean), 'main' for the board as a whole, or '*' for every subject. Default '*' when reading; writing then asks which one (or uses 'main' when the board has no list) `sort` (string): 'helpful' (default), 'new', 'high' or 'low' `filters` (boolean): Show the sort and filter chips. Default true `write` (string): Words on a write button over the list. Default none `pageSize` (number): Reviews per page, 3 to 50. Default 10 `heading` (string): Heading over the list `empty` (string): Words when there are no reviews `look` (string): 'cards' (default, each review on a card) or 'list' (rows with lines between) `subjects` (boolean): Show what each review is about. Default true when subject is '*' | Ready |
| `reviews.stars` | A small rating line: ★ 4.6 (128). Tap it to read and write reviews in a sheet. Put it under a heading, a product or a service, in a hero or on a profile. | `reviews`* (string): A blueprint.reviews id (a review board) `subject` (string): What the reviews are about: a subject id of the board, 'record:' (a record of the board's subjectsFrom collection), '@handle' (a person), any short id (app:weather, deep-clean), 'main' for the board as a whole, or '*' for every subject. Default '*' when reading; writing then asks which one (or uses 'main' when the board has no list) `count` (boolean): Show how many reviews. Default true `size` (number): Star size in points, 12 to 40. Default 16 `empty` (string): Words when there are no reviews yet. Default: hidden until the first one `open` (boolean): Tap opens the reviews sheet. Default true | Ready |
| `reviews.write` | A card that asks for a rating: a question and a row of stars to tap, which opens the full review (words, photos, aspects). Shows your own review once you wrote one, with Edit. | `reviews`* (string): A blueprint.reviews id (a review board) `subject` (string): What the reviews are about: a subject id of the board, 'record:' (a record of the board's subjectsFrom collection), '@handle' (a person), any short id (app:weather, deep-clean), 'main' for the board as a whole, or '*' for every subject. Default '*' when reading; writing then asks which one (or uses 'main' when the board has no list) `question` (string): The question, e.g. 'How was your visit?' `note` (string): A line under the question | Ready |
| `reviews.top` | A ranked list of the board's subjects by rating: top rated services, best games, favourite places, most loved mini-apps. Each row shows stars and the count; tap one for its reviews. | `reviews`* (string): A blueprint.reviews id (a review board) `heading` (string): Heading. Default 'Top rated' `limit` (number): How many, 3 to 50. Default 10 `min` (number): Fewest reviews to be listed, 1 to 100. Default 1 `sort` (string): 'best' (highest average, default) or 'most' (most reviews) | Ready |
| `reviews.queue` | For people who manage: reviews waiting for approval and reviews people reported, to approve, hide or keep. Shows nothing to anyone else. | `reviews` (string): A blueprint.reviews id, or '*' (default) for every board | Ready |
| `hero` | The top of a page: a big round picture or emoji, a title, a line under it, and a row of buttons (its child blocks: button, call or tile). A profile header, a business card, a welcome. *Holds children.* | `title` (string): Big title. Empty uses the space's name `subtitle` (string): A line or two under the title `image` (string): https URL of a round picture (logo, face) `icon` (string): An emoji or a letter shown when there's no picture `cover` (string): Colour of the band behind it: a style colour role (accent, accent2, gold…) or a colour. 'none' for no band `align` (string): 'center' (default) or 'left' `look` (string): 'band' (default: a colour band and a round picture) or 'card' (the chat app's page header: a rounded logo, the space's name in small capitals over the title, a soft glow of the cover colour) `eyebrow` (string): look 'card': the small line over the title. Empty uses the space's name | Ready |
| `tiles` | A grid of app-style tiles (its children are `tile` blocks): a games hub, an apps page, quick actions, a link-in-bio. Big tiles show a note under the name. *Holds children.* | `title` (string): Section title over the grid `columns` (number): Tiles per row on a phone, 2 to 5. Default 4 (2 for big tiles) `size` (string): 'small' (icon and name, default) or 'large' (a card with a note) `tone` (string): How the icons are painted: 'solid' (default), 'soft' (a light tint with the icon in colour, like the chat app's app grid) or 'glossy' (shiny, eye-catching) `frame` (string): 'card' puts the grid on a card (the chat app's sections), 'none' (default) draws it on the page `heading` (string): How the title looks: 'title' (default, big) or 'label' (small capitals over the card) | Ready |
| `tile` | One tile in a `tiles` grid: an icon on a coloured square and a name. Opens a page, a conversation, a link or the AI. | `label`* (string): Name under the icon `icon` (string): An icon name (game, trophy, chat, phone, calendar, coin…) or an emoji `href` (string): '#page-id' opens a page, 'https://…' a website, 'chat:' a conversation (e.g. chat:support, chat:general), 'ai' the AI chat (people who manage), 'mailto:' or 'tel:' a mail or a phone call `tint` (string): The square's colour: a style colour role or a colour. Default accent `note` (string): A short line: under the name on large tiles, a small status line on small ones (12 players, Fridays 8 PM) `badge` (string): A small label on the corner (New, 2). 'Live' shows a red LIVE pill | Ready |
| `links` | A section of a service page or link list (its children are `link` blocks): services with prices, support links, partners, a menu, social links. People who manage the space edit it in place. *Holds children.* | `title` (string): Section title `note` (string): A line under the title `layout` (string): 'list' (default, rows on a card), 'cards' (big cards with a button), 'grid' (two across), 'icons' (the chat app's app grid: icons on one card, four across) or 'banners' (big pictures swiped sideways) `heading` (string): How the title looks: 'title' (default, big) or 'label' (small capitals over the card, like the chat app) `tone` (string): How the entries' icons are painted: 'solid' (default), 'soft' (a light tint with the icon in colour) or 'glossy' | Ready |
| `link` | One entry in a `links` section: a service, a product, a support link, a social profile. | `title`* (string): Name `note` (string): A short description `icon` (string): An icon name or an emoji `image` (string): https URL of a picture (cards, grid and banners show it) `tint` (string): The icon's colour (and a banner's, without a picture): a style colour role or a colour. Default accent `href` (string): '#page-id' opens a page, 'https://…' a website, 'chat:' a conversation (e.g. chat:support, chat:general), 'ai' the AI chat (people who manage), 'mailto:' or 'tel:' a mail or a phone call `price` (string): Price as people should read it ($40, From $120, Free) `action` (string): Button words (Book, Order, Open). Default: an arrow `tag` (string): A small label (New, Popular, Team only) | Ready |
| `chats` | The full-screen chat list of a messaging app: every direct chat, group, channel and case with the newest message, unread counts, pinned on top, search, filters and New chat. Put it alone on a page that is a tab. Its children are custom rows for pages when the app has no tab bar: a `lobby.row` with href '#games' draws the games row with poster art. A `setup.steps` child is a 'Get ready' card at the top of the list for the people who manage the space. *Holds children.* | `title` (string): Title, default 'Chats' `show` (string[]): Kinds to list: direct, group, stream, case. Default all `empty` (string): Words when there are no chats yet `search` (string): 'header' (default: a search button in the bar opens a search line), 'field' (always shown) or 'off' `filters` (boolean): Filter chips (All, Unread, Groups, Channels…), default true `compose` (boolean): A New chat button in the bar, default true `rowsTop` (string[]): Page ids (or 'ai' for the AI chat) shown as rows above the chats, like HQ in the chat app. Only when the app has no tab bar (dashboard.nav 'list') `rowsUnder` (string[]): Page ids shown as rows under the first chat (services, apps, games…). Only when the app has no tab bar. Leave both out to list every page of the dashboard | Ready |
| `calls.recent` | The call log of a phone app: recent calls in and out (missed in red), tap to call back. Its children are `call` blocks shown as the buttons on top (Call us, Video, New call); calling back goes through whichever of them can reach that person. *Holds children.* | `title` (string): Section title, default 'Recent' `empty` (string): Words when there are no calls yet | Ready |
| `hq.overview` | HQ: the space at a glance for people who manage it. Members, new members, active people, messages, page views and plays over the last days, each with the change from the days before. | `days` (number): How many days, 7 (default), 14 or 30 `title` (string): Heading, default 'Overview' | Ready |
| `hq.navigation` | HQ: how people move around the app on a phone. Off (the chat app's way): the home is the chat list, and the other pages are rows in it. On: a bottom tab bar with the first five pages. Also orders the pages. Only people who manage the space see it. | `title` (string): Heading, default 'Navigation' | Ready |
| `hq.starters` | HQ: the starter templates (Business, Personal…) with a button to switch the space to one. Switching adds the missing kits, puts the template's pages in place and sets the tabs and look, as one version that can be undone. Only people who manage the space see it. | `title` (string): Heading, default 'Starters' | Ready |
| `switcher` | A segmented control that shows one of its child blocks at a time: For you / Following over two timelines, Posts / Photos on a profile, Activity / Saved, Menu / Reviews. Each child is one segment, named by labels in the same order. Only the open one is drawn. With one child left it shows that child without the control. *Holds children.* | `labels` (string[]): The segments' names, one per child in order. A missing one uses the child's title prop, else 'Part 2' `look` (string): 'segments' (default, a rounded segmented control), 'tabs' (words with an underline, like a profile) or 'pills' (separate rounded chips) `start` (number): Which segment is open first, counting from 0. Default 0 | Ready |
| `poster` | One bright game card: art on a gradient, the name in big letters, a badge, a corner chip and a line under the name. Tap opens a sheet with how to play and a big Play button (the game opens full screen), or goes to `href`. Put posters in `posters`, `showcase` or `lobby.row`. | `label`* (string): The name on the card `game` (string): A blueprint.games id: Play starts or joins it, and the card shows how many are playing `href` (string): Instead of a game: where the card goes ('#page-id', https link, chat:, ai) `line` (string): A short line under the name (Rounds ~20 s, Teams of 2+, New this week) `about` (string): One or two sentences on the sheet. Default: the game's about `art` (string): The picture: an art name (trophy, board, survey, mask, sketch, bubble, dice, wheel, bingo, cards, bomb, gift, detective, number, poll, buzzer, crown, mic, tv, rocket), an emoji, or an https picture. Default: art that fits the game `palette` (string): The colours: sunset, ocean, grape, lagoon, ember, lime, candy, night, gold, mint, berry, sky. Default: one picked for the game `colors` (string[]): Your own 2 or 3 colours instead of a palette: colours (#ff6b4a) or style colour roles (accent, gold) `badge` (string): A small label top-left: HOT, NEW, LIVE (pulses) or any short word. A game being played shows LIVE by itself `corner` (string): A chip top-right: the hook (x2 points, 2-20 players, 500 coins) `steps` (string[]): How to play, up to 3 short steps, on the sheet. Default: from the game's rounds `tip` (string): One useful tip on the sheet and the intro `opens` (string): 'sheet' (default: details first), or 'play' (straight into the game) | Ready |
| `posters` | A grid of `poster` cards, the main shelf of a games page, with a switch for small, medium or large cards. Cards pop in one after another. *Holds children.* | `title` (string): Section title `note` (string): A line under the title `size` (string): 'small' (3 across), 'medium' (2 across, default) or 'large' (1 across, wide) `sizes` (boolean): Show the size switch (default true) | Ready |
| `showcase` | A featured carousel of `poster` cards as big wide cards with a PLAY button. Swipes, and moves on by itself every few seconds. *Holds children.* | `title` (string): Section title (default none) `every` (number): Seconds before it moves on by itself, 0 to stay still. Default 5 | Ready |
| `game.live` | Games people have started and others can still join, with how many are in and a Join button. Hidden when nothing is on. | `title` (string): Section title, default 'Live now' `games` (string[]): Only these blueprint.games ids. Default: every game on the page's posters, or all games | Ready |
| `game.winners` | The latest winners scrolling by on their own: who won which game, their points, when. | `title` (string): Section title, default 'Big wins' `games` (string[]): Only these blueprint.games ids. Default all `max` (number): How many winners, 3 to 30. Default 12 | Ready |
| `game.record` | Each person's own record per game: games played, games won and best score. Everyone sees only their own. | `title` (string): Section title, default 'Your record' `games` (string[]): Only these blueprint.games ids. Default all | Ready |
| `lobby.header` | The top of a games page: a logo square, the page's big name, a line under it, and the person's coins (Ledger) counting up. | `title` (string): Big name, when it differs from the page title (the page title already shows above). Default none `tagline` (string): A line under the name `image` (string): https URL of a square logo `icon` (string): An art name, icon name or emoji in the logo square when there's no image. Default sparkle `palette` (string): Colours of the logo square (a palette name). Default the accent `unit` (string): A ledger unit id to show the person's balance; 'none' to hide. Default the space's first unit | Ready |
| `lobby.row` | A row for a chat list or a menu that opens a games page: a 2×2 mosaic of the page's poster art, the name, the games' names and a count. Put it at the top of a chat list page. | `title`* (string): Name on the row (e.g. 'Game night') `href`* (string): '#page-id' of the games page `line` (string): Line under the name. Default the posters' names `count` (boolean): Show the number of games (default true) | Ready |
| `game.frame` | A game in its own full play screen: a branded intro, a header with the round and timer, a progress bar, your results as chips, the game in the middle and one big button. Posters open this by themselves; put it on a page to give one game its own page. | `game`* (string): A blueprint.games id `label` (string): Name shown. Default the game's name `line` (string): Line under the name on the intro `art` (string): The picture: an art name (trophy, board, survey, mask, sketch, bubble, dice, wheel, bingo, cards, bomb, gift, detective, number, poll, buzzer, crown, mic, tv, rocket), an emoji, or an https picture. Default: art that fits the game `palette` (string): The colours: sunset, ocean, grape, lagoon, ember, lime, candy, night, gold, mint, berry, sky. Default: one picked for the game `colors` (string[]): Your own 2 or 3 colours instead of a palette: colours (#ff6b4a) or style colour roles (accent, gold) `tip` (string): One tip on the intro `intro` (boolean): Play the short branded intro (default true) | Ready |
| `records.composer` | A writing card: a question, a quick row of the chip field's options (tap a mood) and a big button that opens a full-screen writing sheet for a new record (title, body, chip, day, tags). A journal's 'How was today?', a gratitude log, a quick note, a workout log. | `collection`* (string): A blueprint.collections id `title` (string): Field id drawn as the title (a text field). Default: a field called title. 'none' for no title `body` (string): Field id drawn as the body (a longtext field). Default: the first longtext `chip` (string): Field id drawn as a coloured chip (a choice field: mood, status, rating). Default: the first choice. 'none' for no chip `date` (string): Field id of the record's day (a date field). Default: the first date field, else the day it was written `tags` (string): Field id of #tags (a text field). Default: a field called tags. #tags written in the body count too `field` (string): A text field that holds the prompt a record was written from (records.prompt fills it; others show it when there's no title) `icons` (string[]): An emoji or icon name for each option of the chip field, in order (😄 🙂 😐 😔) `tones` (string[]): A colour for each option of the chip field, in order: style colour roles (tint1, success…) or colours. Default tint1, tint2… `write` (string): Words on the button that opens the writing sheet, e.g. 'Write'. Empty = no button `placeholder` (string): Grey words in the empty body, e.g. 'Dear diary…' `fields` (string[]): Fields the writing sheet asks for, in order. Default all `defaults` (string): JSON object of values set on every new record `saved` (string): Words shown after saving, e.g. 'Saved. {streak} days in a row!' ({streak} is the current streak) `question` (string): The card's question, e.g. 'How was today?' `note` (string): A line under the question `quick` (boolean): Show the chip field's options to tap on the card. Default true | Ready |
| `records.timeline` | A collection's records as a timeline like a diary: grouped under day or month headers, each with its date, title, a few lines, chip and tags. Search box, tap to read in full, edit, delete or share, and a Write button. Journals, notes, logs, changelogs, a reading list. | `collection`* (string): A blueprint.collections id `title` (string): Field id drawn as the title (a text field). Default: a field called title. 'none' for no title `body` (string): Field id drawn as the body (a longtext field). Default: the first longtext `chip` (string): Field id drawn as a coloured chip (a choice field: mood, status, rating). Default: the first choice. 'none' for no chip `date` (string): Field id of the record's day (a date field). Default: the first date field, else the day it was written `tags` (string): Field id of #tags (a text field). Default: a field called tags. #tags written in the body count too `field` (string): A text field that holds the prompt a record was written from (records.prompt fills it; others show it when there's no title) `icons` (string[]): An emoji or icon name for each option of the chip field, in order (😄 🙂 😐 😔) `tones` (string[]): A colour for each option of the chip field, in order: style colour roles (tint1, success…) or colours. Default tint1, tint2… `write` (string): Words on the button that opens the writing sheet, e.g. 'Write'. Empty = no button `placeholder` (string): Grey words in the empty body, e.g. 'Dear diary…' `fields` (string[]): Fields the writing sheet asks for, in order. Default all `defaults` (string): JSON object of values set on every new record `saved` (string): Words shown after saving, e.g. 'Saved. {streak} days in a row!' ({streak} is the current streak) `group` (string): 'month' (default) or 'day' headers `search` (boolean): Show a search box. Default true `lines` (number): Lines of body shown per record, 0 to 8. Default 3 `limit` (number): How many to show before 'Show more', 5 to 100. Default 30 `empty` (string): Words when there are no records yet `heading` (string): Heading over the timeline | Ready |
| `records.calendar` | A month calendar of a collection: each day with records is filled with the colour of its chip (a mood calendar, a habit tracker, a booking or workout calendar). Swipe or tap the arrows for other months; tap a day to see and edit its records, or write one for that day. | `collection`* (string): A blueprint.collections id `title` (string): Field id drawn as the title (a text field). Default: a field called title. 'none' for no title `body` (string): Field id drawn as the body (a longtext field). Default: the first longtext `chip` (string): Field id drawn as a coloured chip (a choice field: mood, status, rating). Default: the first choice. 'none' for no chip `date` (string): Field id of the record's day (a date field). Default: the first date field, else the day it was written `tags` (string): Field id of #tags (a text field). Default: a field called tags. #tags written in the body count too `field` (string): A text field that holds the prompt a record was written from (records.prompt fills it; others show it when there's no title) `icons` (string[]): An emoji or icon name for each option of the chip field, in order (😄 🙂 😐 😔) `tones` (string[]): A colour for each option of the chip field, in order: style colour roles (tint1, success…) or colours. Default tint1, tint2… `write` (string): Words on the button that opens the writing sheet, e.g. 'Write'. Empty = no button `placeholder` (string): Grey words in the empty body, e.g. 'Dear diary…' `fields` (string[]): Fields the writing sheet asks for, in order. Default all `defaults` (string): JSON object of values set on every new record `saved` (string): Words shown after saving, e.g. 'Saved. {streak} days in a row!' ({streak} is the current streak) `heading` (string): Heading over the calendar `week` (string): First day of the week: 'mon' (default) or 'sun' `empty` (string): Words under a day with no records | Ready |
| `records.streak` | Days in a row with at least one record, with a flame, the longest streak, how many in total and this week as dots. Brings people back every day: journals, habits, workouts, practice, reading. | `collection`* (string): A blueprint.collections id `title` (string): Field id drawn as the title (a text field). Default: a field called title. 'none' for no title `body` (string): Field id drawn as the body (a longtext field). Default: the first longtext `chip` (string): Field id drawn as a coloured chip (a choice field: mood, status, rating). Default: the first choice. 'none' for no chip `date` (string): Field id of the record's day (a date field). Default: the first date field, else the day it was written `tags` (string): Field id of #tags (a text field). Default: a field called tags. #tags written in the body count too `field` (string): A text field that holds the prompt a record was written from (records.prompt fills it; others show it when there's no title) `label` (string): What a day counts as, e.g. 'days of writing'. Default 'days in a row' `icon` (string): Emoji or icon name, default 🔥 `goal` (number): A goal in days (7, 30, 100…): shows a progress bar towards it `nudge` (string): Words shown while today is still empty, e.g. 'Write today to keep it going' `done` (string): Words shown once today has a record | Ready |
| `records.memories` | 'On this day': records from a week, a month, six months and every year ago today, as cards to open. Hidden when there are none (or shows `empty`). Journals, photo diaries, logs. | `collection`* (string): A blueprint.collections id `title` (string): Field id drawn as the title (a text field). Default: a field called title. 'none' for no title `body` (string): Field id drawn as the body (a longtext field). Default: the first longtext `chip` (string): Field id drawn as a coloured chip (a choice field: mood, status, rating). Default: the first choice. 'none' for no chip `date` (string): Field id of the record's day (a date field). Default: the first date field, else the day it was written `tags` (string): Field id of #tags (a text field). Default: a field called tags. #tags written in the body count too `field` (string): A text field that holds the prompt a record was written from (records.prompt fills it; others show it when there's no title) `icons` (string[]): An emoji or icon name for each option of the chip field, in order (😄 🙂 😐 😔) `tones` (string[]): A colour for each option of the chip field, in order: style colour roles (tint1, success…) or colours. Default tint1, tint2… `heading` (string): Heading, default 'On this day' `empty` (string): Words when there's nothing from before. Unset = the block hides itself | Ready |
| `records.prompt` | A daily writing prompt on a card: the same question all day, a new one tomorrow, a button for another one, and Write, which opens the writing sheet with the prompt saved on the record. Journals, reflection, team check-ins, kids' diaries. | `collection`* (string): A blueprint.collections id `title` (string): Field id drawn as the title (a text field). Default: a field called title. 'none' for no title `body` (string): Field id drawn as the body (a longtext field). Default: the first longtext `chip` (string): Field id drawn as a coloured chip (a choice field: mood, status, rating). Default: the first choice. 'none' for no chip `date` (string): Field id of the record's day (a date field). Default: the first date field, else the day it was written `tags` (string): Field id of #tags (a text field). Default: a field called tags. #tags written in the body count too `field` (string): A text field that holds the prompt a record was written from (records.prompt fills it; others show it when there's no title) `write` (string): Words on the button that opens the writing sheet, e.g. 'Write'. Empty = no button `placeholder` (string): Grey words in the empty body, e.g. 'Dear diary…' `fields` (string[]): Fields the writing sheet asks for, in order. Default all `defaults` (string): JSON object of values set on every new record `saved` (string): Words shown after saving, e.g. 'Saved. {streak} days in a row!' ({streak} is the current streak) `prompts` (string[]): The prompts to rotate through. Default a set of everyday journal prompts `label` (string): Small heading over the prompt, default 'Today's prompt' `shuffle` (string): Words on the 'another one' button. Empty = no button | Ready |
| `records.stats` | Insights about a collection over the last days: how many records, words written, best streak, and a bar for each option of the chip field (how your moods add up). Journals, habits, feedback, support tickets. | `collection`* (string): A blueprint.collections id `title` (string): Field id drawn as the title (a text field). Default: a field called title. 'none' for no title `body` (string): Field id drawn as the body (a longtext field). Default: the first longtext `chip` (string): Field id drawn as a coloured chip (a choice field: mood, status, rating). Default: the first choice. 'none' for no chip `date` (string): Field id of the record's day (a date field). Default: the first date field, else the day it was written `tags` (string): Field id of #tags (a text field). Default: a field called tags. #tags written in the body count too `field` (string): A text field that holds the prompt a record was written from (records.prompt fills it; others show it when there's no title) `icons` (string[]): An emoji or icon name for each option of the chip field, in order (😄 🙂 😐 😔) `tones` (string[]): A colour for each option of the chip field, in order: style colour roles (tint1, success…) or colours. Default tint1, tint2… `days` (number): How many days back, 7 to 365. Default 30 `heading` (string): Heading, default 'Insights' | Ready |
| `photobook.book` | A photo book from blueprint.books: its cover, then pages people flip through sideways (or scroll), each laying out photos from the book's feed. People who manage the space can arrange pages, change layouts, swap photos and write on pages right there; saving is one version. | `book`* (string): A blueprint.books id `view` (string): 'flip' (default: one page at a time, swipe sideways like a real book) or 'scroll' (pages one under another) `start` (string): 'cover' (default) or 'first' (open on the first page) `thumbs` (boolean): Show a strip of small pages to jump around, default true `edit` (boolean): Show the Edit book button to people who manage the space, default true | Ready |
| `photobook.shelf` | A shelf of photo book covers that open full screen: the space's books, and (albums) one book for every #tag in a feed, so each Gallery album is a book by itself. | `title` (string): Heading `books` (string[]): blueprint.books ids to show, in order; default all of them `albums` (boolean): Also show a book for each #tag of the feed, default true `feed` (string): The feed albums come from, default 'gallery' `columns` (number): Covers per row, 2 to 4, default 2 `empty` (string): Words when there are no books or albums | Ready |
| `search` | A search box with results as you type, grouped by what they are, each one opening where it lives. Looks across the space (pages, records, messages, feed posts, people, files) or only some of it: one collection makes a record finder, one stream a chat search, sources ['people'] a people finder. Only shows what the viewer may see. | `sources` (string[]): What to look in, some of pages, records, messages, posts, people, files; default all `collection` (string): Only records of this collection (a /collections id) `stream` (string): Only messages in this conversation (a /streams id) `feed` (string): Only posts in this feed (a /feeds id) `limit` (number): Results per kind, 1 to 25, default 5 `title` (string): Heading above the box `placeholder` (string): Words in the empty box, default 'Search' `empty` (string): Words when nothing is found `groups` (boolean): Show a small heading for each kind of result, default true | Ready |
| `miniapps.library` | The public mini-app library: browse and search mini-apps people published, try one, add it to this space or make your own copy (remix). | `category` (string): Start on one category: games, tools, party, kids, learning, business, health, social, creative, info `title` (string): Heading words | Ready |
| `miniapps.mine` | For people who manage the space: this space's mini-apps, with publish to the library, updates from the library, and stats. | `title` (string): Heading words | Ready |
| `miniapps.review` | For Zvessa admins only: published mini-apps waiting for review, and reports. Shows nothing to anyone else. | none | Ready |
| `safety.report` | A Report button for one thing: the space itself (default), a person, a post or a record. People pick a reason and it goes to the space's moderators (and to Zvessa's team for serious reasons). Posts and people already have Report in their menus; use this for a 'Report this space' link in a footer, a help page or a profile. | `target` (string): What it reports: post, feedPost, record, person, space (default space) `id` (string): The id of the post, record or person; leave out for this space `label` (string): Button words, default 'Report this space' | Ready |
| `safety.block` | A Block / Unblock button for one person (by user id). Blocking works everywhere: neither sees the other's posts, and neither can message, call, friend or group the other. | `person`* (string): The person's user id `label` (string): Button words, default 'Block {name}' | Ready |
| `safety.queue` | The review queue for the space's moderators: reported posts, records and people, and posts the word filter held, grouped by what they're about, with Dismiss, Hide, Let through, Warn, Mute, Ban and Send to zvessa. Shows nothing to people who don't moderate. `quiet` hides it entirely while nothing waits (for HQ). | `title` (string): Heading, default 'Reports' `quiet` (boolean): Show nothing while the queue is empty `log` (boolean): Also list what moderators did lately (default true) | Ready |
| `safety.filters` | The space's safety settings for people who manage it: filter words, ready word lists, catching links and contact details, what happens to caught words (mask, hold, refuse), report reasons, who moderates, auto-hide, with a box to try a sentence. Saves blueprint.safety as a version (undo from History). | `title` (string): Heading, default 'Word filter' | Ready |
| `safety.blocked` | The person's own blocked people, with Unblock. Works in any space (blocks follow the person). | `title` (string): Heading, default 'Blocked people' | Ready |
| `safety.admin` | Zvessa's admins only: reports that reached Zvessa from every space, the spaces with the most open reports, suspended accounts and locked spaces, with Suspend, Lock and their undo. Shows nothing to anyone else. Pair it with miniapps.review for the mini-app library. | `title` (string): Heading, default 'Safety across Zvessa' | Ready |
| `account.email` | The viewer's email, and a form to change it: their password, the new email, then a code sent to the new email. | `title` (string): Heading, e.g. 'Email' `change` (string): Button words to start, e.g. 'Change email' `sent` (string): Words after the code was sent, e.g. 'We sent a code to your new email.' `done` (string): Words after the change, e.g. 'Your email is changed.' | Ready |
| `account.password` | A form to change the viewer's password (current one, then the new one). Signs them out on their other devices. | `title` (string): Heading, e.g. 'Password' `button` (string): Button words, e.g. 'Change password' `done` (string): Words after the change | Ready |
| `account.sessions` | Sign out here, or sign out on every other device. | `signOut` (string): Button words, e.g. 'Sign out' `others` (string): Button words, e.g. 'Sign out on other devices' `done` (string): Words after signing out the other devices | Ready |
| `account.delete` | Delete the viewer's account and everything that belongs to it: their own spaces (named, with how many people are in each) and what they posted or uploaded in other spaces. Asks for their password, or a code sent to their email. Can't be undone. | `title` (string): Heading, e.g. 'Delete my account' `about` (string): What it does, in the space's words `button` (string): Button words, e.g. 'Delete my account' `confirm` (string): Final button words, e.g. 'Delete forever' | Ready |
| `notify.settings` | The viewer's own notification controls: pause, quiet hours, daily digest time, and on / digest / off for each kind of notification (per kit and topic) here or in every space, and per space. Each person sees and changes only their own. | `title` (string): A heading above it `sections` (string[]): Which parts show, in order: pause, quiet, digest, topics, spaces. Default all `topics` (string[]): Only these topics or groups ("talk", "social.like"); default every topic the space uses | Ready |
| `notify.inbox` | The viewer's notifications, newest first; tapping one opens the exact page it's about (switching space if needed). Shows what quiet hours or the digest are holding. | `title` (string): A heading above it `scope` (string): 'all' (every space, default) or 'space' (only this space) `limit` (number): How many, default 30, up to 100 `topics` (string[]): Only these topics or groups | Ready |
| `notify.bell` | A small button with the viewer's unread count that opens their notifications. | `label` (string): Words next to the bell; default none `page` (string): A page to open (one with a notify.inbox block); default the built-in notifications screen `scope` (string): Count 'all' spaces (default) or this 'space' | Ready |
| `notify.send` | A form for the space's managers to send a notification to members (everyone, some roles); shows only to people who can manage. Ten an hour per space. | `title` (string): A heading above it `topic` (string): space.announce (default) or one of notifications.topics, so people can choose to get it `to` (string[]): Role ids it goes to; default every member `link` (string): Where a tap goes: page: or an app path; default none | Ready |
| `share.save` | For people who manage the space: save the whole space as a template, or pages they pick as a kit. Shows what's inside and every private thing taken out (keys, emails, phone numbers, signed links, uploaded photos, outside accounts) before saving. Saved ones are private until the person publishes them. | `title` (string): Heading words `kind` (string): 'template' or 'kit' to offer only one; default both | Ready |
| `share.library` | Browse templates and kits people published (each accepted by a Zvessa admin), see what's inside, add one to the space or switch to a template, rate and report. | `title` (string): Heading words `kind` (string): 'template' or 'kit' to show only one; default both `category` (string): Start on one category: business, personal, community, creator, games, party, kids, learning, work, health, tools, other | Ready |
| `share.mine` | For people who manage the space: their own templates and kits (publish, new versions, unpublish, delete) and the ones this space added (newer versions, take out). | `title` (string): Heading words | Ready |
| `share.review` | For Zvessa's admins only (shows nothing to anyone else): published templates and kits waiting for review, with what's inside and what was taken out, and reported ones. | none | Ready |
| `web.share` | This space's web address (or one page's), with Copy and Share. Hides itself while the space isn't on the web. | `page` (string): A page id; default the address itself (the web home page) `label` (string): Words before the address, e.g. 'Find me at' `qr` (boolean): Also show the address as a QR code (in the app and on the web page) `design` (object): How that QR code looks: the qr block's design settings (look, dots, eyes, shape, ink, paper, logo, caption…) | Ready |
| `hq.web` | HQ: the space's web address, handle, own domains with their DNS steps and status, which pages are on the web, share card and embed code. | none | Ready |
| `tabbar` | A page's own bottom tab bar, like a phone app: each child is a `tab` block (a name, an icon and its blocks) and only the open tab is drawn. Put it alone on a page and it fills the screen with a header on top and the bar at the bottom (HQ uses it: Overview, Apps, People, Settings). Up to 5 tabs. *Holds children.* | `title` (string): The header's title. Empty uses the open tab's name `caption` (string): The small line under the title (e.g. the space's name). Empty uses the space's name `mark` (boolean): Show the space's first letter in a lit square at the top left, default true `start` (number): Which tab is open first, counting from 0. Default 0 | Ready |
| `tab` | One tab of a `tabbar`: its name and icon in the bar, and the blocks it shows. *Holds children.* | `label`* (string): The name under the icon (one short word) `icon` (string): An icon name (dashboard, grid, users, settings, chat…) `title` (string): The header's title while this tab is open. Default the label `count` (string): A live number on the icon: needs, proposals, reports, requests, unread ("needs" adds up changes waiting, reports and join requests) | Ready |
| `apps` | An app home screen of everything the space has, built from the blueprint so anything added shows up by itself, each with its own icon, in at most four sections: Apps (pages, mini-apps and what kits added), Chats (the AI and conversations), Connections and More (kits working in the background, bots, data). Tap one to open it. People only see what they may open; connections, bots and background kits show only to people who manage the space. Section names are words: apps.apps, apps.chats, apps.connections, apps.more. | `title` (string): Heading over the grid. Empty shows none `groups` (string[]): Which sections to show, in order: apps, chats, connections, more. Default all `look` (string): 'grid' (default: app icons four across, like a phone's home screen) or 'list' (rows with a line under each name) `add` (boolean): An Add tile that opens the Library, for people who manage the space. Default true `widget` (boolean): A big glass card on top with live numbers (members, waiting for you). Default true | Ready |
| `hq.status` | HQ: "Needs you": three cards with live counts (changes the AI proposed, reports to review, people asking to join), each opening the place to act on it. Only people who manage the space see it. | `title` (string): Small label over the cards, default 'Needs you' | Ready |
| `hq.kit` | HQ: one added kit: what it is, what it added (pages, chats, apps, settings) and a Remove button. The Apps grid opens it for kits that have nothing else to open. | `kit`* (string): A kit id (blueprint.kits) | Ready |
## Kits
94 kits, 87 ready.
### Build
| Kit | Id | What it does | Engines | Adds | Status |
| --- | --- | --- | --- | --- | --- |
| Forms | `forms` | A form people fill in, and a list of what came in. Sign-ups, feedback, applications. | collections | page `forms`, collection `forms-responses` | Ready |
| Connections | `connections` | Use your own AI key, connect any MCP server or API with keys the AI uses but never sees, and build this space from any MCP client. | capabilities | page `connections` | Ready |
| Web research | `web-research` | The AI can search the web and read any page, then report back or build from it. Save what you find here. | capabilities, collections | page `web-research`, collection `web-research-notes` | Ready |
| AI helper | `ai-helper` | Pick a fast or careful AI, see and stop what it's working on, and let it remember how you like things built. | ai | page `ai-helper` | Ready |
| Media library | `media` | Photos, videos, audio and files in folders, with a gallery and a player. Big videos go to Cloudflare R2. | files | page `media` | Ready |
| Mini-app library | `app-library` | Browse, try and add mini-apps other people made, and publish your own for everyone. | apps | page `app-library` | Ready |
| Templates and kits | `templates-library` | Save your space as a template or pages as a kit, publish them for everyone, and add ones other people made. | apps | page `templates-library` | Ready |
| Gallery | `gallery` | A grid of photos and videos that opens full screen. Add #tags to sort them into albums. | spaces | page `gallery` | Ready |
| Live info | `liveinfo` | Shows live information from any MCP server or API the space is connected to, with an optional AI summary. | apps, capabilities | page `liveinfo` | Ready |
| Photo book | `photo-book` | Turn your gallery into a photo book you flip through: a cover, chapters by month, layouts picked for each photo, and every album as a book of its own. Arrange pages by hand or ask the AI. | spaces | page `photo-book` | Ready |
### Talk
| Kit | Id | What it does | Engines | Adds | Status |
| --- | --- | --- | --- | --- | --- |
| Group chat | `group-chat` | One conversation for everyone in the space, and smaller groups people make themselves. | spaces | page `chats`, stream `group-chat` | Ready |
| Channels | `channels` | Topics to talk in, and an announcements channel only the owner posts in, with replies in threads. | spaces | page `chats`, stream `announcements`, stream `general`, stream `channels-random` | Ready |
| Direct chat | `direct-chat` | One-to-one conversations with friends and people in your spaces, with reactions, replies and read receipts. | spaces | page `chats` | Ready |
| Teamspace | `teamspace` | Slack-style for a team or org: rooms with threads and @mentions, direct chats, groups, tasks and an invite link. | spaces, collections | page `chats`, page `teamspace`, stream `general`, stream `announcements` | Ready |
| Support desk | `support` | People ask for help in private; your team answers, assigns and closes each request. Works for applications and orders too. | spaces, lifecycle | page `support`, page `support-desk`, stream `support` | Ready |
| Posts and threads | `feed` | Short posts with photos and videos, replies in threads, likes, reposts, quotes and follows. Your own X or Threads. | spaces | page `feed`, page `explore`, page `profile`, page `activity` | Ready |
| Photo feed | `photos` | Photos and videos with captions, likes, comments and saves, and a profile grid. Your own Instagram. | spaces | page `photos`, page `profile`, page `explore` | Ready |
| Stories | `stories` | Photos and videos that disappear after a day, shown as a row at the top of a page. | spaces | page `stories` | Ready |
| Forum | `forum` | Topics with titles and long threaded discussions, the most active on top. | spaces | page `forum` | Ready |
### People
| Kit | Id | What it does | Engines | Adds | Status |
| --- | --- | --- | --- | --- | --- |
| Contacts | `contacts` | People you know or work with, with their details and notes. | collections | page `contacts`, collection `contacts-people` | Ready |
| Teams and hierarchy | `hierarchy` | Organise people into any structure: teams, departments, classes, squads, branches, households. Units can have leads, titles and their own rooms. | structure | page `structure` | Ready |
| Tiers | `tiers` | Ranked levels people climb: Bronze, Silver and Gold, bought with coins each month or given by you. Pages can be for a tier and up. | ledger | page `tiers` | Ready |
| VIP | `vip` | A top level you hand out yourself, with a mark by the person's name. Pages and blocks can be VIP only. | ledger | page `vip` | Ready |
| Verified | `verified` | A check mark you give people after you've confirmed who they are. Shown by their name; pages can be for verified people only. | ledger | page `verified` | Ready |
| Profile and followers | `profiles` | Your profile with photo, bio, link and follower counts, who to follow, and your activity. | spaces | page `profile`, page `activity` | Ready |
| Welcome message | `welcome-bot` | Greets each new member in General with a message you write. | automations, spaces | – | Ready |
### Money
| Kit | Id | What it does | Engines | Adds | Status |
| --- | --- | --- | --- | --- | --- |
| Catalog | `catalog` | Products, services or a menu, with prices. Taking payments comes with the Payments kit. | collections | page `catalog`, collection `catalog-items` | Ready |
| Coins | `coins` | Your own currency: everyone who joins gets some, people send them to each other, and you give more out. | ledger | page `coins` | Ready |
| Memberships | `memberships` | A membership with perks, renewed each month. Paid in coins for now; card payments plug in later without changing it. | ledger | page `memberships` | Ready |
| Tip and bill splitter | `split` | Work out the tip and what each person pays. | apps | page `split` | Ready |
| Payments | `payments` | Take card payments and subscriptions with Stripe. | ledger | – | Coming |
| Tips | `tips` | Let people tip in coins or money. | ledger | – | Coming |
### Play
| Kit | Id | What it does | Engines | Adds | Status |
| --- | --- | --- | --- | --- | --- |
| Leaderboards | `leaderboards` | Rank people by coins or any unit, or by what they do: most coins this week, most held overall, most active. | ledger, events | page `leaderboards` | Ready |
| Polls | `polls` | Ask a question, people vote, results live. | rounds | page `polls` | Ready |
| Wheel | `wheel` | Spin for a random pick or prize that everyone sees, or take turns spinning for dares and tasks. | rounds | page `wheel` | Ready |
| Hot Potato | `hot-potato` | Pass it before the timer runs out. | rounds | page `hot-potato` | Ready |
| High Card | `high-card` | Everyone draws; the highest card wins. | rounds | page `high-card` | Ready |
| Scorekeeper | `scorekeeper` | Keep score for any game: players, plus and minus, a target to win, undo. | apps | page `scorekeeper` | Ready |
| Dice and coin | `dice` | Roll dice, flip a coin or pick a random number. Fair randomness. | apps | page `dice` | Ready |
| Puzzles | `puzzles` | Tic-tac-toe (with a friend or the phone), a sliding puzzle and a memory match. | apps | page `puzzles` | Ready |
| Point system | `points` | Give people points for anything (chores, wins, help) and show who's ahead this week and overall. | ledger, events | page `points` | Ready |
| Quests and achievements | `quests` | Missions, streaks, badges and levels. | events, structure, ledger | – | Coming |
### Party
| Kit | Id | What it does | Engines | Adds | Status |
| --- | --- | --- | --- | --- | --- |
| Trivia night | `trivia` | Rounds of questions, a timer and a live scoreboard. Make your own or let the AI write them. | rounds | page `trivia` | Ready |
| Quiz board | `quiz-board` | A board of categories and points, Jeopardy style, with buzzers on everyone's phone. | rounds | page `quiz-board` | Ready |
| Survey showdown | `survey-showdown` | Two teams guess the top answers, Family Feud style. | rounds | page `survey-showdown` | Ready |
| Charades | `charades` | Prompts on one phone, a timer and team scores on the TV. | rounds | page `charades` | Ready |
| Party pack | `party-pack` | More group games: drawing, would you rather, two truths and a lie, most likely to, bingo and hidden roles. | rounds | page `party-pack` | Ready |
| Game maker | `game-maker` | Describe any party or board game and the AI builds it from rounds, teams, timers and scores. | rounds | page `game-maker` | Ready |
### Plan
| Kit | Id | What it does | Engines | Adds | Status |
| --- | --- | --- | --- | --- | --- |
| Group plans | `group-plan` | Plan a picnic, Thanksgiving or a trip: who's coming, who brings what, a vote on the date and shared costs. | collections, rounds | page `group-plan`, collection `group-plan-guests`, collection `group-plan-bring`, collection `group-plan-costs` | Ready |
| Secret Santa | `secret-santa` | Draw names fairly, keep them secret, share wish lists and a budget. | rounds, collections | page `secret-santa`, collection `secret-santa-wishes` | Ready |
| Habit tracker | `habits` | Tick off habits each day and keep your streaks going. | apps | page `habits` | Ready |
| Journal | `journal` | A private diary: write each day with a mood, daily prompts, a streak, a mood calendar and 'on this day'. Only you see it unless you change who sees it. | collections | page `journal`, page `journal-calendar`, collection `journal` | Ready |
### Live
| Kit | Id | What it does | Engines | Adds | Status |
| --- | --- | --- | --- | --- | --- |
| Calls | `calls` | Voice and video calls, one-to-one or in groups, with screen share. | rooms | page `calls` | Ready |
| Live streams | `live` | Go live to many viewers. Hosts are on stage, everyone else watches and reacts, and you can bring people up to speak. | rooms | page `live` | Ready |
| Show on TV | `tv` | Put any page, game or scoreboard on a TV with a TV link, AirPlay or Chromecast; phones become controllers. | screens | page `tv` | Ready |
### Time
| Kit | Id | What it does | Engines | Adds | Status |
| --- | --- | --- | --- | --- | --- |
| Appointments | `appointments` | People book a time with you: set your days, hours and how long each booking is. | time | page `appointments` | Ready |
| Scheduler | `scheduler` | Write posts now and have them go out later in your chats, channels or feed. | time, spaces | page `scheduler` | Ready |
| Timer | `timer` | A countdown timer with presets that rings when time is up. Can run cycles, like focus and break. | apps | page `timer` | Ready |
| Stopwatch | `stopwatch` | A stopwatch with laps that keeps counting while you're away. | apps | page `stopwatch` | Ready |
| Countdown | `countdown` | Days, hours and minutes until a date: a launch, a trip, a birthday. | apps | page `countdown` | Ready |
| Focus timer | `focus` | Work in focused blocks with short breaks in between (25 and 5 minutes, four rounds). | apps | page `focus` | Ready |
| Service page | `service-page` | Your services with prices, booking and payment. | time, collections, ledger | – | Coming |
| Events and tickets | `events-tickets` | RSVPs, tickets and check-in by QR. | time, ledger | – | Coming |
### Work
| Kit | Id | What it does | Engines | Adds | Status |
| --- | --- | --- | --- | --- | --- |
| Boards and tasks | `tasks` | A board with columns you move things across. Tasks, orders, a pipeline. | collections | page `tasks`, collection `tasks-items` | Ready |
| Drafts and approvals | `drafts` | Write now and send later, keep drafts, and hold posts from chosen roles until a moderator approves them. | spaces, lifecycle | page `drafts` | Ready |
| Bots | `bots` | AI teammates with their own role, tools and approval rules. Ask the AI for one: a helper, a greeter, a reporter, a moderator. | automations, spaces | page `bots` | Ready |
| Automations | `automations` | When something happens, do something: on a schedule, when a record, post or member arrives, or when an outside service calls in. | automations, events, time | page `bots` | Ready |
| Helper bot | `helper-bot` | @mention the helper in any chat and it answers, in context. Its answers wait for your OK until you let it act on its own. | automations, spaces | – | Ready |
| Reply drafts | `reply-drafts` | When someone writes to your help desk, a bot drafts the answer. Your team edits it or sends it as is, as themselves. | automations, spaces | – | Ready |
| Daily digest | `daily-digest` | Every morning a bot reads what happened in your chats and lists, and sends you a short summary. | automations, spaces, time | – | Ready |
| Docs and wiki | `docs` | Pages with sections, search and versions. | collections | – | Coming |
### Grow
| Kit | Id | What it does | Engines | Adds | Status |
| --- | --- | --- | --- | --- | --- |
| Analytics | `analytics` | Counts page views, posts, records, joins and plays per day, as charts only you see. | events | page `analytics` | Ready |
| Guestbook | `guestbook` | Visitors leave a short note on your page. | spaces | page `guestbook` | Ready |
| Outside feeds | `outside-feeds` | Pull posts from X, Bluesky, Mastodon, Instagram, Threads, YouTube or any RSS feed into your space, with your own keys. | spaces, capabilities | page `outside` | Ready |
| Reviews and ratings | `reviews` | Star ratings with words and photos for anything: services, products, places, games, people. Helpful votes, verified reviewers, your answers, reports and spam limits. | spaces | page `reviews` | Ready |
| Coins for posts | `post-rewards` | Members earn a coin each time they post in a chat. Coins only, never real money. | automations, ledger, spaces | – | Ready |
| Public links | `public-links` | Your pages on the web without the app: your own address (handle.zvessa.com), your own domain, link previews and embeds. | web | page `public-links` | Ready |
| Invites and referrals | `invites` | Everyone gets their own invite code and QR. When someone they invite stays a day, both get coins. See who invited whom and the top inviters. | structure, ledger, events | page `invites` | Ready |
| Auto-post scheduler | `auto-post` | Write once, schedule posts to your social accounts, track how they do. | time, capabilities, events | – | Coming |
### Tools
| Kit | Id | What it does | Engines | Adds | Status |
| --- | --- | --- | --- | --- | --- |
| Notifications | `notifications` | A page with everyone's notifications and their own settings: per kit, per space, quiet hours, a daily digest. | notify | page `notifications` | Ready |
| Announcements | `announcements` | Managers send a notification to members, or to some roles; members choose how they get them. | notify | page `announce` | Ready |
| Calculator | `calculator` | A calculator with keyboard support. | apps | page `calculator` | Ready |
| Drawing board | `drawing` | Draw and sketch with the space's colours; undo, eraser, and it remembers your drawing. | apps | page `drawing` | Ready |
| Unit converter | `converter` | Convert length, weight, temperature, volume, area, speed, time and data. | apps | page `converter` | Ready |
| Flashcards | `flashcards` | Study with cards that flip; shuffle, again or got it. Edit the deck yourself. | apps | page `flashcards` | Ready |
| Metronome | `metronome` | A steady beat with an accent on the first beat; tap to set the tempo. | apps | page `metronome` | Ready |
| Colour picker | `colors` | Pick a colour, get its HEX, RGB and HSL, shades, and this space's own colours. | apps | page `colors` | Ready |
| QR maker | `qr` | Turn a link, text or Wi-Fi login into a QR code to scan. | apps | page `qr` | Ready |
| Compass | `compass` | A compass that shows which way you're facing, and can point out a direction. | apps | page `compass` | Ready |
| Weather | `weather` | Today's weather, the next hours and the week, for a place you pick or where you are. Free data from Open-Meteo. | apps, capabilities | page `weather` | Ready |
| Pocket tools | `toolbox` | Calculator, unit converter, tip splitter, timer and stopwatch, together. | apps | page `toolbox` | Ready |
| Search | `search` | One search box for the whole space: pages, records, messages, posts, people and files. Everyone only finds what they're allowed to see. | search | page `search` | Ready |
### Safety
| Kit | Id | What it does | Engines | Adds | Status |
| --- | --- | --- | --- | --- | --- |
| Kids space | `kids` | Made for kids: private and invite-only, grown-up and kid roles, and a kids' chat only grown-ups can moderate. | audiences, structure, spaces | page `kids`, stream `kids-chat` | Ready |
| Moderation and safety | `moderation` | Word filters, a review queue for reports, auto-hiding and who moderates. Reporting and blocking work in every space anyway. | spaces, lifecycle | page `moderation` | Ready |
## Permissions
| Verb | What it allows |
| --- | --- |
| `view` | See the space's pages |
| `post` | Write in chats and streams |
| `play` | Vote, play games and join rounds |
| `add` | Add records (forms, lists, boards) and change their own |
| `edit` | Change or delete anyone's records |
| `invite` | Invite people |
| `manage` | Change the space: pages, styles, kits, roles; approve the AI's changes |
## Default roles
| Role | Id | Can |
| --- | --- | --- |
| Owner | `owner` | view, post, play, add, edit, invite, manage |
| Member | `member` | view, post, play, add |
| Guest | `guest` | view, play |
| Visitor | `visitor` | view |
## Server functions
Public queries and mutations (call them with `useQuery` / `useMutation` or the Convex client).
Internal functions are left out. Who may call each: [API](https://docs.zvessa.com/dev/api.html).
| Function | Kind | About |
| --- | --- | --- |
| `account.account.me` | query | The person's account as their own account blocks show it. |
| `account.account.startEmailChange` | action | Step 1: your password and the new address. A code goes to the new address. |
| `account.account.confirmEmailChange` | action | Step 2: the code from the new address. The old address gets a note that it changed. |
| `account.account.changePassword` | action | Your current password and a new one (8+ characters). Other devices are signed out. |
| `account.account.signOutOthers` | action | Signs the person out on every other device. |
| `account.account.startDeletion` | action | |
| `account.account.deleteMe` | action | Deletes the account. Give the password, or the code from startDeletion. The person is signed out everywhere at once; the rest of their data goes over the next moments. |
| `ai.messages` | query | |
| `ai.send` | mutation | Sends the person's message and starts the AI on it. A limit hit comes back as { ok: false, limit }. |
| `ai.retry` | mutation | Asks again after a failed reply: the failed reply goes away and the AI answers the person's last message anew. |
| `ai.stop` | mutation | Stops an AI job in this space: the one given, or the chat's current one. |
| `ai.jobs` | query | The space's latest AI jobs (the ai.jobs block). |
| `aiNotes.mine` | query | |
| `aiNotes.add` | mutation | |
| `aiNotes.remove` | mutation | |
| `blueprint.ensure` | mutation | Creates the person's workspace on first sign-in, in the starter style they picked. |
| `blueprint.onboard` | mutation | Onboarding in one step: the person's name, and (unless they only joined someone else's space) their own space with a name, a look and the kits they picked. Saved as one version, so the first undo point is the finished setup. |
| `blueprint.me` | query | Where the person is in onboarding, for the screens before a space opens. |
| `blueprint.change` | mutation | A change the person makes themselves (HQ controls, installing a kit): same checks as an AI proposal, applied at once, saved as a version so it can be undone. |
| `blueprint.get` | query | |
| `blueprint.history` | query | |
| `blueprint.pending` | query | |
| `blueprint.approve` | mutation | A person approves an AI proposal: apply it to the current blueprint, check it, save a version. |
| `blueprint.discard` | mutation | |
| `blueprint.restore` | mutation | Undo = restore an earlier version as a new version, so undo itself can be undone. |
| `blueprint.addKit` | mutation | Adds a kit (docs/KITS.md) to the space, as one undoable version. |
| `blueprint.removeKit` | mutation | Takes a kit out again: its pages and collection shapes. Records stay, so adding it back restores them. |
| `bookings.slots` | query | Slots on a day (in the block's time zone), with which are taken. |
| `bookings.days` | query | The days people can book, in the block's time zone, starting today. |
| `bookings.book` | mutation | |
| `bookings.cancel` | mutation | |
| `bookings.upcoming` | query | Upcoming bookings on a block: everyone's for managers, your own otherwise. |
| `calls.targets` | query | Who the viewer can ring from a call block (for 'pick', the list to choose from). |
| `calls.start` | mutation | Rings people. From a call block, whoever can see the block can ring its targets (an owner's "Call us" button), and 'pick' needs "post". Without a block (a members list, a direct conversation) it needs "post", everyone rung must be in the space, and each must be a friend unless the caller may invite. Kids only ring and get rung within their kids spaces. |
| `calls.incoming` | query | Calls ringing the signed-in person right now, in any space. |
| `calls.current` | query | The call the signed-in person is on (placed, or answered), if any. |
| `calls.answer` | mutation | |
| `calls.decline` | mutation | |
| `calls.hangUp` | mutation | |
| `capabilities.connections.test` | action | HQ: checks a connection works and lists what it offers. |
| `capabilities.tokens.list` | query | |
| `capabilities.tokens.create` | action | Makes a token and returns it. This is the only time anyone sees it. |
| `capabilities.tokens.revoke` | mutation | Stops a token working at once. |
| `capabilities.vault.list` | query | The keys in the open space, without their values. |
| `capabilities.vault.save` | action | Adds a key, or replaces the value of one with the same name. The value is never returned. |
| `capabilities.vault.setScopes` | mutation | Narrows where a key may be sent and what it's for, without touching its value. Widening needs the key pasted again (save), so nobody can send a key they don't have somewhere new. |
| `capabilities.vault.remove` | mutation | Removes a key from the vault. Connections that use it stop working until it's added again. |
| `dms.list` | query | One direct conversation, oldest first; null when these two can't talk and have nothing yet. |
| `dms.send` | mutation | |
| `dms.remove` | mutation | The writer takes a message back. |
| `dms.conversations` | query | The person's direct conversations, newest first. |
| `events.track` | mutation | Counts something from the app: a page view, or a custom event a block or mini-app sends ("tap-buy", "finished-level"). Anyone who can see the space may send them. |
| `events.summary` | query | Daily totals for the last `days` days, per event name. For people who manage the space. |
| `events.leaders` | query | People ranked by how often they did something over the last `days` days (or all time). The data source for leaderboards: anyone who can see the space can read it. |
| `friends.list` | query | Friends, requests to me, and requests I sent. |
| `friends.status` | query | How I stand with one person: none, asked (I asked), asking (they asked), friends, or me. |
| `friends.ask` | mutation | Asks someone to be friends; if they already asked me, we become friends. |
| `friends.answer` | mutation | |
| `friends.remove` | mutation | Unfriends, or takes back a request. |
| `friends.spaces` | query | Spaces my friends own that I'm allowed to look at (friends, link or public). |
| `kits.ailimits.limits.allowance` | query | What the signed-in person has left today in the space they have open. For the chat and "ai.allowance". |
| `kits.ailimits.limits.usage` | query | Today's AI use in the open space, for people who manage it (the "hq.aiUsage" block). |
| `kits.approval.auto.undo` | mutation | Undo on an "Applied automatically" card: goes back to the version before the change, saved as a new version (so the undo can be undone in History). Only while it's still the latest change; after that, History is where to go back, so nothing newer is lost by surprise. |
| `kits.bots.engine.run` | mutation | Starts a manual automation (the bots.run block). Who may: when.who, else people who manage. |
| `kits.bots.engine.ask` | mutation | Ask a bot from the app (people who manage, or the roles a bots.ask block is for). |
| `kits.bots.engine.waiting` | query | Actions waiting for this person to approve. |
| `kits.bots.engine.decide` | mutation | Approve (optionally with changed words) or discard a waiting action. |
| `kits.bots.engine.runs` | query | Recent runs, for people who manage (the bots.runs block). |
| `kits.bots.engine.overview` | query | The space's bots and automations with today's use, for the bots.list block. |
| `kits.bots.engine.canRun` | query | Whether a manual automation can be run by this person (the bots.run block shows itself only then). |
| `kits.bots.engine.runOf` | query | One run (to follow an answer while a bot thinks). |
| `kits.bots.hooks.create` | mutation | Makes a webhook address for an automation. The secret is shown once. |
| `kits.bots.hooks.list` | query | |
| `kits.bots.hooks.revoke` | mutation | |
| `kits.firststeps.steps.status` | query | |
| `kits.invites.referrals.myCode` | query | The viewer's own code in the open space, and whether they can get one. |
| `kits.invites.referrals.getCode` | mutation | Makes (or returns) the viewer's own invite code for the open space. |
| `kits.invites.referrals.mine` | query | People the viewer invited to the open space. |
| `kits.invites.referrals.all` | query | Every referral in the open space, for people who may invite or manage. |
| `kits.invites.referrals.recheck` | mutation | Checks pending referrals now, e.g. after changing what qualifies. Managers only. |
| `kits.ledger.ledger.balances` | query | The signed-in person's balance in every unit of the space (or one unit). |
| `kits.ledger.ledger.history` | query | The signed-in person's latest movements in a unit (or all units). |
| `kits.ledger.ledger.leaderboard` | query | A ranking of holders of one unit: by balance now, by everything ever received, or by what was received in a recent window (day, week, month, year). |
| `kits.ledger.ledger.levels` | query | The levels of a space, with whether the signed-in person holds each and until when. |
| `kits.ledger.ledger.marks` | query | The public marks (top level and badges) of a person in the open space, to show beside their name. |
| `kits.ledger.ledger.rights` | query | What the signed-in person may manage here, so blocks only show tools that will work. |
| `kits.ledger.ledger.holders` | query | Everyone holding a level or badge, for whoever may give it. |
| `kits.ledger.ledger.open` | mutation | Opens the signed-in person's accounts in the open space, giving each unit's welcome amount once. Visitors who haven't joined don't get accounts. |
| `kits.ledger.ledger.send` | mutation | Send units to another member, when the unit allows it. |
| `kits.ledger.ledger.issue` | mutation | Create units and give them to someone (or to the space's purse). Needs the unit's `issue` permission. |
| `kits.ledger.ledger.take` | mutation | Take units back from someone. Needs the unit's `issue` permission. |
| `kits.ledger.ledger.buyLevel` | mutation | Buy (or renew) a level with its price, paid to the space's purse. Renewing adds a period to the time left. |
| `kits.ledger.ledger.grant` | mutation | Give a level or a badge by hand (verified, VIP, staff…). Needs its `grant` permission. |
| `kits.ledger.ledger.revoke` | mutation | |
| `kits.lobby.lobby.summary` | query | |
| `kits.photobook.books.read` | query | |
| `kits.photobook.books.photos` | query | Photos people who manage the space can put in a book: the feed's images, newest first. |
| `kits.photobook.books.shelf` | query | A shelf's covers: the space's books, then one book per #tag of the feed (most photos first). |
| `kits.records.views.days` | query | Every day that has records: how many, and the chip value of the newest one that day (to colour a calendar). Small enough to send whole; a streak, a calendar and insights all read it. |
| `kits.records.views.onDays` | query | The records on some days (a calendar's day, "on this day"), newest first. |
| `kits.records.views.search` | query | Records whose words (any text field) or #tags match, newest first. |
| `kits.records.views.one` | query | One record (a reader opened from a link or after an edit). |
| `kits.reviews.reviews.summary` | query | The rating at a glance, the person's own review, and whether they may write. |
| `kits.reviews.reviews.stars` | query | Just the average and count, for a small star line (one read). |
| `kits.reviews.reviews.list` | query | A page of live reviews. Sorts by helpful, newest, highest or lowest; filters by stars, photos, verified. |
| `kits.reviews.reviews.top` | query | The board's subjects ranked by rating (or by how many reviews). |
| `kits.reviews.reviews.subjects` | query | What someone can pick to review when a block doesn't say: the board's list, its collection's records, and subjects pages name. |
| `kits.reviews.reviews.queue` | query | Reviews waiting for approval and reported ones, for people who manage. |
| `kits.reviews.reviews.write` | mutation | Writes this person's review of a subject, or changes the one they wrote. |
| `kits.reviews.reviews.remove` | mutation | Deletes a review: your own, or anyone's for people who can edit or manage. |
| `kits.reviews.reviews.vote` | mutation | Helpful (true), not helpful (false), or take the vote back (left out). Not on your own review. |
| `kits.reviews.reviews.reply` | mutation | The public answer to a review. Empty words take it away. |
| `kits.reviews.reviews.report` | mutation | Reports a review once. Enough reports hide it until someone who manages looks. |
| `kits.reviews.reviews.moderate` | mutation | For people who manage: approve a waiting review, hide one, or keep a reported one (clears its reports). |
| `kits.safety.safety.report` | mutation | |
| `kits.safety.safety.block` | mutation | |
| `kits.safety.safety.unblock` | mutation | |
| `kits.safety.safety.blocked` | query | The people I blocked, to unblock. |
| `kits.safety.safety.isBlocked` | query | Whether I blocked someone (for a Block / Unblock button). |
| `kits.safety.safety.me` | query | |
| `kits.safety.safety.queue` | query | The open space's review queue: open cases and what moderators did lately. Null for people who don't moderate. |
| `kits.safety.safety.waiting` | query | How many open cases wait in the open space (for a badge). Null for people who don't moderate. |
| `kits.safety.safety.act` | mutation | |
| `kits.safety.safety.setSettings` | mutation | |
| `kits.safety.safety.settings` | query | The space's safety settings as they apply, and the ready lists, for the settings block. |
| `kits.safety.safety.tryFilter` | query | What the open space's filter would do to some words (to try settings out). |
| `kits.safety.safety.admin` | query | Everything that reached Zvessa's admins, across spaces, and the suspended accounts and locked spaces. Null for everyone else. |
| `kits.search.search.everywhere` | query | Search as the signed-in person, in the space they have open. |
| `kits.sharing.library.preview` | query | What would be shared, before saving: what's inside, what was taken out, and any problems. |
| `kits.sharing.library.saveMine` | mutation | Saves a page, some pages or the whole space as the person's own template or kit (private until they publish). |
| `kits.sharing.library.publish` | mutation | Publishing: the person's own tap, never the AI's. The version (default the newest) waits for a Zvessa admin; other spaces can add it once accepted. |
| `kits.sharing.library.unpublish` | mutation | Takes a published one out of the library again (spaces that added it keep their copy). |
| `kits.sharing.library.forget` | mutation | Deletes one of the person's own templates or kits that was never accepted into the library. |
| `kits.sharing.library.mine` | query | The person's own templates and kits, with where each one is in the library. |
| `kits.sharing.library.browse` | query | |
| `kits.sharing.library.details` | query | |
| `kits.sharing.library.add` | mutation | Adds a template or kit to the open space: the person's tap, saved as one version. |
| `kits.sharing.library.added` | query | What this space added from the library, with newer versions where there are some. |
| `kits.sharing.library.takeOut` | mutation | Takes out a kit added from the library: its pages and parts (records are kept). One version, so it can be undone. |
| `kits.sharing.library.rate` | mutation | |
| `kits.sharing.library.report` | mutation | |
| `kits.sharing.library.queue` | query | |
| `kits.sharing.library.source` | query | The whole piece of a waiting version, for an admin to read before accepting. |
| `kits.sharing.library.accept` | mutation | |
| `kits.sharing.library.reject` | mutation | |
| `kits.sharing.library.remove` | mutation | Takes one out of the library for everyone. Spaces that added it keep their copy: it's their blueprint now. |
| `kits.sharing.library.restore` | mutation | |
| `kits.sharing.library.dismissReports` | mutation | |
| `kits.social.activity.list` | query | |
| `kits.social.activity.unseen` | query | |
| `kits.social.activity.markSeen` | mutation | |
| `kits.social.people.me` | query | The signed-in person's profile. `handle` is unset until they pick one or first post. |
| `kits.social.people.byHandle` | query | Anyone's profile by @handle. |
| `kits.social.people.view` | query | A profile to show: by @handle, the open space's owner, or (neither) the person looking. |
| `kits.social.people.handleFree` | query | Whether a handle can be taken. |
| `kits.social.people.saveProfile` | mutation | Saves the person's profile. Also used by onboarding. |
| `kits.social.people.follow` | mutation | Follows someone, or stops following them. |
| `kits.social.people.list` | query | Followers or following of a person, newest first. |
| `kits.social.people.suggested` | query | People in this space the person doesn't follow yet, most followed first. |
| `kits.social.people.cards` | query | Photo and @handle for a short list of people (friends lists), up to 100. Signed-in only. |
| `kits.social.posts.timeline` | query | A page of posts, newest first. `next` is passed back as `before` for the next page. |
| `kits.social.posts.thread` | query | One post with the posts above it (what it replies to) and its replies. |
| `kits.social.posts.pending` | query | Posts waiting for approval, for people who manage the space. |
| `kits.social.posts.create` | mutation | |
| `kits.social.posts.edit` | mutation | |
| `kits.social.posts.remove` | mutation | Deletes a post. One with replies stays as a "deleted" marker so its thread keeps its shape. |
| `kits.social.posts.moderate` | mutation | Approve a waiting post, hide one, or show a hidden one again. |
| `kits.social.reactions.react` | mutation | Adds or takes back one reaction. The feed's first reaction is the main one (likes). |
| `kits.social.reactions.reactors` | query | Who reacted to a post, newest first. |
| `kits.social.reactions.repost` | mutation | Reposts a post into its feed, or takes the repost back. |
| `kits.social.reactions.save` | mutation | Saves a post for later, or un-saves it. Saves are private. |
| `kits.social.reactions.saved` | query | The person's saved posts in this space, newest save first. |
| `kits.social.search.posts` | query | |
| `kits.social.search.trending` | query | |
| `kits.social.sources.status` | query | The sources of the open space and how their last pull went (for people who manage it). |
| `kits.social.sources.pullNow` | mutation | Pulls one source now (and sets up timers for sources added since the last save). |
| `kits.structure.structure.placements` | query | Everyone placed in the space's units, with names, titles and leads, and what this person may change. |
| `kits.structure.structure.place` | mutation | Places someone in a unit, or changes their title or lead mark there. |
| `kits.structure.structure.unplace` | mutation | Takes someone out of a unit. People can always take themselves out. |
| `kits.structure.structure.join` | mutation | Joins a unit whose join setting is "open". |
| `kits.structure.structure.mine` | query | The units this person is in. |
| `kits.talk.talk.conversation` | query | A conversation's header: title, what this person may do, pinned posts, who's in it, read marks. |
| `kits.talk.talk.posts` | query | Posts in a conversation (or in one post's side thread), newest first, a page at a time. |
| `kits.talk.talk.post` | query | One post, for the top of its side thread. |
| `kits.talk.talk.typing` | query | Who else is typing here now. |
| `kits.talk.talk.inbox` | query | Every conversation this person has in the open space (and their direct chats), pinned first, then newest. |
| `kits.talk.talk.send` | mutation | |
| `kits.talk.talk.edit` | mutation | |
| `kits.talk.talk.remove` | mutation | The writer takes a post back; a moderator hides someone else's. |
| `kits.talk.talk.review` | mutation | A moderator lets a waiting post through, or keeps it out. |
| `kits.talk.talk.react` | mutation | |
| `kits.talk.talk.reactors` | query | Who reacted with what on one post. |
| `kits.talk.talk.pin` | mutation | |
| `kits.talk.talk.read` | mutation | Marks the conversation read up to now. |
| `kits.talk.talk.setSeat` | mutation | This person's own settings for a conversation: mute it, pin it to the top. |
| `kits.talk.talk.setTyping` | mutation | |
| `kits.talk.talk.openDirect` | mutation | The key of a direct chat with someone (it starts on its first post). |
| `kits.talk.talk.people` | query | People in the open space, to start a chat or a group with. |
| `kits.talk.talk.createGroup` | mutation | |
| `kits.talk.talk.addToGroup` | mutation | |
| `kits.talk.talk.leaveGroup` | mutation | |
| `kits.talk.talk.renameGroup` | mutation | |
| `kits.talk.talk.setGroupAdmin` | mutation | |
| `kits.talk.talk.openCase` | mutation | A person opens a case in a queue with a short title and their first words. |
| `kits.talk.talk.cases` | query | A queue's cases for its team: one status at a time (or the ones given to me), newest first. |
| `kits.talk.talk.myCases` | query | A person's own cases in a queue. |
| `kits.talk.talk.setCase` | mutation | The team picks who answers, or moves the case on. |
| `kits.talk.talk.team` | query | People who answer a queue, to give cases to. |
| `kits.talk.talk.draft` | query | The words someone left in a conversation's message box. |
| `kits.talk.talk.saveDraft` | mutation | |
| `kits.talk.talk.schedule` | mutation | Writes now, sends at `at`. |
| `kits.talk.talk.cancelScheduled` | mutation | |
| `kits.talk.talk.sendNow` | mutation | |
| `kits.talk.talk.drafts` | query | A person's drafts and scheduled posts, and for moderators the posts waiting for them. |
| `kits.templates.templates.apply` | mutation | Switches the open space to a template: adds its kits, puts its pages, tabs and look in place. |
| `kits.templates.templates.overview` | query | HQ's overview: the space at a glance over the last `days` days, each number with the same number of days before it, so the owner sees what's going up and what's going down. |
| `kits.templates.templates.recentCalls` | query | The person's latest calls in the open space, in and out, newest first. |
| `livekit.join` | action | A token to join a room block's room. |
| `livekit.joinCall` | action | A token to join a call you placed or answered. |
| `livekit.moderate` | action | Hosts: bring someone on stage, send them back, mute them or remove them. |
| `livekit.record` | action | Hosts: start or stop recording a room into the media library's "recordings" folder. |
| `media.list` | query | The items a gallery block shows. |
| `media.latest` | query | The newest item of the block's kind in its folder (the media.player block). |
| `media.uploadTarget` | mutation | Where to upload a file of this size: Convex storage, or R2 for big files when it's set up. An R2 link only accepts this exact size and type, and works for 10 minutes. |
| `media.save` | mutation | After the upload finishes: put the file in the library. |
| `media.remove` | mutation | |
| `media.rename` | mutation | |
| `media.can` | query | What the viewer may do in the library (the gallery hides upload for people who can't add). |
| `media.byIds` | query | Library items by id, for other kits that point at media (a post's photos, a profile picture). Ids from another space, or deleted, come back as null in their place. |
| `members.list` | query | |
| `members.mySpaces` | query | Every space the person is in, for the space switcher. |
| `members.open` | mutation | |
| `members.setName` | mutation | |
| `members.setRole` | mutation | |
| `members.removeMember` | mutation | |
| `members.createInvite` | mutation | |
| `members.invites` | query | |
| `members.revokeInvite` | mutation | |
| `members.peek` | query | What an invite link opens, shown before joining. |
| `members.join` | mutation | |
| `members.visit` | mutation | Opens another space: as a member, or as a visitor when its visibility allows. |
| `members.home` | mutation | Goes back to the person's own space. |
| `members.explore` | query | Public spaces anyone can look at (Explore). |
| `members.joinOpen` | mutation | Joins the open space as a member, when its join rule is "open"; otherwise asks to join. |
| `members.requests` | query | |
| `members.answerRequest` | mutation | |
| `miniapps.bridge.me` | query | |
| `miniapps.bridge.members` | query | |
| `miniapps.bridge.storageGet` | query | |
| `miniapps.bridge.storageList` | query | |
| `miniapps.bridge.storageSet` | mutation | |
| `miniapps.bridge.storageDelete` | mutation | |
| `miniapps.bridge.recordsList` | query | |
| `miniapps.bridge.recordsAdd` | mutation | |
| `miniapps.bridge.recordsUpdate` | mutation | |
| `miniapps.bridge.recordsDelete` | mutation | |
| `miniapps.bridge.scoresSubmit` | mutation | |
| `miniapps.bridge.scoresTop` | query | |
| `miniapps.bridge.track` | mutation | |
| `miniapps.bridge.channelSend` | mutation | |
| `miniapps.bridge.channel` | query | |
| `miniapps.bridge.here` | mutation | The device says "still here" every 20 s while the mini-app shows. |
| `miniapps.bridge.presence` | query | |
| `miniapps.bridge.notifyMembers` | mutation | |
| `miniapps.library.isAdmin` | query | |
| `miniapps.library.publish` | mutation | |
| `miniapps.library.mine` | query | This space's mini-apps with their library state: published, waiting, updates to take. |
| `miniapps.library.browse` | query | |
| `miniapps.library.details` | query | |
| `miniapps.library.preview` | query | The HTML to try a mini-app before adding it. It runs in preview: no space data, nothing saved. Anyone signed in gets the live version; admins and the author can try any version. |
| `miniapps.library.taken` | query | Whether a mini-app added from the library was taken down. |
| `miniapps.library.add` | mutation | |
| `miniapps.library.remix` | mutation | |
| `miniapps.library.update` | mutation | Takes the library's newest accepted version. If it asks for more, the person saw that before tapping. |
| `miniapps.library.rate` | mutation | Rate a library mini-app, with words if you like. Rating again changes yours. |
| `miniapps.library.reviews` | query | A library mini-app's rating summary, its newest reviews, and yours. |
| `miniapps.library.replyReview` | mutation | The author answers a review in public (empty text takes the answer away). |
| `miniapps.library.reportReview` | mutation | Report a review; an admin decides. |
| `miniapps.library.moderateReview` | mutation | Admins: hide a review (it stops showing and counting) or keep it. |
| `miniapps.library.report` | mutation | |
| `miniapps.library.queue` | query | |
| `miniapps.library.accept` | mutation | |
| `miniapps.library.reject` | mutation | |
| `miniapps.library.remove` | mutation | Takes a mini-app down: out of the library, and it stops running in every space that added it. |
| `miniapps.library.restore` | mutation | |
| `miniapps.library.dismissReports` | mutation | |
| `miniapps.outside.connectionsCall` | action | |
| `miniapps.outside.connectionsDescribe` | action | |
| `miniapps.outside.aiAsk` | action | |
| `notify.settings` | query | |
| `notify.change` | mutation | |
| `notify.zone` | mutation | The phone's time zone (sent when the app opens); writes only when it moved. |
| `notify.undo` | mutation | |
| `notify.inbox` | query | |
| `notify.unread` | query | |
| `notify.markRead` | mutation | |
| `notify.open` | mutation | A tap on a notification (on the phone or in the inbox): marks it read, opens its space if it's another one the person is in, and answers the path to open. |
| `notify.send` | mutation | |
| `notify.sent` | query | |
| `push.register` | mutation | Saves this phone's push token for the signed-in person (moves it if someone else had it). |
| `push.unregister` | mutation | Stops pushes to this phone (on sign out). |
| `records.list` | query | |
| `records.count` | query | |
| `records.add` | mutation | |
| `records.update` | mutation | |
| `records.remove` | mutation | |
| `rooms.status` | query | What a room block shows: is it live, how many are in, and what the viewer may do. |
| `rounds.quick.vote` | mutation | |
| `rounds.quick.poll` | query | |
| `rounds.quick.spin` | mutation | |
| `rounds.quick.wheel` | query | |
| `rounds.sessions.create` | mutation | Starts a new game from one of the space's game definitions. The caller hosts it. |
| `rounds.sessions.join` | mutation | Joins a game by its code. The code is the invite, so people outside the space can join, except in a kids space. Wrong codes are counted and joining pauses after a few, so codes can't be guessed; a wrong code returns `{ error }` instead of throwing so the count is saved. |
| `rounds.sessions.leave` | mutation | |
| `rounds.sessions.setPlayer` | mutation | Host only: move a player to a team, rename them, or take them out of the game. |
| `rounds.sessions.setOpen` | mutation | Host only: stop taking new players, or open the game again. |
| `rounds.sessions.act` | mutation | Every move in every game goes through here. |
| `rounds.sessions.view` | query | The game as the signed-in person may see it. |
| `rounds.sessions.screen` | query | The big-screen view: public parts only (no secret words, hidden answers or gift names). For anyone who can see the game's space, including a TV paired to it. |
| `rounds.sessions.live` | query | The game of this kind on now in the space (newest, ended under 6 hours ago), for big screens. |
| `rounds.sessions.current` | query | The newest game of this kind in a space that the caller can still join or is in. |
| `screens.open` | mutation | A screen opens the TV link: it gets a code to show and a secret to keep. No sign-in needed. |
| `screens.pending` | query | What a screen that isn't signed in yet shows: its code, until someone connects it. |
| `screens.pair` | mutation | Someone in a space types the code shown on a screen. |
| `screens.mine` | query | For the screen itself, once signed in: which page to show. Null when it's been disconnected. |
| `screens.ping` | mutation | The screen says it's still on (every minute), so remotes can show it as connected. |
| `screens.list` | query | The space's connected screens, for remotes: members who can use some remote block. |
| `screens.show` | mutation | A remote puts a page on a screen. Only pages the block allows and the screen's role can see. |
| `screens.disconnect` | mutation | Disconnects a screen: it leaves the space and goes back to showing a code. |
| `streams.list` | query | The newest posts of a stream, oldest first, with who wrote them. |
| `streams.send` | mutation | |
| `streams.remove` | mutation | The writer takes a post back; a moderator hides someone else's. |
| `streams.schedule` | mutation | Sets a post to go out at a time. It's checked again when it goes out. |
| `streams.cancelScheduled` | mutation | |
| `streams.scheduledList` | query | Posts waiting to go out (and the last few that went), yours or, for managers, everyone's. |
| `streams.writable` | query | Streams the person may write in, for a "post later" picker. |
| `web.addresses.mine` | query | The open space's web addresses and pages (HQ's Web block). Visitors get only the public parts. |
| `web.addresses.link` | query | The web address of one page of the open space (the share block), or null when it isn't on the web. |
| `web.addresses.available` | query | Whether a handle is free for the open space. |
| `web.addresses.checkNow` | mutation | Checks an own domain again now (after the person added the DNS records). |
## Field types
`text`, `longtext`, `number`, `money`, `date`, `choice`, `checkbox`, `email`, `phone`, `url`
# The in-app AI
Code: `convex/ai.ts`. Key: the space's own Claude key from its vault ("Connect your AI" in HQ, or the Connections
page). The platform's own key is optional and off for now: when it's set,
spaces without a key use it under the platform's limits (docs/build/AI-LIMITS.md); when it isn't,
they see "Connect your AI".
## How it works
1. The person sends a message (`ai.send`), which schedules `ai.respond`.
2. `respond` sends Claude the block list, the rules and the current blueprint, plus the last 20
messages.
3. Claude calls `propose_change({ summary, ops })`. The server applies the ops to a copy and runs
`checkBlueprint`. Errors go back to Claude to fix (up to 6 rounds).
4. A valid proposal is stored as pending. The person approves it in the chat or in HQ.
## Limits
- The AI never writes the blueprint directly; only `blueprint.approve` does, after a person taps it.
- It can't remove the HQ page.
- Mini-app HTML runs sandboxed (WebView on phones, iframe on web) with a strict content policy
(no network).
## Next tools
- `read_blueprint` for big workspaces (send only the part it needs)
- Capability tools: list connected MCP servers and keys by name, call them on the server
- `save_template`: turn a page or the whole blueprint into a reusable template
# Blocks
A block is data: `{ id, kind, props?, children? }`. Every kind is described once in
`convex/lib/blocks.ts` (what it's for, its props, whether it holds children). That one list feeds:
- the renderer (`src/components/Renderer.tsx`, one function per kind),
- the checks (`checkBlueprint` refuses unknown kinds, missing or wrong props, duplicate ids),
- the AI's system prompt (it can only use kinds on the list).
## Adding a block
1. Add its description to `BLOCKS` in `convex/lib/blocks.ts`.
2. Add its renderer to `RENDERERS` in `src/components/Renderer.tsx`.
3. Nothing else: HQ and the AI pick it up from the list.
Keep blocks abstract: a `stat` is a number with a label, not a "streak counter".
## Today
stack, heading, text, image, button, list, card, stat, divider, miniapp, and the HQ system blocks
hq.proposals, hq.history, hq.blueprint.
# Bots and automations
Bots are AI teammates in your space. Automations are rules: when this happens, if these checks
pass, do these steps. You make both by asking the AI ("add a helper bot people can @mention",
"every Monday at 9, post the week's open tasks in General"), by hand, or from your own AI over MCP.
Every change is a proposal you approve and can undo.
## For people
- **Add the Bots kit** (HQ → Library) for the Bots page: what's waiting for you, your bots and
automations with an on/off switch, and what happened. Recipes: Helper bot, Welcome message,
Reply drafts, Daily digest, Coins for posts.
- **Bots ask first.** Until you set a bot to act on its own, everything it would change (a post, a
record, a notification, coins, any other action) waits on the Bots page. You can change the words
before you approve. In a kids space bots always ask a grown-up.
- **A bot's role decides what it may do**, like a person's: where it may post, whether it may add
or change records. Bots never approve anything.
- **Reply drafts:** on a help desk, a bot drafts the answer and your teammate sends it as themselves.
- **@mention a bot** by name in a chat to ask it something. Its answer counts against your daily AI
use; automations count against the space's.
- **Webhooks:** let an outside service (a form tool, a shop, a script) start an automation. Make the
address on the page with a `bots.hooks` block; it's shown once.
## For builders
Blueprint entries (full rules in `convex/kits/bots/model.ts`, the AI's guide part `bots`):
```json
{
"bots": [{ "id": "helper", "name": "Helper", "role": "bot", "instructions": "Answer briefly.",
"tools": ["read_conversation", "post_message", "list_records"], "approval": "ask" }],
"automations": [{ "id": "helper-answers", "name": "Helper answers mentions",
"when": { "event": "post", "mentions": "helper" },
"do": [{ "do": "ask", "bot": "helper", "prompt": "{{person.name}} asked: {{text}}" },
{ "do": "post", "reply": true, "text": "{{last}}", "as": "helper" }] }]
}
```
- **Bot fields:** `id, name, about, icon, role, instructions, tools, approval ("ask"|"auto"), askFor,
approvers, model ("small"|"large"), perDay, on`. `builder` is the AI chat: set its name,
instructions and model only.
- **Tools:** any tool from the one tool list (the same ones MCP clients and the AI chat use), except
approving. A tool runs as the bot's account, so its role applies. Tools that change something
follow the approval rules; proposals always wait.
- **Triggers (`when.event`):** `record`, `record.changed`, `post`, `join`, `booking`, `play`, `review`,
any event a kit reports, plus `schedule` (every 15m, 30m, hour, day, week, month; at, day, timezone),
`webhook` and `manual`. Other keys filter on the event's fields (`collection`, `feed`, `board`…);
`stream` also matches every case in a queue; `mentions` names a bot; `fromBots` hears bots too.
- **Checks (`if`):** `[{ path, is | not | contains | gt | lt | exists }]`.
- **Steps (`do`):** `post`, `add_record`, `update_record`, `notify`, `give` (coins and other Ledger
units; never money), `ask` (a bot thinks; its answer is `{{last}}`), `wait`, `call` (a connection),
`tool` (any tool, as a bot). Text is a template: `{{text}}`, `{{person.name}}`, `{{data.x}}`,
`{{last}}`, `{{date}}`, `{{space.name}}`.
- **Approval:** words you wrote into an automation go as written; words a bot wrote follow its
approval; `as: "approver"` makes a draft the approver sends; `approval: "ask"` on an automation
makes every step wait.
- **Safety:** an automation never starts itself again, chains stop after 3 rounds, per-day caps on
every bot (default 50) and automation (default 200), webhooks 60 a minute and 16 KB.
**Blocks:** `bots.list`, `bots.waiting`, `bots.runs`, `bots.run`, `bots.ask`, `bots.hooks`.
**MCP tools:** `read_conversation`, `post_message`, `notify`, `give`, `run_automation`, `ask_bot`,
`list_runs`, `list_waiting`, `decide_action` (plus `propose_change` for making bots and automations).
**Webhook:** `POST {CONVEX_SITE_URL}/hooks/` with a JSON body; answers 202, 404, 409, 413 or 429.
# Capabilities: MCP, your own keys, outside services
The Capabilities engine lets a space reach outside Zvessa, and lets anything outside build it.
Three parts, all on the server, all under the same rule as everything else: **changes are
proposals a person approves, and every approved change is a version that can be undone.**
People get the screens by adding the **Connections** kit (HQ → Library). It adds a Connections
page with one system block, `hq.connections`. The engine itself works without the kit.
## 1. Build from anywhere (the MCP server)
Any MCP client (Claude Desktop, Claude Code, Cursor, another app's AI) can build a space with
the same tools as the in-app AI.
- Address: `{CONVEX_SITE_URL}/mcp` (self-hosted default `http://127.0.0.1:3211/mcp`; put it
behind HTTPS before using it from another machine).
- Auth: an **access token** made on the Connections page: `Authorization: Bearer mbk_…`.
Tokens end after 90 days (1–365 when made) and allow 120 calls a minute. For clients that only
take a URL, a token can be made as a URL token (`{CONVEX_SITE_URL}/mcp/t/mbk_…`); URLs end up
in logs, so those can only read and propose. Batches are capped at 20 messages.
- Transport: Streamable HTTP, stateless, JSON responses. Checked with the official MCP SDK client.
- A token belongs to one person in one space. It can do what **both** its scopes and the
person's current role allow, so changing someone's role changes their tokens at once.
Only a hash is stored; the token is shown once. Revoking stops it at once.
```sh
claude mcp add --transport http zvessa https://your-backend/mcp --header "Authorization: Bearer mbk_…"
```
| Scope | Tools | Needs role permission |
| --- | --- | --- |
| `read` | get_space, read_blueprint, describe_library, list_proposals, list_versions | view (proposals, versions: manage) |
| `propose` | propose_change, add_kit, remove_kit | manage |
| `approve` | approve_proposal, discard_proposal, undo_to_version | manage |
| `data.read` | list_records | view |
| `data.write` | add_record, update_record, delete_record | add (others' records: edit) |
| `connections` | list_connections, call_connection | view / manage |
New tokens get `read` + `propose`: the client proposes, a person approves in the app. Give a
token `approve` only for a client you'd let change the space on its own (it can still be undone;
History shows "approved from "). Kids spaces can't have approving tokens. When the owner turns on
Auto approval (`/ai/approval`, [AI](https://docs.zvessa.com/dev/ai.html#auto-approval)), a `propose` token's changes apply
at once too, unless `mcp` is false or the change touches a path that still asks.
The tools are described once in `convex/lib/capabilities/tools.ts`. The in-app AI gets the
same ones (minus the outside-only reads it already has in its prompt, and minus approving).
How to build, in words for an AI, is `convex/lib/guide.ts`: the in-app AI's instructions and
the `describe_library` tool both come from it.
## 2. Your own keys (the vault)
- A space's keys live in the `secrets` table, sealed with AES-256-GCM under the platform's
vault key and tied to their space and name. No query returns a value; only server
code about to send a request opens one (`openSecret` in `convex/capabilities/vault.ts`).
- Each key has **scopes**: what it's for in this space (`ai`: the AI chat's model key;
`connections`: connections may send it) and the **hosts** it may go to (`api.example.com`
or `*.example.com`). A key is refused for any other host, whatever a connection says.
- **Your AI key:** a key with use `ai` (host `api.anthropic.com`) makes the space's AI chat run
on that key. Without one, the space's AI asks a manager to connect a key (the "ai.setup"
block), unless the platform offers its own key (optional, off for now).
- Only people who can manage the space add, change or remove keys. Changing a key's hosts or
uses can only narrow them; widening needs the key pasted again, so nobody can send a key they
don't have to a new place.
- Wildcard hosts can't cover shared suffixes (`*.co.uk`, `*.vercel.app`, `*.github.io`…).
## 3. Outside services (connections)
A connection is a setting in the blueprint, so the AI can propose one and a person approves it:
```json
"connections": [
{ "id": "github", "name": "GitHub", "kind": "mcp", "url": "https://api.githubcopilot.com/mcp/",
"auth": { "secret": "github", "as": "bearer" }, "tools": ["search_repositories", "create_issue"] },
{ "id": "weather", "name": "Weather", "kind": "http", "url": "https://api.example.com/v1",
"auth": { "secret": "weather", "as": "query", "name": "key" } }
]
```
- `mcp`: a remote MCP server (Streamable HTTP; event-stream answers are fine). Only tools the
server marks read-only can be called, unless the person lists them in `tools`.
- `http`: any HTTPS API. Calls stay under `url`; only `GET` unless the person lists `methods`.
- `auth.secret` names a vault key; the server adds it as a bearer token, a header or a query
parameter. Keys are scrubbed from results and errors.
- Every call leaves through `convex/capabilities/egress.ts` (a Node action). The host must be
public HTTPS on port 443; it is resolved, every address must be public (no loopback, private,
link-local, CGNAT, NAT64/6to4/Teredo, IPv4-compatible or reserved ranges), and the request is
pinned to that address, so a name can't switch inside between the check and the call. No
redirects, 2 MB and 30 s caps. For more depth, also firewall the backend's outbound traffic
away from your private network.
- Header values in the blueprint can't look like keys (`X-API-Key`, `Authorization`…): those go
in the vault. Only people who manage a space see its connections.
- The in-app AI can only read through connections (GET, read-only MCP tools), and what comes back
is marked as outside data, so a page or tool result can't make it send anything out. Changing
calls run from an MCP client with the `connections` scope.
- Local (stdio) MCP servers can't run on the backend; connect the remote version of a server.
## Files
- `convex/capabilities/`: `tables.ts` (schema), `vault.ts`, `tokens.ts`, `mcp.ts` + `routes.ts`
(the MCP server), `connections.ts` (calling outside services), `builder.ts` (tool server side),
`run.ts` (one place that checks scopes and roles and runs a tool)
- `convex/lib/capabilities/`: `tools.ts` (the tool list and scopes), `connections.ts` (connection
and key checks), `mcpClient.ts` (calling MCP servers), `crypto.ts` (sealing, hashing)
- `convex/lib/guide.ts`: how to build, for any AI
- `src/components/capabilities/ConnectionsPanel.tsx`: the `hq.connections` block
- `tests/capabilities.test.ts`: vault, tokens, MCP server, connections
# Core
What every workspace has, always, with no switch.
**Rule of thumb:** if switching it off would hurt the person (their data, their way back, their
safety), it's core.
- **The dashboard**: HQ, a page and the AI chat. Panes and their order are settings, but the HQ
page can't be removed (`checkBlueprint` refuses it).
- **HQ**: proposals waiting for approval, version history with undo, the blueprint itself. Built
from `hq.*` system blocks, so it can be rearranged.
- **Versions**: every save is kept; undo restores an earlier version as a new one.
- **Account**: Convex Auth, email + password; one workspace per account for now. Next: Apple,
Google and email-code sign-in, several workspaces per account.
# HQ
HQ is where the people who run a space see and change everything. It is a normal page (id `hq`)
made only of general blocks, so the AI can rearrange it like any other page. It looks and moves
like chat-app's futuristic HQ: a frosted header with the space's mark, a bottom tab bar with a lit
edge, "Needs you" cards, a big glass widget and an app grid.
Code: `convex/lib/hqshell/` (descriptions, the Apps list, the HQ page), `src/kits/hqshell/`
(screens), `tests/hqshell.test.ts`.
## The tabs
| Tab | What it holds |
| --- | --- |
| Overview | Connect your AI (until a key is in), Needs you, changes waiting, reports, the 7-day overview, and anything a template adds (Business: quick actions, a chart, leaderboards) |
| Apps | `apps`: everything the space has, as a phone's home screen |
| People | rows for Members, Invite people and Who can come in; each opens its own screen |
| Settings | three short groups of rows; each row opens its own screen |
Settings rows: Space (Look, Navigation, Pages, Starters), AI and connections (AI key, MCP servers
and APIs, Access tokens, Library), Safety and versions (Safety, History, Blueprint).
`hq.connections` takes `part` (`ai`, `servers`, `tokens`) so each row shows one short screen.
The header has a back chevron on the left ("Back" spelled out on wide screens). The card on top
of Apps shows what needs you and how many apps are on.
## The blocks
| Kind | What it is |
| --- | --- |
| `tabbar` | A page's own bottom tab bar; children are `tab` blocks; only the open tab is drawn. Alone on a page it is the whole screen (header + bar). Inside another page, or where the app already has a tab bar, the tabs sit in a row on top. Up to 5. |
| `tab` | One tab: `label`, `icon`, `title` (header title), `count` (a live number on the icon: needs, proposals, reports, requests, unread). |
| `apps` | Everything the space has, built from the blueprint each time, in at most four sections: Apps, Chats, Connections, More. Props: `title`, `groups`, `look` (grid or list), `add`, `widget`. |
| `hq.status` | Needs you: changes to approve, reports to review, people asking to join. |
| `hq.kit` | One added kit: what it is, what it added, Remove. |
Rows that open their own screen are the Layout blocks' `item` (or `section` with `open: true`)
holding child blocks; the page route shows them at `/p/?part=`.
## Nothing turned on is hidden
`appSections()` in `convex/lib/hqshell/apps.ts` reads the blueprint, so a kit, a mini-app, a
connection or a chat shows up in Apps the moment it is added:
- **Apps**: the space's pages, its mini-apps, then what each kit added, in the Library's order.
A mini-app with no page runs in a sheet.
- **Chats**: the AI chat and every conversation stream.
- **Connections** (managers): every MCP server and API; a tap opens MCP servers and APIs.
- **More** (managers): kits that work in the background (a bot, a rule, a setting) with a card
saying what they do, bots, and the space's own data.
Every entry has an icon: the page's or mini-app's own, else a house for `home` (as in the chat
list), else its kit's (`KIT_ICONS`, a test checks
every kit has one), else its Library group's. Section names are words: `apps.apps`,
`apps.chats`, `apps.connections`, `apps.more`.
Members only see what their blueprint holds (`blueprintFor`), so never connections, bots or HQ.
## Looks
Everything reads the style. In glowing styles (Futuristic, Tech) HQ gets chat-app's touches: the
style's mono face for numbers and small labels, a lit edge on the header and tab bar, glass cards
that hide the grid, glossy dark app icons with an accent sheen, and a glowing pill over the open
tab (`src/kits/hqshell/look.ts`). Classic stays clean with soft tinted icons. When the Tech style
adds `type.mono`, `surface.edge` and `surface.glow`, HQ reads them first.
## Old spaces
`upgradeHq()` runs inside `upgradeBlueprint`: a space whose HQ is the old flat list of system
blocks gets the new HQ, and any block it added itself (a chart, tiles) moves to the end of
Overview. An HQ from the first tab-bar version gets People and "AI and connections" as rows (only
HQ's own blocks are swapped; anything added there stays). Any other HQ with a tab bar, or one
rebuilt by hand, is left alone.
# Kits
Everything beyond the dashboard is an optional **kit**: a bundle a person adds to their space
only if they want it. A new space has only HQ, the page and the AI chat. Kits are described once
in `convex/lib/kits.ts`; HQ → Library lists them, and the AI can propose adding one.
## What a kit is
- **Engines it switches on** (below). Engines are the only server code; kits are settings.
- **What it adds to the blueprint:** pages, blocks, words and settings, under ids the kit owns.
- **Its settings** in `blueprint.kits.`, so it can be changed by hand or by the AI.
- Adding a kit is a change saved as a version; removing it takes out what it added. Both can be undone.
A kit never hard-codes a use case: "Support desk" is a space with a queue and assignment, "VIP"
is a rank. People rename and restyle everything a kit adds.
## The engines
| Engine | What it is, abstractly | Kits on it |
| --- | --- | --- |
| Collections | Any records with fields the AI or a person defines | Forms, shop, boards and tasks, contacts, docs |
| Spaces | A place with members, roles and a stream of posts | Direct chat, group chat, channels, teamspace, support desk, feed |
| Structure | Any tree or graph of units with roles, ranks and badges | Teams and hierarchy of any kind, tiers, VIP, verified |
| Ledger | Balances in any unit and transfers | Coins, payments (Stripe), tips |
| Rounds | An interactive session: players, turns, timer, randomness, result, prize | Polls, Hot Potato, wheel, High Card |
| Rooms | Live audio and video (LiveKit) | Calls, video, group calls, screen share, live streams |
| Time | Resources, availability, bookings, schedules | Scheduler, appointments, service page, events and tickets |
| Lifecycle | Draft, scheduled, published, archived, with approvals | Drafts, scheduled posts, requests and approvals |
| Events | Anything recorded, counted and ranked | Analytics, leaderboards, quests |
| Apps | Sandboxed mini-apps behind the bridge | Apps, custom games, the game maker |
| Capabilities | Outside services through a vault (MCP, APIs, web search) | Web research, connections, auto-post |
| Screens | Show a page or game on another screen (TV, projector); phones act as controllers | Show on TV, party games |
| Audiences | Who a space is for, with age-aware rules set by grown-ups | Kids spaces |
| Automations | Bots (AI teammates with a role, tools and approval rules) and when → if → do rules, schedules, webhooks | Bots, automations, helper bot, welcome message, reply drafts, daily digest, coins for posts |
## What's ready
`kitReady` in `convex/lib/kits.ts` is the source of truth; HQ → Library shows the rest as "Soon".
Engines ready: Collections, Spaces, Structure, Time, Events, Apps, Capabilities, Audiences.
| Kit | What it adds | Server code |
| --- | --- | --- |
| Forms, Contacts, Tasks, Catalog | Collections and pages with records blocks | `convex/records.ts` |
| Group chat, Channels, Feed, Teamspace | Streams (`blueprint.streams`) and `stream` blocks; channels limit writers with `postRoles` | `convex/streams.ts` |
| Direct chat | The `direct` block; DMs belong to the two people, not the space | `convex/dms.ts` |
| Kids space | `audience: "kids"`, grown-up and kid roles, a kids' chat; the space stays private and invite-only | checks in `convex/lib/blueprint.ts` |
| Web research | Turns on the AI's web search and page reading, plus a notes collection | `convex/ai.ts` |
| Analytics | An owner-only page with the `analytics` block | `convex/events.ts`, `convex/tally.ts` |
| Appointments | A `booking` block whose props are the opening hours | `convex/bookings.ts`, `convex/lib/time.ts` |
| Scheduler | The `scheduled` block: write now, post into a stream later | `convex/streams.ts` (`schedule`) |
| Bots, Automations and recipes | `/bots` and `/automations`, the Bots page (`bots.*` blocks); recipes are only entries | `convex/kits/bots` (README there) |
Counting (Events): engines call `count(ctx, ws, name, userId)` from `convex/tally.ts`. It writes
only when the space has a kit in `TALLY_KITS`, so spaces pay nothing for what they don't use.
`api.events.leaders` ranks people by any event; leaderboards and quests read it.
A kit can add collections, pages, streams and roles, and set an audience. Removing it takes out
what it added (records stay, roles still in use stay).
Each engine's code sits in `convex/kits//` with a README.md (settings, functions, tables,
blocks, kits) and its screens in `src/kits//`. Spaces: `convex/kits/talk`. Structure:
`convex/kits/structure`.
Party games (trivia, quiz board, survey showdown, charades, the party pack) are all Rounds with
teams, a timer, scores and a screen, so the game maker can build new ones from the same parts.
Group plans and Secret Santa are collections, polls and a fair draw inside a space.
The full catalog and the order they are built in: the "Zvessa kits and styles" doc in the project.
# Layers and the blueprint
## The layers
| Layer | What it is | Runs where | Today |
| --- | --- | --- | --- |
| Core | Dashboard (HQ, page, AI chat), account, versions | Inside Zvessa | Dashboard, versions, sign-in |
| Features | Engines a workspace switches on | Inside Zvessa | None yet |
| Blocks | Ready parts, configuration only | Inside Zvessa | 10 + 3 HQ blocks |
| Apps | Custom logic: AI-made mini-apps, developer apps | Sandbox (WebView / iframe) | Mini-apps |
| Styles | Looks as settings (docs/STYLES.md) | Inside Zvessa | Classic, Futuristic, own styles, page looks |
| Templates | Ready-made blueprints | Data | The default starter |
| Capabilities | MCP, API keys, HTTP, jobs | Server only | Planned |
Quick test for something new:
- Filled in and placed → block
- Its own engine that the server must run (data, games with rules, payments) → feature
- Custom logic someone else writes → app
- An outside service → capability
## The blueprint
One JSON document per workspace (`convex/lib/blueprint.ts`):
```json
{
"identity": { "name": "My space" },
"words": { "chatTitle": "AI", "approve": "Approve" },
"look": { "style": "futuristic", "scheme": "system", "changes": { "colors": { "accent": "#22c55e" } } },
"styles": [{ "id": "paper", "name": "Paper", "extends": "classic", "type": { "body": "serif" } }],
"dashboard": { "panes": [{ "kind": "page", "page": "hq" }, { "kind": "page", "page": "home" }, { "kind": "chat" }] },
"pages": [{ "id": "home", "title": "Home", "blocks": [{ "id": "hello", "kind": "heading", "props": { "text": "Hello" } }] }],
"apps": [{ "id": "snake", "name": "Snake", "html": "" }]
}
```
Changes are JSON Patch ops (add, replace, remove). Every applied change is checked
(`checkBlueprint`) and saved as a version, so undo is "restore version N".
## Three ways in
1. **HQ**: screens that edit parts of the blueprint.
2. **The AI**: proposes a patch, the person approves.
3. **The API / MCP server (later)**: the same propose and approve, under the same permissions.
# Layout, text and tokens
Every page in Zvessa is built from the same small set of layout and text blocks. A block never
holds a size, font or colour. It names a **text role**, a **size step** or a **colour role**, and
the space's style says what those look like. Change a token once and every page follows.
The AI and MCP clients use these blocks through `propose_change`, like any other block. People
add them by hand in the page editor (Page header, Section, Grid, Shelf, List rows, Space, Badge, Tags).
## How a page is put together
A calm page usually reads top to bottom like this:
1. **header**: the page's title, with an optional small line over it, a line under it, a mark or
logo, and one or two action buttons. When it is the first block it *is* the page title, and the
bar at the top shows its title once you scroll (the iPhone large-title behaviour).
2. **section** blocks, each with a small uppercase title and an optional "See all" link.
3. Inside a section: a **grid** of cards or stats, a **scroll** shelf people swipe sideways, a
**row** of buttons or badges, or a **group** of **item** rows.
4. An **item** (or a **section** with `open: true`) that holds blocks opens them as their own
screen, so a settings page can live inside one page.
Defaults are quiet: no extra lines or frames unless the style asks for them, small uppercase
labels over groups, one icon per row.
## Example
```json
[
{ "id": "top", "kind": "header", "props": { "title": "Studio", "eyebrow": "Today", "subtitle": "Everything happening in your space", "mark": "S" },
"children": [{ "id": "top-search", "kind": "button", "props": { "icon": "search", "href": "#search" } }] },
{ "id": "glance", "kind": "section", "props": { "title": "At a glance", "action": "See all", "href": "#stats" }, "children": [
{ "id": "glance-grid", "kind": "grid", "props": { "columns": 2 }, "children": [
{ "id": "members", "kind": "stat", "props": { "label": "Members", "value": "128", "change": "12%", "trend": "up" } },
{ "id": "bookings", "kind": "stat", "props": { "label": "Bookings", "value": "24" } } ] } ] },
{ "id": "settings", "kind": "group", "props": { "title": "Your space" }, "children": [
{ "id": "people", "kind": "item", "props": { "title": "People & invites", "icon": "users", "href": "#people" } },
{ "id": "alerts", "kind": "item", "props": { "title": "Notifications", "icon": "bell", "value": "On", "href": "#alerts" } } ] }
]
```
## Designing a header
Everything about a header is a setting on the block:
| What | Setting | Options |
|---|---|---|
| Size | `size` | `large` (default, the big title), `inline` (a compact bar), `hero` (a tall band) |
| Height | `height` | `compact`, `regular`, `tall` |
| Background | `background` | `none` (default), `surface`, `tint`, `gradient`, `glass` (frosted), `image` |
| Title style | `titleVariant` | any text role below |
| Title font | `font` | `body`, `display`, `mono` (the style's) or a font name |
| Title weight | `weight` | 400, 500, 600, 700, 800 |
| Title colour | `titleTone` | a colour role or value, or `gradient` (accent to accent2) |
| Alignment | `align` | `start`, `center` |
| Logo | `mark` / `image` | a letter, emoji or icon name; or an https picture |
| Colour | `tint` | the mark and band colour |
| Line under it | `divider` | true or false |
| Actions | child `button` blocks | icon-only buttons when they have an icon and no label |
## Text roles (tokens)
Each role is a token in the style at `type.roles.` with these settings: `size` (8-96 px,
before `type.scale`), `weight` (400-800), `font` (`body`, `display`, `mono` or a font
name), `case` (`none` or `upper`), `tracking` (-2 to 6 px), `leading` (1-2, line height as a
multiple of size) and `tone` (a colour role). `display`, `title` and `headline` also follow the
style's `headingCase` and tracking when they don't set their own.
Values below read: size · weight · font · case · tracking · leading · colour. A dash means the
style's default.
| Role | Classic | Futuristic |
|---|---|---|
| display | 34 · 800 · display (system) · - · -0.4 · 1.15 · text | 34 · 800 · display (mono) · - · -0.4 · 1.15 · text |
| title | 28 · 700 · display (system) · - · -0.3 · 1.2 · text | 28 · 700 · display (mono) · - · -0.3 · 1.2 · text |
| headline | 22 · 700 · display (system) · - · - · 1.25 · text | 22 · 700 · display (mono) · - · - · 1.25 · text |
| subtitle | 17 · 600 · body (system) · - · - · 1.3 · text | 17 · 600 · body (system) · - · - · 1.3 · text |
| body | 15 · - · body (system) · - · - · 1.45 · text | 15 · - · body (system) · - · - · 1.45 · text |
| callout | 16 · - · body (system) · - · - · 1.45 · text2 | 16 · - · body (system) · - · - · 1.45 · text2 |
| caption | 13 · - · body (system) · - · - · 1.35 · text2 | 12.5 · - · body (system) · - · - · 1.35 · text3 |
| label | 12.5 · 600 · body (system) · upper · 0.6 · 1.3 · text3 | 10.5 · 500 · mono (mono) · upper · 1.3 · 1.3 · text3 |
| overline | 11 · 600 · body (system) · upper · 1.2 · 1.3 · text3 | 10.5 · 500 · mono (mono) · upper · 1.3 · 1.3 · text3 |
| number | 28 · 700 · display (system) · - · - · 1.1 · text | 28 · 500 · mono (mono) · - · - · 1.1 · text |
| quote | 17 · - · body (system) · - · - · 1.5 · text2 | 17 · - · body (system) · - · - · 1.5 · text2 |
| code | 13 · - · mono (mono) · - · - · 1.5 · text | 13 · - · mono (mono) · - · - · 1.5 · text |
To change one for a whole space, change the look, for example:
```json
{ "op": "add", "path": "/look/changes/type/roles/title", "value": { "size": 32, "font": "serif" } }
```
A page's own look (`/pages/N/look`) can do the same for that page only.
## Other tokens the blocks read
| Token | Where | What it does |
|---|---|---|
| `type.body`, `type.display`, `type.mono` | style | The three fonts roles point at |
| `type.scale` | style | Scales every role (0.75-1.5) |
| `space.density` | style | The spacing unit: compact 6, regular 8, roomy 10 px. Size steps are xs 0.5, sm 1, md 2, lg 3, xl 5 units |
| `shape.radius`, `shape.radiusSmall` | style | Corners of cards, buttons, chips |
| `shape.marks` | style | Lit corner marks on cards (Futuristic) |
| `surface.treatment`, `surface.depth` | style | Fill, outline, glass or gradient; flat, shadow or glow |
| `parts.card`, `parts.header`, `parts.button`, `parts.tile`, `parts.chip` | style | Restyle one part: treatment, depth, radius, bg, text, line |
| Colour roles | style | bg, surface, raised, text, text2, text3, line, accent, accent2, onAccent, success, warn, danger, gold and any name a style adds |
## Cards
| Size | Use it for |
|---|---|
| `sm` | Small squares in grids and shelves |
| `md` | The everyday card (default) |
| `lg` | A big card with room for a story |
| `wide` | Icon or picture on the left, words on the right; takes the full width of a grid |
| `hero` | Tall, the picture fills it and the words sit on top; full width |
Variants: `default` (the style's card), `outline`, `filled` (a soft tint), `accent` (solid
colour, or a lit edge in outlined styles), `glass`, `plain` (no frame).
## Every layout and text block
### header
The page's header (app bar), first on a page: a small line over the title (eyebrow), the title, a subtitle, an optional mark or logo, and its action buttons (children: button blocks, icon-only when they have an icon and no label). size 'large' is the phone's big title, 'inline' a compact bar title, 'hero' a tall picture band with the title on it. When it is the first block it replaces the page's own title, and the bar shows its title as you scroll.
| Setting | Type | What it does |
|---|---|---|
| title | string | Title. Empty uses the page's name |
| subtitle | string | A line under the title |
| eyebrow | string | A small line over the title (the space's name, a date, a section) |
| size | string | 'large' (default), 'inline' or 'hero' |
| align | string | 'start' (default) or 'center' |
| mark | string | A letter, emoji or icon name in a square before the title (like an app's logo) |
| image | string | https URL: the picture behind a 'hero' header, or the logo square otherwise |
| tint | string | Colour of the mark square or the hero band without a picture: a style colour role. Default accent |
| titleVariant | string | The title's text role (display, title, headline, subtitle, body, callout, caption, label, overline, number, quote, code). Default display (large), subtitle (inline), title (hero) |
| titleTone | string | The title's colour: a style colour role or a colour value. 'gradient' paints it from accent to accent2 |
| font | string | The title's font: 'body', 'display' or 'mono' (the style's), or a font name (system, rounded, serif, mono…) |
| weight | string | The title's weight: 400, 500, 600, 700 or 800 |
| background | string | Behind the header: 'none' (default, the page shows through), 'surface', 'tint' (a soft wash of tint), 'gradient' (tint to accent2), 'glass' (frosted, see-through) or 'image' (the picture in image) |
| height | string | 'compact', 'regular' (default) or 'tall' |
| divider | boolean | A thin line under the header |
Holds child blocks.
### heading
A title for a part of a page, with an optional small line over it and a line under it.
| Setting | Type | What it does |
|---|---|---|
| text | string, required | The words |
| level | number | 1 (title), 2 (headline), 3 (subtitle) or 4 (small label). Default 1 |
| subtitle | string | A quieter line under it |
| eyebrow | string | A small line over it |
| align | string | 'start' (default), 'center' or 'end' |
### text
Words in one of the style's text roles. variant picks the role: display, title, headline, subtitle, body, callout, caption, label, overline, number, quote, code. body is a paragraph, callout a bigger quiet intro, caption small print, label and overline small capitals, number a big figure, quote a pulled quote, code fixed-width.
| Setting | Type | What it does |
|---|---|---|
| text | string, required | The words. New lines are kept |
| variant | string | The text role: display, title, headline, subtitle, body, callout, caption, label, overline, number, quote, code. Default body |
| align | string | 'start' (default), 'center' or 'end' |
| tone | string | A colour role from the style (accent, accent2, success, warn, danger, gold, text2…). Default depends on the block. Default the role's own |
| lines | number | Show at most this many lines, then … |
### list
A list of short lines: bullets, numbers or ticks.
| Setting | Type | What it does |
|---|---|---|
| items | string[], required | One string per item |
| variant | string | 'bullet' (default), 'number' or 'check' |
### badge
A small pill with a word or a count: New, Live, 3, Beta, Sold out.
| Setting | Type | What it does |
|---|---|---|
| text | string, required | The words |
| tone | string | A colour role from the style (accent, accent2, success, warn, danger, gold, text2…). Default depends on the block. Default accent |
| icon | string | An icon name (home, chat, calendar, star, settings, users, bolt, game, coin…) or an emoji |
| variant | string | 'soft' (default: a light tint), 'solid' or 'outline' |
### chips
A row of small tags that wraps: topics, skills, opening days, filters to read.
| Setting | Type | What it does |
|---|---|---|
| items | string[], required | One tag per item |
| tone | string | A colour role from the style (accent, accent2, success, warn, danger, gold, text2…). Default depends on the block. Default neutral |
### card
A framed box: a picture or icon, a small line over the title, the title, a line under it, text, a badge, and child blocks. size 'sm' (a small square for grids and shelves), 'md' (default), 'lg' (big, a picture on top), 'wide' (picture or icon on the left, words on the right) or 'hero' (tall, the picture fills it with the words on top). With href the whole card opens it.
| Setting | Type | What it does |
|---|---|---|
| title | string | Title |
| text | string | Text |
| subtitle | string | A line under the title |
| eyebrow | string | A small line over the title |
| size | string | 'sm', 'md' (default), 'lg', 'wide' or 'hero' |
| variant | string | 'default' (the style's card), 'outline', 'filled' (a tint of tint), 'accent' (solid tint), 'glass' or 'plain' (no frame) |
| image | string | https URL of a picture |
| icon | string | An icon name (home, chat, calendar, star, settings, users, bolt, game, coin…) or an emoji |
| tint | string | Colour of the icon square and of 'filled' and 'accent' cards: A colour role from the style (accent, accent2, success, warn, danger, gold, text2…). Default depends on the block |
| badge | string | A small label in the corner (New, 2, Live) |
| href | string | '#page-id' opens a page, 'https://…' a website, 'chat:' a conversation (e.g. chat:support, chat:general), 'ai' the AI chat (people who manage), 'mailto:' or 'tel:' a mail or a phone call |
| action | string | Words of a button at the bottom that opens href (Book, Open, Play) |
Holds child blocks.
### stat
A big number with a label, and optionally its change (+12%) and a note.
| Setting | Type | What it does |
|---|---|---|
| label | string, required | What the number is |
| value | string, required | The number or short value |
| change | string | The change, as people should read it: +12%, -3, New |
| trend | string | 'up', 'down' or 'flat': colours the change (up green, down red) |
| note | string | A short line under it |
| icon | string | An icon name (home, chat, calendar, star, settings, users, bolt, game, coin…) or an emoji |
| href | string | '#page-id' opens a page, 'https://…' a website, 'chat:' a conversation (e.g. chat:support, chat:general), 'ai' the AI chat (people who manage), 'mailto:' or 'tel:' a mail or a phone call |
### image
A picture from a URL, in a shape you pick, with an optional caption.
| Setting | Type | What it does |
|---|---|---|
| src | string, required | https URL |
| alt | string | What the picture shows |
| ratio | string | Shape: 1:1, 4:3, 3:2, 16:9, 21:9, 3:4, 9:16. Default 16:9 |
| caption | string | Small words under it |
| round | boolean | A circle (a face, a logo) |
### button
A button that opens a page, a conversation, a link or the AI. With an icon and no label it is a round icon button (header actions).
| Setting | Type | What it does |
|---|---|---|
| label | string | Button words |
| href | string | '#page-id' opens a page, 'https://…' a website, 'chat:' a conversation (e.g. chat:support, chat:general), 'ai' the AI chat (people who manage), 'mailto:' or 'tel:' a mail or a phone call |
| variant | string | 'primary' (default), 'secondary', 'plain' or 'danger' |
| icon | string | An icon name (home, chat, calendar, star, settings, users, bolt, game, coin…) or an emoji |
| size | string | 'sm', 'md' (default) or 'lg' |
| full | boolean | Fill the width |
### section
A part of a page with a small title, an optional line under it and a link on the right (See all), holding child blocks. variant 'card' puts the children on one card. With open true it shows as one row that opens its children as their own screen (a settings page inside a page).
| Setting | Type | What it does |
|---|---|---|
| title | string | Small title over the section |
| subtitle | string | A line under the title |
| action | string | Words of the link on the right (See all, Edit) |
| href | string | Where the link goes: '#page-id' opens a page, 'https://…' a website, 'chat:' a conversation (e.g. chat:support, chat:general), 'ai' the AI chat (people who manage), 'mailto:' or 'tel:' a mail or a phone call |
| variant | string | 'plain' (default) or 'card' |
| open | boolean | Show as one row that opens the children as their own screen |
| icon | string | With open: the row's icon. An icon name (home, chat, calendar, star, settings, users, bolt, game, coin…) or an emoji |
Holds child blocks.
### stack
Lays out child blocks in a column (or a row).
| Setting | Type | What it does |
|---|---|---|
| direction | string | 'column' (default) or 'row' |
| gap | number | Space between children in px, 0-64, default 12 (row and grid take size steps instead) |
| align | string | 'stretch' (default), 'start', 'center' or 'end' |
Holds child blocks.
### row
Child blocks side by side, wrapping onto the next line when they don't fit: buttons, badges, small cards, stats.
| Setting | Type | What it does |
|---|---|---|
| gap | string | Space between: xs, sm, md, lg, xl. Default sm |
| align | string | Up and down: 'start', 'center' (default), 'end' or 'stretch' |
| justify | string | Across: 'start' (default), 'center', 'end' or 'between' |
| equal | boolean | Every child gets the same width (stats, cards) |
Holds child blocks.
### grid
Child blocks in columns of equal width: cards, stats, pictures. A child can span more columns with props.span.
| Setting | Type | What it does |
|---|---|---|
| columns | number | Columns on a phone, 1 to 6. Default 2 |
| columnsWide | number | Columns on a tablet or the web, 1 to 8. Default twice columns, up to 4 |
| gap | string | Space between: xs, sm, md, lg, xl. Default sm |
Holds child blocks.
### scroll
A shelf of child blocks people swipe sideways: cards, pictures, stats. Each child is as wide as its size (cards) or itemSize.
| Setting | Type | What it does |
|---|---|---|
| itemSize | string | Width of children that don't say: 'sm', 'md' (default), 'lg' or 'full' |
| gap | string | Space between: xs, sm, md, lg, xl. Default sm |
| snap | boolean | Stop on each child. Default true |
Holds child blocks.
### group
Grouped list rows on one card with thin lines between them (a settings list, a menu, a contact card). Its children are usually `item` blocks.
| Setting | Type | What it does |
|---|---|---|
| title | string | Small title over the group |
| footer | string | A small note under it |
| variant | string | 'inset' (default: on a card) or 'plain' (rows on the page) |
Holds child blocks.
### item
One list row: an icon or picture, a title, a line under it, a value on the right, a badge, and an arrow when it opens something. Opens href, or, when it holds child blocks, opens them as their own screen titled by the row.
| Setting | Type | What it does |
|---|---|---|
| title | string, required | Title |
| subtitle | string | A line under the title |
| icon | string | An icon name (home, chat, calendar, star, settings, users, bolt, game, coin…) or an emoji |
| tint | string | Colour of the icon square: A colour role from the style (accent, accent2, success, warn, danger, gold, text2…). Default depends on the block. Default accent |
| tone | string | How the icon square is painted: 'soft' (default), 'solid' or 'glossy' |
| image | string | https URL of a round picture in place of the icon (a face, a logo) |
| value | string | Short words on the right (On, 3 members, $40) |
| badge | string | A small pill on the right (New, 2) |
| href | string | '#page-id' opens a page, 'https://…' a website, 'chat:' a conversation (e.g. chat:support, chat:general), 'ai' the AI chat (people who manage), 'mailto:' or 'tel:' a mail or a phone call |
| danger | boolean | Red title (Delete, Leave) |
| chevron | boolean | The arrow on the right. Default: shown when it opens something |
Holds child blocks.
### divider
A thin line between parts, optionally with a word in the middle.
| Setting | Type | What it does |
|---|---|---|
| label | string | A word in the middle (or, Today) |
| inset | boolean | Start the line after the space of an icon, like between list rows |
### spacer
Empty space between parts.
| Setting | Type | What it does |
|---|---|---|
| size | string | xs, sm, md, lg, xl. Default md |
# Live: calls, streams, media and TV
Three engines (docs/KITS.md), four kits. Code: `convex/lib/live.ts` (blocks, kits, words, checks),
`convex/rooms.ts`, `calls.ts`, `livekit.ts`, `media.ts`, `screens.ts`, `screenAuth.ts`, `liveHttp.ts`, `push.ts`,
tables in `convex/lib/liveTables.ts`, screens in `src/kits/live/`, the TV link at `src/app/tv.tsx`, the iPhone screen-share extension in `targets/broadcast/`.
| Kit | Engine | Blocks |
| --- | --- | --- |
| Calls | Rooms | `call` (rings people), `room` mode talk (group call) |
| Live streams | Rooms | `room` mode stage, `media.gallery` of recordings |
| Media library | Files | `media.gallery`, `media.player` |
| Show on TV | Screens | `screen.pair`, `screen.remote` |
Every setting is a block prop, every word is in `blueprint.words` (defaults in `LIVE_WORDS`), and
every permission is a verb from `convex/lib/roles.ts` (a room's `speak`, `watch`, `host`; a screen
block's `pair`, `control`). The server reads props from the saved blueprint, never from the device.
## What keeps it safe
A gallery's files are only readable by people who can see that gallery
block. Upload links accept one exact size and type for 10 minutes, and unsaved uploads are
deleted hourly. Files that aren't images, video, audio or PDF are stored as plain downloads.
Recording links are random. Room passes last 10 minutes (LiveKit renews them for people
already in), and a host's remove or send-back holds for 2 hours. Calls are limited to 3 a
minute and 20 an hour per person, kids are only rung from their kids spaces, and ringing
without a call button needs friendship or "invite". A TV can only get a role that views and
plays, connecting one needs "invite", wrong codes are limited to 10 a minute, and a TV removed
from the space can't sign back in.
Files in private spaces get links that run out after an hour. Each space can keep 20 GB of media by default.
Recordings land in the "recordings" folder of the space's media library.
## Phones
LiveKit needs a development build (not Expo Go): `npx expo run:ios|android` or an EAS build.
**Screen sharing** works on the web, Android and iPhone. On iPhone it goes through a Broadcast
Upload Extension in `targets/broadcast/` (added to the Xcode project by `@bacons/apple-targets`
on `npx expo prebuild`): the share button opens iOS's "Start Broadcast" sheet, the extension sends
the screen (JPEG, up to 15 fps, longest side 1280px, to keep phones cool) over a socket in the
App Group `group.`, and LiveKit publishes it. The platform owner sets up the App Group.
**Ringing a closed phone.** Starting a call sends a push notification (convex/push.ts, Expo's
push service) to every phone of the people being rung, with Answer and Decline buttons. It is
time-sensitive on iPhone (it breaks through Focus) and uses a loud "Calls" channel on Android.
If nobody answers within 45 seconds, they get "Missed call from …". An open app rings with the
in-app card instead. People are asked to allow notifications the first time they ring someone
or answer a call, not at sign-in. The platform owner turns push on once.
Calls ring as a notification, not the full-screen phone-call screen (CallKit on iPhone,
a full-screen call intent on Android). That needs VoIP push keys on the server and native call
code; it can be added later without changing the call data.
## TV
Open `/tv` on the TV's browser (or a laptop you AirPlay or cast). It shows a code. Someone with the
`pair` permission types it into a "Show on TV" block; the screen signs in as its own account
(`ConvexCredentials` provider "screen"), joins the space with the block's role and shows a page.
Because it's a member, every block works on it, live. `screen.remote` switches what it shows.
Opening `/tv` while signed in as a person shows any page full screen instead. Codes nobody used
are cleared by `screens._sweep` (hourly cron).
## Tests
`tests/live.test.ts` (convex-test + vitest, `npm test`): calls ring, answer, end and time out;
a call rings a closed phone by push and dead push tokens are dropped; rooms grant
speaker, audience and host; the webhook keeps counts; a TV pairs, shows pages and disconnects;
the media library keeps its permissions.
# Game lobbies
A games page that looks and feels like a real game app, made only of blocks. It is how the
Business starter's **Game Night** page is built, and any AI can build the same thing (or an apps
page, a class's activities, a menu of challenges) from the same parts.
Code: `convex/lib/lobby/` (descriptions, checks, art names, palettes, words, ready page),
`convex/kits/lobby/lobby.ts` (one server read), `src/kits/lobby/` (screens), `tests/lobby.test.ts`.
## The blocks
| Kind | What it is | Holds |
| --- | --- | --- |
| `poster` | One bright card: art on a gradient, the name in big letters, a badge (HOT, NEW, LIVE), a corner hook, a line. A game poster opens a sheet (how to play, players, time, your best, big Play) and then the game full screen; a link poster opens its `href`. | |
| `posters` | The main shelf: a grid of posters in small (3 across), medium (2) or large (1, wide) with a switch. Cards pop in one after another. | `poster` |
| `showcase` | The featured carousel: big wide cards with a PLAY button, moves on by itself every `every` seconds. | `poster` |
| `game.live` | Games people started that others can still join, with how many are in and a Join button. Hidden when nothing is on. | |
| `game.winners` | The latest winners scrolling by. | |
| `game.record` | Each person's own plays, wins and best score per game. | |
| `lobby.header` | Logo square, a line under the page title, and the person's coins counting up (Ledger). | |
| `lobby.row` | A chat-list row that opens the games page: a 2×2 mosaic of its posters' art, the name, the games, a count. | `poster` (optional, extra) |
| `game.frame` | One game's play screen on a page. Posters open the same screen by themselves. | |
Every prop is described once in `convex/lib/lobby/blocks.ts`; HQ, the page editor and the AI read
the same descriptions.
## Looks are settings
- **The page** gets chat-app's **Arcade glow** look by default (`ARCADE_LOOK` in
`convex/lib/scene.ts`, a page `look`): a deep night ground, grid lines that sweep from teal to
violet to amber to pink, soft lights drifting, a lit floor of lines moving toward you at the
bottom, light streaks, glass cards with a lit edge and glowing section titles. It's only style
settings (`backdrop.lines`, `glow`, `floor`, `streaks`), so the AI can change any of it or give
the same look to any other page. `gamesLobbyPage({ look: null })` keeps the space's own look.
- **Art** (`art`): one of 20 drawn scenes by meaning (`trophy`, `board`, `survey`, `mask`, `sketch`,
`bubble`, `dice`, `wheel`, `bingo`, `cards`, `bomb`, `gift`, `detective`, `number`, `poll`,
`buzzer`, `crown`, `mic`, `tv`, `rocket`), an emoji, or an https picture. Each scene has a part
that floats gently.
- **Colours** (`palette` or `colors`): 12 palettes (`sunset`, `ocean`, `grape`, `lagoon`, `ember`,
`lime`, `candy`, `night`, `gold`, `mint`, `berry`, `sky`) or your own 2–3 colours / style roles.
- Leave both out and a game poster picks art from what the game does (a board for a quiz board, a
pencil for a drawing game) and a palette from its id, the same every time. A game the AI just
made already looks finished.
- The play screen borrows the look of its game's poster wherever it is.
- **Words**: `blueprint.words["lobby."]` (keys in `convex/lib/lobby/words.ts`).
## The play screen
Intro splash (one second, tap skips; off with reduced motion), then a start screen (art, players /
rounds / time, how to play, name, Start or Join · CODE, a code box). In the game: a header in the
game's colours with the round and a timer, a progress bar, a chip per turn with what you scored,
the game itself (the Rounds engine's `GamePlay`), and confetti when you win.
## Light on the device
- One server read per shelf (`convex/kits/lobby/lobby.ts summary`), only for the parts a block
shows; it stops while a game is open full screen or the page is in the background.
- Only cards someone is playing right now (and the showcase card on screen) animate on their own.
Everything else pops in once and rests. All motion is native-driven and off with reduced motion.
- No images to download: art is vector (react-native-svg), gradients are styles.
## For the AI
The guide part is `LOBBY_GUIDE()` in `convex/lib/lobby/guide.ts`. Typical page, top to bottom:
`lobby.header`, `showcase`, `game.live`, `posters` "House games", `posters` "Quick play" (small,
hrefs), `game.winners`, `game.record`, `ledger.leaderboard`. `gamesLobbyPage()` in
`convex/lib/lobby/page.ts` builds exactly that from a list of games.
When the AI makes a new game it also adds a poster for it.
A poster for a game that was taken out (its kit removed) is not an error: it hides for members and
shows greyed to people who manage the space.
# Performance, battery and size
Zvessa has to feel instant on a mid-range phone, stay cool, and stay small. These rules apply to
every screen, block, kit and engine. A change that breaks one says why in its description.
## Budgets
- **60 fps** while scrolling and animating (120 on ProMotion screens). No dropped frames on a
three-year-old Android phone.
- **Idle means idle.** With nothing changing on screen, the app does no work: no timers, no
animation frames, no polling, no sensor reads.
- **Background means asleep.** In the background the app holds only its Convex connection; mini-apps,
animations, sensors and media are paused.
- **Size.** The JS bundle grows only with code people use; new native packages are added only
when the package plan says so. Icons cost 18 KB on Android and web, nothing on iOS.
## Drawing
1. **Few views.** Draw decoration (grids, dots, gradients, glows) with style properties
(`backgroundImage`, `boxShadow`), never with one view per line or dot.
2. **Lists that can grow are virtualized.** Chat, records, history, members, feeds: use a list
component (FlashList once installed, FlatList until then), never `.map()` inside a ScrollView.
Give rows a stable `key` and wrap the row component in `memo`.
3. **Lists inside a page load in pages.** A block inside a scrolling page can't virtualize, so it
asks the server for a page (about 50) and shows "Show more" (`blueprint.words.showMore`).
4. **Don't redraw what didn't change.** Derived values (`resolveStyle`, lookups, filtered lists)
go in `useMemo`; look things up through a `Map`, not `.find()` inside a loop.
5. **Images** come in at the size they're shown (ask the server for a thumbnail), with a
placeholder (thumbhash) and `expo-image` caching once installed.
## Motion
6. **Animations run on the UI thread.** `useNativeDriver: true` today, Reanimated worklets once
installed. Animate `transform` and `opacity` only, never `width`, `height`, `top` or colours.
7. **Decoration stops when it isn't seen.** Anything that moves on its own uses
`useDecorativeMotion()` from `src/lib/activity.ts`: it is false when the screen is covered, the
app is in the background, or the person turned on Reduce Motion.
8. **A style's `motion.speed: "off"` turns off all decorative motion.** Respect it everywhere.
## Work that keeps running
9. **Gate it on `useLive()`.** Timers, sensors, location, camera, audio, live streams and game
loops start only while `useLive()` (screen showing and app in the foreground) is true, and stop
in the effect's cleanup.
10. **Sensors** (`expo-sensors`): subscribe in an effect gated on `useLive()`, set the slowest
`setUpdateInterval` that works (100 ms for tilt and shake, 16 ms only for a game that needs
it), and always remove the listener. Location: lowest accuracy that works, never in the
background unless a kit is about background location and the person turned it on.
11. **Mini-apps pause themselves.** `MiniApp` calls `window.myaibio.setPaused()` when hidden:
animation frames, timers and media stop, and the mini-app gets `myaibio:pause` and
`myaibio:resume` events to save state. Timers can't fire faster than every 16 ms.
12. **No polling.** Live data comes from Convex subscriptions. Something outside Convex is
fetched by a Convex action or a scheduled job, never by a timer on the phone.
## Data
13. **Subscribe to what the screen shows.** Every `useQuery` returns a bounded result (`.take(n)`
or pagination, never `.collect()` on a table that grows). Counts and totals come from
`@convex-dev/aggregate`, not from reading every row (`records.count` moves there with the
Events engine).
14. **Ask once.** Two blocks that need the same data call the same query with the same arguments;
Convex shares one subscription.
15. **Small results.** Queries return only the fields a screen draws; big text and files are
fetched when opened.
16. **Writes are cheap.** Mutations take what changed (a patch), not the whole document.
## Loading and size
17. **Native first, package second.** Use what React Native and Expo already ship (style
gradients, `boxShadow`, `expo-symbols`) before adding a package. Every new package goes
through the package plan and `npx expo install`.
18. **Heavy and rare code loads on demand on the web.** Kits a workspace hasn't added (and their
editors) are imported with `React.lazy` so first load stays small. On phones the bundle is
Hermes bytecode read from disk, so this matters less there.
19. **Release builds shrink.** Android uses R8 and resource shrinking (`expo-build-properties`),
iOS and Android ship Hermes bytecode, and the web build is minified with tree shaking.
20. **Measure before and after.** Use the React Native DevTools profiler and Expo Atlas
(`EXPO_ATLAS=1 npx expo export`) to check render counts and bundle size.
## Icons
Use `Icon` from `src/icons` with a name from `src/icons/icons.json` ("send", "delete", "ai"…):
```tsx
```
- iOS draws Apple's SF Symbols natively. Android and the web draw Material Symbols from
`myaibio-icons.ttf`, a font holding only the listed icons (18 KB instead of 1 MB).
- Names say what an icon means, not what it shows, so blueprints and the AI pick from the same
list (`ICON_NAMES`). Unknown names draw a circle instead of failing.
- Colour is a style role and size scales with the style's type, like text.
- To add one: add `"name": ["sf.symbol.name", "material_name"]` to `icons.json`, then run
`python3 src/icons/build-font.py` (needs `pip install fonttools`) and `npm run typecheck`, which
rejects SF Symbol names that don't exist.
# Photo books
A photo book is a blueprint setting (`blueprint.books`): a cover, then pages that each lay out a
few photos from a feed (the Gallery by default). The photos stay posts in their feed; a book only
points at them, so a deleted photo simply drops out. Two general blocks show books, and the AI
builds them with the same blueprint changes as anything else.
Code: `convex/kits/photobook/` (lib, kits, blocks, books, guide), `src/kits/photobook/`,
`tests/photobook.test.ts`. The starter is `convex/lib/templates/photobook.ts`.
## The kit
`photo-book` (group Build) includes the Gallery, adds one book (`book`, chapters by month) and a
page (`photo-book`) with the book and a shelf. Taking the kit out takes `/books` with it (undo
brings it back).
## A book
| Setting | What it does |
| --- | --- |
| `id`, `name` | Short kebab-case id; the name people see |
| `feed` | Where the photos come from (a `/feeds` id), default `gallery` |
| `tag` | Only photos with this #tag: an album |
| `from`, `to` | Only photos posted between these days (YYYY-MM-DD) |
| `shape` | `portrait` (default), `square` or `landscape` |
| `cover` | `{ title?, subtitle?, photo?: ref or "none", layout?: photo · framed · plain, tint? }` |
| `fill` | How it lays itself out: `{ order: old · new, perPage: mix · 1–4, chapters: none · month · day · tag, captions, max: 1–400 }` |
| `pages` | Hand-made pages. Unset: the book lays itself out from the photos and grows with them |
| `end` | Words on the last page |
| `samples` | `[{ emoji, tint?, label? }]`: a sample spread shown until the book has photos |
A **page** is `{ id, layout, photos?, title?, text?, date?, tint? }`. Layouts: `full` (1),
`framed` (1, with its caption), `two` (2 side by side), `stack` (2 one above the other), `trio`
(1 big and 2 small), `grid` (4), `words` (none), `chapter` (a title or a date, with an optional
photo behind it). A **photo ref** is `""` or `"#"` (the post's nth photo).
`tint` is a style colour name (`tint1`…`tint6`, `accent`, `paper`), never a hex value.
Laying out (`autoPages`) is pure and stable: the same photos always make the same pages with the
same ids. With `perPage: "mix"` a photo with words gets a framed page (at most every third page),
two wide photos stack, two tall ones sit side by side, a wide one with two tall ones makes a trio,
and every fifth page with four photos to go is a grid.
Limits: 24 books, 200 pages a book, 1200 placed photos in all. Big books should keep laying
themselves out.
## Blocks
- `photobook.book` `{ book, view: flip · scroll, start: cover · first, thumbs, edit }`: the cover,
then pages you swipe through with a light page-turn tilt (off when the device asks for less
motion), a page count and a strip of small pages. Tap a page to read it full screen. People who
manage the space get **Edit book**.
- `photobook.shelf` `{ title, books, albums, feed, columns, empty }`: covers that open full screen.
The space's books first, then (albums, default on) one book for every #tag of the feed, so each
Gallery album is a book by itself.
## Editing a book
**Edit book** opens a sheet with the cover (title, words, photo, layout, paper), the shape and
chapters, and every page as a small picture. Tap a page to change its layout, swap a photo (tap a
slot), write a title or words, pick its paper, move it or remove it. "+" adds a page from up to
four picked photos, with the layout that fits them. A book that lays itself out becomes hand-made
the first time a page changes ("Arrange by hand"); "Lay out from photos again" goes back. Save is
one blueprint change (`blueprint.change`), so it is one version with undo in History.
## Server functions (`convex/kits/photobook/books.ts`)
Every read names the block that shows the book; the server finds the block and checks the person
can see it, so a book is exactly as visible as its page and block.
- `read({ block, book?, album?, upTo?, all? })`: the cover, the page count and the first `upTo`
pages with photo links (the app asks for more a few pages ahead). `all` (managers) adds every
page with its refs, for editing. Gone photos leave an empty slot for managers and drop out for
everyone else.
- `photos({ feed?, tag?, before? })` (manage): photos to pick, newest first, 48 posts at a time.
- `shelf({ block })`: the shelf's book covers and albums (tag, count, newest photo).
- `_photoList` (internal): what the AI's `list_photos` tool returns.
## The AI
The guide (kits part) explains books, layouts and refs. `list_photos { feed?, tag?, limit? }`
(scope `data.read`, manage) returns each photo's ref, caption, #tags, shape (wide, tall, square)
and date; captions are marked as data. "Make a book of #italy with a chapter for each day" is one
proposal: `{ op: "add", path: "/books/-", value: { id: "italy", name: "Italy", tag: "italy", fill: { chapters: "day" } } }`
plus a page or shelf that shows it.
## Words
Every word is `blueprint.words["book."]` (keys in `BOOK_WORDS`, `convex/kits/photobook/lib.ts`).
## The Photo book starter
`photobook` in `convex/lib/templates/photobook.ts`, in the **Album** style (warm paper and ink,
serif titles, terracotta and sunflower, charcoal and amber by night). Tabs: Book (the book that
fills itself, then tiles: Add photos, Albums, Guestbook, New book), Photos (the Gallery grid), Albums
(the shelf), Chats, Guestbook; HQ and the AI in the top bar. Kits: photo-book (with the Gallery),
guestbook, direct-chat, group-chat, analytics. It is only blueprint data made of general blocks,
so the AI can build the same thing in any space. Its picker card uses `showcase`.
# Designed QR codes
A QR code is a block like any other: what it holds and how it looks are props, so the AI (in the
app or over MCP) and a person in HQ make the same thing. Nothing is on by default: a space has QR
codes only where someone adds a `qr` block, turns on `qr` on `web.share`, or uses `invites.code`.
## What it holds
One of:
| Prop | Encodes |
|---|---|
| `value` | Any text or full link, up to 600 characters. Wi-Fi: `WIFI:T:WPA;S:;P:;;` |
| `page` | A link that opens that page of the space in Zvessa |
| `link: "space"` | A link that opens the space itself (`/s/`) |
| `link: "web"` (+ optional `page`) | The space's public web address (Public links kit); hides while the space isn't on the web |
| `link: "invite"` | The viewer's own invite link (needs `/referrals/codes`); each person sees their own |
Other props: `label` (words printed under the code, inside the design), `title`, `size` (96-360,
default 220), `actions` (Share / Copy link / Print buttons, default on), and the button words
`share`, `copy`, `print`.
## How it looks: `design`
Every setting is optional. A `look` is a preset of the others; anything set alongside it wins.
| Look | Dots | Corners | Paper |
|---|---|---|---|
| `classic` (default) | square | square | square |
| `rounded` | soft | round | rounded |
| `round` | round (joined like pills) | round | rounded |
| `dots` | dot | circle | rounded |
| `circle` | dot | circle | a round badge, like Apple's, with a ring of dots |
- `dots`: square, soft, round, dot, diamond. `eyes` (the three corner squares): square, round, circle, leaf. `shape`: square, rounded, circle. `fill`: the circle's ring of dots (default on).
- Colours are style values, like everywhere else: a role (`accent`, `text`…), `#hex`, or `mix()`/`alpha()`/`contrast()`. `ink` (default the style's `qrInk`, else black), `paper` (default `qrPaper`, else white), `eyeInk`, `border`, and a `gradient` (`linear` or `radial`) from `ink` to `ink2` at `angle`.
- `logo` in the middle: `zvessa` (the app's spiral), `initial` (a letter, default the space's first), or `image` (`logoImage`, an https address). A logo raises error correction to H.
- `caption` (up to 60 characters), `ecc` (L, M, Q, H; default M, H with a logo), `margin` (2-6 modules).
**It always scans.** Ink is kept dark and paper light with at least 4:1 contrast; when a style or
a design asks for something cameras can't read (light ink, dark paper), the colours are made
darker or lighter and the AI's tools say so. The circle's decorative dots stay two modules clear
of the code. `tests/qr.test.ts` draws every look, dot, corner, logo and gradient exactly as the app
does and reads it back with ZXing.
## Sharing, saving, printing
On phones, **Share** saves the code as a 1024 px PNG and opens the share sheet, which also offers
Save Image (Photos) and Print. On the web, Share shares the PNG file where the browser can, or
downloads it; **Print** prints the code alone, centred, 12 cm wide. **Copy link** shows for links.
No new native package: drawing is react-native-svg (already in the app), the picture is its
`toDataURL`, and sharing is expo-sharing and expo-file-system. A logo `image` from another site
may be left out of a saved picture (browsers don't load outside pictures into SVG images). If
printing straight from the app (no share sheet) is wanted later, that's `expo-print`, which needs
a new EAS build.
## Where else
- `invites.code` takes the same `design`.
- `web.share` takes `qr: true` and a `design`: the address as a code, in the app and on the web page.
- Public web pages draw `qr` blocks inline as SVG (no script, no request); `link: "invite"` stays in the app.
## Tools (AI and MCP)
- `preview_qr` (scope read): what a code would encode, its version, error correction and any
colour fixes; `svg: true` returns the SVG file. Changes nothing.
- `make_qr` (scope propose, needs manage): proposes adding a `qr` block to a page; a person
approves it, and it can be undone.
## Code
- `convex/kits/qr/encode.ts`: the QR maker (byte mode, levels L/M/Q/H, versions 1-40). Pure code.
- `convex/kits/qr/design.ts`: designs → shapes → SVG; colour fixes; checks.
- `convex/kits/qr/blocks.ts`, `tools.ts`, `run.ts`, `qr.ts`: the block, the tools, the tools' runner, the space's context for them.
- `src/kits/qr/QrCode.tsx` (drawing, share, print), `src/kits/qr/QrBlocks.tsx` (the `qr` block).
- `convex/kits/invites/qr.ts` remains for older imports.
# Record views and the Journal starter
Record views are more ways to show and write a collection's records, on the Collections engine. They work for any collection: a journal, a gratitude log, a habit or workout tracker, a reading list, a sleep diary, a team's daily check-in. The Journal kit and the Journal starter are made only of these blocks plus existing ones, so the AI can build any of those from chat the same way.
## The blocks
All are described in `convex/lib/records/views.ts` (the AI, the page editor and the docs catalog read the same descriptions) and drawn by `src/kits/records`.
| Block | What it is |
|---|---|
| `records.composer` | A card with a question, the chip field's options to tap (a mood) and a button that opens the full-screen writing sheet |
| `records.timeline` | Records under month or day headers, with search (words and #tags) and a Write button. Tap one to read it |
| `records.calendar` | A month painted with each day's chip colour; swipe or tap arrows for other months; tap a day to see or write for it |
| `records.streak` | Days in a row, the best streak, the total, this week as dots, an optional goal bar, and a nudge until today has one |
| `records.memories` | "On this day": records from a week, a month, six months and each year ago. Hidden when there are none |
| `records.prompt` | A daily prompt (the same all day, new tomorrow), "another one", and Write, which saves the prompt on the record |
| `records.stats` | Insights over the last N days: days written, records, words, best streak, and a bar per chip option |
Tapping a record anywhere opens the reader: the whole record, Share (the system share sheet), Edit (the writing sheet again) and Delete (with a confirm). Everything updates live.
### Which field is which
Props name the field for each part, so the blocks fit any collection:
- `title`: a text field (default: a field called `title`)
- `body`: a longtext field (default: the first longtext)
- `chip`: a choice field drawn as a coloured chip, with `icons` (an emoji or icon per option) and `tones` (a colour role or colour per option; default tint1, tint2…)
- `date`: a date field, the record's day (default: the first date field; without one, the day it was written, at the person's own midnight)
- `tags`: a text field of #tags. #tags written in the body count too
- `field`: a text field holding the prompt the record was written from
`"none"` or `""` turns a part off. A field the collection doesn't have is simply not drawn, so an older collection keeps working.
### The writing sheet
A native page sheet (swipe down on iOS): the day (‹ Today ›, a day at a time, never the future), the prompt if any, the chip options as big buttons, a big title, the body with a word count, #tags, then any other fields of the collection. Save plays a success haptic and the block shows the `saved` words (`{streak}` becomes the current streak). Cancel asks before throwing away what was written.
All words are props: `question`, `note`, `write`, `placeholder`, `saved`, `label`, `shuffle`, `empty`, `heading`, `nudge`, `done`.
## Server reads
`convex/kits/records/views.ts`: `days` (each day with records: count, newest chip, words), `onDays` (the records on given days), `search` and `one`. They follow the same rule as `records.list`: a person who can view the space sees a collection unless every block that shows it is hidden from them, so `visibleTo: ["owner"]` on a page keeps a journal private on the server too. Each read looks at the newest 2,000 records. Writing goes through `records.add`, `records.update` and `records.remove`.
## Journal kit
`convex/kits/journal/kits.ts`. Adds the `journal` collection (day, mood, title, entry, tags, prompt) and two owner-only pages: Journal (streak, prompt, composer, on this day, entries) and Mood calendar (calendar, insights). The collection id is the same as the Personal starter's journal: a space that already has a `journal` collection keeps it and its entries, gets only the missing fields, and keeps it if the kit is taken out.
## Journal starter
`convex/lib/templates/journal.ts`, in the Paper style (cream paper and ink, serif headings, sunrise accents, a night-blue dark mode; tint1 to tint4 are the moods).
- **Today**: a header, the streak, "How are you feeling?" with the four moods, today's prompt, on this day, and tiles to Entries, the calendar, Photos and "Reflect with AI".
- **Entries**: the searchable timeline.
- **Calendar**: the mood calendar and a 30-day streak goal.
- **Photos**: the gallery feed, owner only.
- **Insights**: the last 30 days, this year, and a 100-day goal.
- **HQ** and the AI in the top bar.
Every journal page has `visibleTo: ["owner"]`. Its `showcase` gives the picker a bright cover (tagline, gradient, stickers and three mini cards).
# Reviews and ratings
Star ratings with words and photos about anything: services, products, places, games, mini-apps,
people, a collection's records. Part of the Spaces engine. Code: `convex/kits/reviews/` (server and
settings), `src/kits/reviews/` (blocks), tests in `tests/reviews.test.ts`.
## A board is a setting
`blueprint.reviews` is an object of review boards by id. The Reviews kit adds board `reviews`; the
AI or the owner can add more (a `menu` board and a `staff` board).
```json
"reviews": {
"reviews": {
"name": "Reviews",
"subjects": [{ "id": "cut", "name": "Haircut" }, { "id": "colour", "name": "Colour" }],
"aspects": [{ "id": "value", "name": "Value" }, { "id": "friendly", "name": "Friendly" }],
"writers": ["member", "visitor"],
"verified": ["booked", "badge:verified"],
"managers": false,
"photos": 4
}
}
```
| Setting | What it does | Default |
| --- | --- | --- |
| `name` | The board's name | required |
| `scale` | Stars, 3 to 10 | 5 |
| `icon` | What a star is: an emoji (★ ❤️ 🔥) or an icon name | ★ |
| `subjects` | Things to pick when writing: `[{ id, name, about? }]` | none |
| `subjectsFrom` | A collection whose records are the things to review (`record:`) | none |
| `aspects` | Up to 6 parts rated on their own: `[{ id, name }]` | none |
| `writers` | Who may write: role ids, `member`, `level:`, `level:+`, `badge:` | roles that can post or add |
| `verified` | Whose reviews get the Verified mark: same tokens plus `booked` (has a booking here) | none |
| `managers` | Whether people who manage may review their own space | true |
| `text` | `off`, `optional` (stars alone are fine) or `required` | optional |
| `minLength`, `maxLength` | Characters | 0, 2000 |
| `titles` | A one-line title | false |
| `photos` | Photos per review, 0 to 10 | 4 |
| `approval` | New and edited reviews wait for someone who manages | false |
| `replyRoles` | Who may answer in public; `[]` = nobody | people who manage |
| `helpful` | Helpful / not helpful votes | true |
| `reports` | Reporting | true |
| `hideAfter` | Reports before a review hides itself until checked | 3 |
| `perDay` | Reviews one person may write per day on the board | 10 |
| `links` | Links in reviews (blocked by default: mostly spam) | false |
| `edit` | People can change their review | true |
Every word is a setting: `blueprint.words["reviews."]` (keys in `REVIEW_WORDS`,
`convex/kits/reviews/model.ts`).
## Subjects
Every review is about a subject, and each person writes one review per subject (writing again
changes it). A subject is one of:
- an id from the board's `subjects` list,
- `record:`, a record of the board's `subjectsFrom` collection,
- any short id a visible block names (`app:weather`, `game:trivia`, `@handle` for a person),
- `main`, the board as a whole, when the board has no list.
So putting `reviews.stars` with a new subject on a page is all it takes to make that thing
reviewable. Anything else is refused, so nobody can fill the database with made-up subjects.
People can't review themselves (`@handle`).
## Blocks
| Block | What it shows |
| --- | --- |
| `reviews.summary` | Average in big stars, count, a bar per star (tap to read those), aspect averages, Write a review |
| `reviews.list` | Reviews with sort (most helpful, newest, highest, lowest) and filters (stars, with photos, verified); your own first |
| `reviews.stars` | A small ★ 4.6 (128) line that opens the reviews in a sheet; hidden until the first review unless `empty` is set |
| `reviews.write` | A "How was it?" card with stars to tap; shows your review once written |
| `reviews.top` | The board's subjects ranked by rating or by how many reviews |
| `reviews.queue` | Waiting and reported reviews, to approve, hide or keep (people who manage only) |
Inside every review: avatar and name, the Verified mark, stars, aspects, title, words (Read more),
photos full screen, helpful votes, the owner's answer, report (spam, offensive, fake, off topic,
something else), edit and delete your own, hide and approve for people who manage.
## Who sees what
- A board whose every block is hidden from a person (visibleTo, levels, badges) is hidden on the
server too, like collections and feeds.
- Only live reviews show and count. Pending and hidden ones are seen by their writer and by people
who manage.
- Reviews that hit `hideAfter` reports hide themselves and go to the queue. Editing doesn't bring
them back; Keep does (and clears the reports).
- With `approval`, edits go back to waiting, so a review can't be swapped after approval.
## AI tools (MCP, API keys, in-app AI)
Boards, blocks and pages are built like everything else: `add_kit reviews`, or `propose_change`
on `/reviews` and pages, approved by a person. Reading and acting on reviews are builder tools
(`convex/lib/capabilities/tools.ts`, run in `convex/capabilities/run.ts`):
| Tool | Scope | What it does |
| --- | --- | --- |
| `list_reviews` | data.read | Totals and reviews of a board or subject; status live, pending, hidden or reported |
| `answer_review` | data.write | The space's public answer under a review (empty takes it away) |
| `moderate_review` | data.write | approve, hide or keep a review |
All need the manage permission. There is no tool that writes a review on purpose: reviews come
from people, so an AI or an API key can never fake one.
## Light on the device
- A summary or a star line reads one totals row (`reviewStats`), however many reviews there are.
- Lists load a page at a time (Convex pagination) with indexes for each sort and star filter.
- Photos upload to the media library folder `reviews` and go with the review when it's deleted.
## Recipes
- **Services page:** a board with the services as `subjects`, `reviews.summary` + `reviews.list`
with subject `*`, and `reviews.stars` under each service with its subject.
- **Product catalog:** `subjectsFrom` the products collection, plus `reviews.top`.
- **Games or mini-apps:** subjects `game:` / `app:` and a `reviews.stars` next to each.
- **A coach or tutor:** subject `@handle`, `writers: ["member"]`, `verified: ["booked"]`.
- **A business:** `managers: false` so the owner can't review their own business.
# Moderation and safety (the Safety engine)
What the app stores ask for wherever people post: a way to report content and people, a way to
block people, a filter for objectionable words, and someone who acts on reports. In Zvessa all
of it is general pieces (settings, blocks and tools) that people and any AI combine.
## Always on, in every space and direct chat
- **Report** a chat message (`post`), a feed post (`feedPost`), a record (`record`), a person
(`person`) or a whole space (`space`). Report and Block sit in every post menu, people list and
profile; no kit or block is needed.
- **Block** a person everywhere: neither sees the other's chat or feed posts, and neither can
message, call, friend, follow or group the other. Blocking ends a friendship and follows.
- Reports go to the space's **moderators** (the owner, roles that can manage, or
`safety.modRoles`). Serious reasons (`safety.escalate`), anything in a kids space, and anything
outside a space (direct chats, whole spaces) also reach **Zvessa's admins**.
## Settings: `blueprint.safety`
| Key | What | Default |
| --- | --- | --- |
| `words` | Words and phrases to catch. `word` = whole word, `word*` = any word starting with it, `f*ck` = any letter in the star | none |
| `presets` | Ready lists: `basic` (strong swearing), `insults` | none |
| `links`, `contacts` | Catch links; catch phone numbers and emails | off |
| `action` | `mask` (f•••), `hold` (waits in the review queue), `refuse` | mask |
| `reasons` | What people pick when reporting | Spam, Harassment or bullying, Hate, Sexual content, Violence or threats, Self-harm, Scam or fraud, Someone's private info, Something else |
| `escalate` | Reasons that also reach Zvessa's admins | Sexual content, Violence or threats, Self-harm |
| `modRoles` | Roles that moderate | roles with `manage` |
| `autoHide` | Hide a post or record once this many people reported it (0 never) | 0 |
| `notify` | Push to moderators when a new case opens | on |
| `who` | `members` or `anyone` may report | members |
The filter undoes look-alikes (sh1t, $hit), squeezes repeats (fuuuck), ignores accents, and only
matches whole words (so "Scunthorpe" and "class" pass). It runs on the server on chat posts and
edits, feed posts, titles and edits, and records' text fields (records can't wait, so held words
are refused there). Direct chats have no moderator: nothing is filtered unless a kid is in it, and
then held words are refused.
**Kids spaces** never go below: `basic` + `insults`, links and contacts caught, `hold`,
`autoHide` 1, every reason escalated, only members report.
## People who can't post
- **Mute** (a moderator, per space, default 24 h): can't post or add records in that space.
- **Ban** (a moderator, per space): removed, can't rejoin by invite or request; `unban` undoes it.
- **Suspend** (admins): the account can only look, everywhere; nobody can reach it.
- **Lock** (admins): the space closes to everyone but its owner, who can only look.
## Blocks
`safety.queue` (moderators' review queue; `quiet: true` shows nothing while empty, used in HQ),
`safety.filters` (settings editor with a try-it box), `safety.blocked` (my blocked people),
`safety.report` (a Report button for the space, a person, a post or a record), `safety.block`
(a Block button for a person), `safety.admin` (Zvessa's admins only; pair with `miniapps.review`).
Words: `words["safety."]` (convex/lib/safety/words.ts).
The **Moderation kit** (`moderation`) adds a Safety page (queue, filter, blocked list) and turns on
`presets: ["basic"]`, `action: "mask"`, `autoHide: 3`.
## Moderator actions
`dismiss`, `hide`, `show` (also lets a held post through), `remove` (records), `warn` (push with a
note), `mute`, `ban`, `unban`, `escalate` (to admins); admins also `suspend`, `unsuspend`, `lock`,
`unlock`. Every action closes the target's open reports and is written to the space's log.
## Functions (`api.kits.safety.safety`)
Anyone: `report`, `block`, `unblock`, `blocked`, `isBlocked`, `me` (suspended, locked, reasons,
isMod, isAdmin, recent warnings), `tryFilter`. Moderators: `queue`, `waiting` (a count for a
badge), `act`. Managers: `settings`, `setSettings` (saves a version; undo from History). Admins:
`admin`, `act` with `asAdmin`.
## MCP and the in-app AI
| Tool | Scope | What |
| --- | --- | --- |
| `list_reports` | read | The review queue (moderators). Members' words come back marked as data |
| `check_text` | read | What the filter would do to some words |
| `act_on_report` | moderate (new) | Any action above, as the token's person |
| `report` | data.write | Report as the token's person |
| `block_person` | data.write | Block or unblock as the token's person |
| `list_blocked` | read | The token's person's blocked list |
| `safety_admin` | moderate | The admin view (admins only) |
The in-app AI can read the queue and try the filter, and proposes `/safety` changes like any other
change. Moderating is always a person's decision: in the app, or with their own MCP token.
## Tables (`convex/kits/safety/tables.ts`)
`reports` (one per person per thing; the filter files held posts as reports by their writer with
the reason "Caught by the filter"), `userBlocks`, `safetyHolds` (mute, ban, suspend, lock;
optional `until`), `safetyLog`.
Adding a new kind of reportable thing: one entry in `TARGET_KINDS`/`ACTIONS_FOR`
(convex/lib/safety/model.ts) and one loader in convex/kits/safety/targets.ts.
# Search
One search across a space: pages, records, messages, feed posts, people and files. People use the
`search` block; any AI uses the `search` tool (in-app chat, MCP, or API keys). Everyone only finds
what they could open anyway; the server checks every result.
## The block
```json
{ "id": "find", "kind": "search", "props": { "sources": ["records"], "collection": "tasks-items", "placeholder": "Find a task" } }
```
| Prop | What it does |
| --- | --- |
| `sources` | Some of `pages`, `records`, `messages`, `posts`, `people`, `files`. Default all. |
| `collection` / `stream` / `feed` | Only that collection, conversation or feed. |
| `limit` | Results per kind, 1–25 (default 5). |
| `title`, `placeholder`, `empty` | Words on the block. |
| `groups` | Small heading per kind (default true). |
Words are settings too: `blueprint.words["search.placeholder"]`, `search.empty`, `search.short`,
and one per kind (`search.records`…).
Recipes: a people finder (`sources: ["people"]`), a search inside one chat (`stream: "general"`), a
product finder on a catalog page (`collection: "catalog-items"`).
## The kit
**Search** (Tools): adds a Search page with one search box. Optional; the block works anywhere.
## The tool
`search { query, sources?, collection?, stream?, feed?, limit? }` (scope `data.read`, read-only).
Returns a list of `{ kind, id, title, snippet, at, by, open }`; `open` says where it lives: `page`
(+`block`), `key` (+`thread`) for a conversation, `post`, `userId`, `collection`, `media`.
With an MCP token it searches as the token's person. The in-app AI searches as a manager but never
sees anyone's direct chats or private groups.
## What each person finds
Pages and blocks they can see (visibleTo, levels, badges; never HQ) · records in collections not
hidden from them · messages in conversations they can open (held posts only for the writer and
moderators, hidden posts for moderators, case notes for the team) and their own direct chats · feed
posts they could see in the feed · members (only if they are a member) · files they uploaded or that
sit in a gallery they can see.
## For developers
`api.kits.search.search.everywhere({ q, sources?, collection?, stream?, feed?, limit? })`. Search
uses Convex search indexes on `records.text`, `posts.text`, `feedPosts.text` and `media.name`.
Code that writes records must also write `text: recordText(data)` (`convex/lib/search.ts`).
After upgrading an existing deployment, run `npx convex run kits/search/search:_backfill` once.
# Templates and kits people make
Any space can be saved as a **template** (the whole space: pages, tabs, look, words, and everything
they use) and any pages as a **kit** (those pages plus the collections, conversations, feeds, books,
games, mini-apps, styles, connections, roles and ledger entries they point at). Both are a *piece*:
plain blueprint data, so whatever is added becomes the owner's to change by hand, by chat or over MCP.
Code: `convex/kits/sharing/` (piece.ts pure logic, library.ts server, tools.ts + run.ts AI/MCP tools,
kits.ts blocks and kit, model.ts names and `blueprint.fromLibrary` check), `src/kits/sharing/`
(blocks), `tests/sharing.test.ts`.
## Flow
1. **Save** (a person in the app, or the AI with `save_template`): makes the piece, takes private
things out, checks it would make a valid space, and keeps it **private** to its author. The
author can add a private one to any space they manage.
2. **Publish** (only a person's tap, `publish`): the version waits for a Zvessa admin.
3. **Admin review** (`share.review` block): accept, reject with a reason, take down, restore, reports.
4. **Add** (`share.library` block, or `add_template` as a proposal a person approves):
- `mode: "add"` puts its pages and parts next to what is there. Ids that clash get new ones
(`shop` → `shop-2`) and every reference in the piece follows (`collection`, `#page`,
`level:vip`, `records.add:orders`…). Identical parts and the space's own roles, units, levels and
badges are reused. Built-in kits it uses are added first. Pages that were tabs become tabs.
- `mode: "switch"` (templates): also its tabs, look, words, chat settings and org kinds; nothing is
removed; a kids template makes the space a private kids space.
- One saved version either way, so it can be undone from History.
5. **Take out** (`share.mine` block, `remove_template`): removes what an "add" added (records stay),
using `blueprint.fromLibrary`.
## What is taken out before saving
Keys and tokens (sk-, ghp_, xox, AKIA, JWTs, private keys, long random strings), emails and phone
numbers (replaced with you@example.com / 555-0100), links with a sign-in or a key/signature in the
query, links to this deployment's uploads (Convex storage, R2) and private networks, outside-feed
accounts (`feeds[].sources`), photos in books, connection headers. Connections keep their `auth`
(just a vault key name: whoever adds it saves their own key). A block that can't work without what was
removed (a picture whose link was private) is left out. The person sees the full list before saving,
and admins see it while reviewing. Mini-apps from the mini-app library travel as a reference and are
added from that library again (if still listed). Identity, access and members never travel.
## Ratings and kids
A piece can't be rated younger than its mini-apps. Kids spaces only see and add pieces rated for
everyone, and anything saved from a kids space is rated for everyone.
## AI and MCP tools
`search_templates` (with `mine`), `view_template` (with `source`), `add_template`, `remove_template`,
`save_template` (with `preview`). No tool publishes. The open server `POST /mcp/library` also offers
`search_templates` and `view_template` with no account.
# Feeds and social kits
One engine, many kits. A **feed** is a blueprint setting (`blueprint.feeds`, `convex/kits/social/lib.ts`):
a named place where people publish posts with words, photos and videos, and reply, react,
repost, quote, save and follow. Every behaviour is a setting on the feed, so the same code makes:
| Kit | What it is | Feed settings that make it |
| --- | --- | --- |
| `feed` Posts and threads | Your own X or Threads | short posts (500), up to 4 media, nested replies, reposts, quotes |
| `photos` Photo feed | Your own Instagram | media required, up to 10, captions (2200), flat comments, profile grid, explore |
| `gallery` Gallery | Photos and videos in a grid, #tags as albums | media required, owner posts, others like and comment |
| `stories` Stories | Posts that disappear after a day | `expiresHours: 24`, one photo or video, several reactions |
| `forum` Forum | Topics with long threaded discussions | `titles`, 10000 characters, sorted by latest reply |
| `reviews` Reviews | Star ratings with words and photos | `rating`, flat replies |
| `guestbook` Guestbook | Short notes visitors leave | no media, 300 characters |
| `outside-feeds` Outside feeds | Pull X, Bluesky, Mastodon, Instagram, Threads, YouTube, RSS | `sources` |
| `profiles` Profile and followers | Profile, who to follow, activity | (no feed of its own) |
The AI or a person can change any of it: rename a feed, allow videos, switch replies from
threads to comments, add ❤️😂🔥 reactions, require approval, make posts disappear, add a
source. Every word is a setting too: `blueprint.words["feed."]` (keys in `FEED_WORDS`).
## Feed settings
`id`, `name`, `postRoles`, `replyRoles`, `replies` (off · flat · nested), `media` (off · optional ·
required), `mediaKinds`, `maxMedia`, `maxLength`, `titles`, `rating`, `reactions` (emoji, first is
the main "like"), `reposts`, `quotes`, `expiresHours`, `approval`, `sources`.
## Blocks
`feed.timeline` (layouts list · cards · grid · stories; sort new · active · top; scope all ·
following · person · mine; tag; mediaOnly), `feed.composer`, `feed.profile`, `feed.people`,
`feed.activity`, `feed.search`, `feed.saved`, `feed.sources`. Screens: `/post/` (a post and
its thread), `/u/` (a profile), `/tag/`.
## Data
Tables in `convex/kits/social/tables.ts`: `feedPosts` (replies keep `root`, so a thread loads
with one index; counts are kept on the post), `feedTags`, `feedReactions`, `feedSaves`,
`socialProfiles` (@handle, bio, photo, link, follower counts), `follows`, `feedActivity`,
`feedSourceRuns`. Photos and videos are files in the space's media library (`convex/media.ts`,
folder `posts`); deleting a post deletes the files uploaded for it.
Who may do what uses the space's roles: read with `view`; post and reply with `post` (and the
feed's `postRoles` / `replyRoles`); react with `post` or `play`; edit your own posts; delete
anyone's with `edit`; approve and hide with `manage`. Follows and profiles belong to the person,
across spaces. Posting is limited to 60 posts per 10 minutes per person per space.
## Outside feeds
A source is `{ id, provider, target, key?, every?, paused? }` on a feed. `key` is the **name** of
a key in the key vault, never the key. Every 5 minutes a job pulls sources that are due (default
every 60 minutes, at least 15), adds new posts once (by the provider's id), shows the original
author and link, and backs off when a source keeps failing. The key is only read inside the
server action that calls the provider.
| Provider | Target | Key |
| --- | --- | --- |
| X | `@handle`, or a search like `from:nasa #space` | X API bearer token (paid API tier) |
| Bluesky | `handle.bsky.social` | none |
| Mastodon | `@user@instance.social` | optional |
| Instagram | `me` (the token's account) | Instagram API access token |
| Threads | `me` | Threads API access token |
| YouTube | `@channel` or a channel id | YouTube Data API key |
| RSS / Atom | the feed's https link | none |
Links to private network addresses are refused. Instagram and Threads media links expire after
a while on Meta's side, so old imported posts can lose their picture.
## Feed recipes
Ready settings for the kinds of feed people ask for (`convex/kits/social/recipes.ts`). The AI reads them in its guide and uses one when the person names the kind ("add a clips feed", "a Q&A board for my class"), then changes what they ask. `feedRecipeOps(bp, recipe, { name?, id?, page?, tab? })` gives the changes that add the feed and a page for it, with ids that never clash.
| Recipe | What it makes |
| --- | --- |
| threads | Short posts, threads, reposts, quotes (X / Threads) |
| photos | Photos and videos with captions, cards (Instagram) |
| stories | One photo or video, gone after a day |
| clips | Short videos only, several reactions (TikTok / Reels) |
| updates | Owner-only news with titles and comments (blog, changelog) |
| board | Members post, managers approve first (class or fan wall) |
| moments | Private family or friends photos, warm reactions |
| questions | Titles and threaded answers, most helpful on top (Q&A) |
| forum | Topics, long threads, most active on top |
| links | Links with titles, upvotes and discussion (Reddit) |
| reviews | Star ratings with words and photos |
| guestbook | Short notes, no pictures |
| portfolio | Owner-only grid, #tags as collections |
## What the AI knows about feeds
`convex/kits/social/guide.ts` (FEEDS_GUIDE, part of the guide's kits section): the `/feeds` settings, that everything inside a post comes with the blocks, the feed blocks and the switcher, the `feed.` words, the recipes, how to add a feed and its page, and how a whole social app is put together.
## Taking out a kit
When a kit is taken out, blocks on the pages that stay and show one of its feeds, collections or streams go with it (`removeKitOps`), so a page never points at something that's gone.
# Styles
A **style** decides how everything looks: the dashboard, HQ, every block, the AI chat and
mini-apps. Styles are data (`convex/lib/styles.ts`), never code. **Classic** and **Futuristic**
are the two built-in styles and are written exactly the way anyone's own style is written, so
they are the worked examples. Inspired by chat-app's styles (docs/STYLES.md there), but every
setting is open, not only six colours.
## What a style holds
| Setting | What it is | Options |
| --- | --- | --- |
| `colors` | Colours for every scheme | Core names: `bg surface raised text text2 text3 line grid accent accent2 onAccent success warn danger`. Any other name (`gold`, `win`, `felt`, `wheel-1`) is allowed for blocks, kits and mini-apps. |
| `schemes` | Named schemes, each overriding `colors` | `light`, `dark`, or any name (`night`, `contrast`); up to 8 |
| `type` | Fonts and text | `body`, `display`: system, rounded, serif, mono · `scale` 0.75–1.5 · weights 400–800 · `headingCase` none/upper · `tracking` |
| `shape` | Corners and lines | `radius`, `radiusSmall`, `line` width, `marks` (corner marks on boxes) |
| `space` | Density | compact, regular, roomy (6, 8 or 10 px steps) |
| `surface` | How boxes are drawn | `treatment` fill/outline/glass/gradient · `depth` flat/shadow/glow |
| `backdrop` | The ground behind everything (drawn by `src/components/Scene.tsx`) | plain, grid, dots, gradient · `size` · `beams` (moving light) · the lit scene (`convex/lib/scene.ts`): `lines` (1–4 colours the grid sweeps through), `glow` (0–3 drifting lights), `floor` (a lit floor of lines moving toward you), `streaks` (0–3 light streaks) |
| `motion` | Feel | `speed` off/calm/normal/snappy · `haptics` off/light/full |
| `parts` | Per-part overrides | `pane card button input bubbleMine bubbleTheirs tabbar header`: treatment, depth, radius, bg, text, line |
| `extends` | Start from another style | a style lists only what it changes |
**Colour values** (`convex/lib/color.ts`): `#rrggbb`, `#rrggbbaa`, `white`, `black`,
`transparent`, another colour's name, `mix(a, b, 40%)`, `alpha(a, 20%)`, `contrast(a)`. They nest,
so a style can set three colours and derive the rest. A value that can't resolve falls back to
Classic's colour, so a mistake never breaks a screen.
## Layers
Lowest first; each layer is the same shape, so any layer can change anything:
1. The platform default (every setting has a value)
2. The style, and whatever it extends (`blueprint.look.style`, from the built-ins or `blueprint.styles`)
3. The space's own changes (`blueprint.look.changes`), such as just a new accent
4. A page's look (`page.look`)
5. Later: a person's own look on their device
The scheme is `blueprint.look.scheme`: `system` follows the device, or any scheme the style has.
## Rules
- **Screens never write a colour, size or font.** They read the resolved style through
`src/lib/workspace.tsx` (`surface`, `bodyText`, `headingText`, `gap`, `color`). A screen that
needs something new adds a setting or a colour name here first.
- **Blocks never hold looks in their props.** A different look is a style, a page look or a part override.
- **Checked on the server.** `checkStyles` / `checkLayer` refuse unknown settings, values out of
range and anything that isn't a colour value, so a style can't fake a screen or run code.
- **Mini-apps get the style** as CSS variables (`--bg`, `--accent`, `--gold`, `--radius`…) and as
`window.myaibio.style`, so a game matches every style, including ones made after it.
## Starters
During onboarding people pick Classic or Futuristic (`src/components/Onboarding.tsx`, drawn by `StylePreview.tsx`); each card
is drawn in its own style. HQ → Style switches style and scheme at any time (saved as a version,
so it can be undone). Anything else, such as "a warm paper look with a serif font", is asked of
the AI, which writes a new style that extends one of these.
# Starter templates
A starter template is a ready mix of kits, pages, words, tabs and a style. People pick one in onboarding ("How do you want to start?"), or the owner switches to one later from HQ (the **Starters** block). What a template makes is an ordinary blueprint: every page, tab, word and colour stays a setting that the owner can edit on the page or ask the AI to change. Switching is one saved version, so it can be undone in History.
| | Business starter | Personal starter |
|---|---|---|
| Feels like | The chat app: one chat list, no tab bar | A soft, personal home |
| Style | Messenger (extends Classic; chat-app colours, glass bars, light and dark) | Soft (cream and rose, rounded type, roomy) |
| Navigation | `dashboard.nav: "list"`: the chat list is home; HQ, AI, Calls, Games, Services and Apps are rows in it | Tabs: Home, Chats, Calls, Plans, Photos (HQ and AI in the top bar) |
| Kits | direct-chat, group-chat, channels, support, calls, trivia, quiz-board, survey-showdown, charades, party-pack, game-maker, polls, wheel, tv, coins, leaderboards, vip, verified, appointments, analytics | direct-chat, group-chat, calls, gallery, guestbook, group-plan, polls, analytics |
## Business starter
- **Home is the chat list**, exactly like the chat app: your picture (opens your spaces), the title "Chats", New chat and Search icons, All / Unread / Channels chips, then rows: HQ (accent square with a live line such as "Bloom Studio · 12 members · 48 messages this week"), AI, the first chat, then Calls, Games, Services and Apps (a folder tile of their first four icons and a count), then every other chat. Rows have 54px gradient-initial avatars (square for channels and cases), "You:" previews, unread and @ counts, pins, and hold for Pin / Mute / Mark read. There is no bottom tab bar; the owner can turn one on in HQ → Navigation.
- **Wide screens**: the list on the left (380px) and the open chat, page or AI on the right.
- **The open chat** is the page `chat-screen` (`talk.screen`), made of one `conversation` block with `background: "chatBg"` and `send: "icon"`: the chat app's bar (Back, picture, name, members, call, video and info icons), the chat colour behind the messages, and a round send button. Replies, threads, reactions, edits, photos and read receipts come from the Chats kit settings. Add blocks around the conversation (rules, a pinned card, a poll) to change every chat screen at once.
- **Calls**: call buttons (Call us, Video call, New call) and the recent calls log (missed in red, tap to call back, video button). A team room for the owner.
- **Game Night** (page `games`, the chat app's games lobby from docs/LOBBY.md, made with `gamesLobbyPage`): a header with your coins, a showcase of Trivia Night, Quiz Board and Survey Showdown with a PLAY button, Live now, House games as bright posters (small, medium or large), Quick play (Polls, Wheel, Show on TV, Make a game), Big wins, Your record and the weekly leaderboard. In the chat list its row is a `lobby.row` (a child of the chats block) with a mosaic of the poster art. No slots or casino games.
- **Services** (the chat app's services page): a header card (the space's logo or first letter on a rounded square, its name in small capitals over "How can we help?", a line and Message us / Book), "This week" banners swiped sideways, Services cards with prices and buttons, Support rows, Find us as an icon grid, and Team links only the team sees. Section titles are small capitals over cards.
- **Apps** (the chat app's apps page): sections Games, For members and For the team, each a card of soft-tinted 56px icons, four across, with a short status line under each name ("6 to play", "Vote now") and a LIVE badge; plus your coins.
- **HQ** (managers): Overview (members, active people, messages, page views, plays, forms and bookings over 7/14/30 days with change and sparklines), Quick actions, Waiting for you (proposals), Analytics, four leaderboards (coins this week, most active, top players, all time), Members, Invite, Access, Pages, Library, Styles, Starters, History, Blueprint.
- Customers can call the business: "Call us" rings the owner without the customer seeing anyone's number.
## Personal starter
- **Home**: a header with your picture, a line about you, Message me and Sign my guestbook; a Now card; glossy Highlights tiles; Links with soft icons.
- **Chats** and **Calls** with your people, plus a Hangout room.
- **Plans**: a to-do board (its own `todo` collection), a Journal only you see (its own `journal` collection), and With others (group plans, polls).
- **Photos**: the gallery.
- **HQ**: overview, quick actions, analytics, friends and family, invite, access, pages, starters, history.
## Photo book starter
Your photos made into a book you flip through, in the **Album** style. Tabs: Book (the book fills
itself from the Gallery, a chapter per month), Photos, Albums (every #tag is a book), Chats,
Guestbook; HQ and the AI in the top bar. Details in [PHOTOBOOK.md](https://docs.zvessa.com/more/photobook.html).
## Editing a page
Every page has an **Edit** button for people who manage the space. Edit mode turns the page into its own editor:
- Rename the page in the title.
- Each part sits in a frame with its name, move up / down, settings and more (Duplicate, Remove).
- Headers, tiles and service sections stay live: tap a button, tile or service to edit it, or "+" to add one. Hold to move or remove.
- Settings open as a sheet built from the block's description: text, numbers, switches, lists, icon picker, colour picker, links with page chips, choices, and **Who sees it** (roles).
- **Add to page** offers headings, text, services or links, app tiles, a header, a picture, a button, a card, a number, a list, a poll, a wheel, a call button and a line.
- **Save** saves the whole page as one version (Undo in History). **Cancel** asks before throwing changes away.
## Native on phones
- iOS and Android use the system tab bar through expo-router native tabs (UITabBar, Material bottom navigation) with SF Symbols and Material icons, an unread badge on the chats tab, and the tab bar minimising on scroll (iOS).
- Pages, conversations and the AI are pushed on a native stack: they slide in and swipe back.
- Screens use large titles that collapse into a bar as you scroll, automatic insets on iOS, and action sheets (ActionSheetIOS) and page sheets for menus and forms.
- Haptics follow the style's `motion.haptics` (off, light, full).
- The web on a phone gets the same tabs drawn in the style. Wide screens get a sidebar (or the classic dashboard when a space has three panes or fewer).
- Lists are virtualised (FlatList with small batches); kit renderers load lazily.
## Blocks the templates add
All are described in `convex/lib/templates/blocks.ts`, so the AI, the editor and the docs catalog read the same descriptions.
| Block | What it is |
|---|---|
| `hero` | Page header. `look: "band"` (colour band, round picture) or `"card"` (the chat app's header: rounded logo, `eyebrow` in small capitals over the title, soft glow). Buttons are its children (button, call, tile) |
| `tiles` / `tile` | App-style grid. `size` small or large, `tone` solid / soft / glossy, `frame: "card"`, `heading: "label"`. A tile's `note` is a status line on small tiles; `badge: "Live"` draws a red LIVE pill |
| `links` / `link` | Service or link section. `layout` list, cards, grid, icons (the app grid on a card) or banners (swiped sideways); `heading`, `tone`; each link has title, note, price, tag, action, icon, tint, image |
| `chats` | The chat list (full screen when it is a page's only block). Its children are custom rows for pages, matched by href (a `lobby.row` for `#games`). `search` header / field / off, `filters`, `compose`, `rowsTop` and `rowsUnder` (page ids or "ai") place page rows in the list |
| `conversation` | (Chats kit) One chat with its bar. On the page `talk.screen` names, it is every opened chat. `background`, `send: "icon"`, `calls`, `details`, `header` |
| `hq.navigation` | HQ → Navigation: the bottom tab bar switch and the order of the panes |
| `calls.recent` | Call buttons (children: call) and the recent calls log |
| `hq.overview` | HQ KPIs over 7/14/30 days |
| `hq.starters` | Switch the space to a starter template |
Links (`href`) take `#page-id`, `https://…`, `chat:`, `ai`, `mailto:` and `tel:`. A link to a page that doesn't exist is dimmed for managers and hidden for others, so removing a kit never breaks a page.
## Navigation
`blueprint.dashboard.panes` is the app's pages in order. `blueprint.dashboard.nav` picks how they show:
- `"tabs"` (default): on phones the first five panes are tabs, the rest buttons in the top bar. Each page's `icon` (an icon name from `src/icons/icons.json`) is its tab icon.
- `"list"`: the chat app's way. No tab bar; home is the chat list and the panes are rows in it (placed by the chats block's `rowsTop` and `rowsUnder`). Wide screens keep the list on the left.
The owner switches it in HQ → Navigation, or asks the AI.
## Built only from blocks and settings
A template is plain blueprint data. Every block kind and prop it uses is in the block catalog the AI reads, and every setting (`dashboard.nav`, `talk.screen`) is in the AI guide, so the AI can build the same starter, or a new one, from scratch. `tests/templates.test.ts` checks this for every template.
## Making another template
Add a `TemplateDef` in `convex/lib/templates/` (see `types.ts`) and list it in `TEMPLATES` in `index.ts`. Tests in `tests/templates.test.ts` check every template makes a valid space, its tabs have icons, its links resolve, every kit can still be removed, and switching twice is stable.
## Server functions
- `blueprint.onboard({ name, space: { …, template } })`: makes the space from a template.
- `kits/templates/templates.apply({ template })` (manage): switches an existing space.
- `kits/templates/templates.overview({ days })` (manage): HQ overview numbers.
- `kits/templates/templates.recentCalls()`: the signed-in person's calls in this space.
| | Feeds starter |
|---|---|
| Feels like | A social app (Instagram, Threads, X) |
| Style | Pulse (extends Classic; white by day, true black by night, hot pink and violet, bold type) |
| Tabs (phone) | Home, Explore, New, Activity, Profile |
| In the top bar | Messages, HQ, AI |
| Kits | feed, photos, stories, profiles, gallery, direct-chat, outside-feeds, polls, analytics |
## Feeds starter
Built only from general blocks the AI also uses, so anyone can ask the AI for the same thing (or any part of it).
- **Home**: a stories row, a box to post, then a switcher: **For you** (every feed of the space, newest first) and **Following** (people you follow, with Who to follow).
- **Explore**: search with trending #tags, tiles to Photos, Gallery, From outside and Polls, and a grid of the week's top photos and videos.
- **New**: a switcher of composers: Post, Photo, Story (gone in 24 hours). Your own posts as a grid below.
- **Activity**: a switcher of Activity (likes, replies, mentions, follows) and Saved.
- **Profile**: your profile (photo, @handle, bio, followers, Edit profile), then Posts, Photos and People.
- **Messages**: the chat list (direct and group chats).
- **HQ**: overview, quick actions (Ask the AI, New post, Pull in feeds, Gallery), analytics, people, invite, access, pages, starters, library, styles, history.
- Everything inside a post comes with the feed blocks: composer, avatars, replies and threads, reactions, reposts, quotes, edit and delete, saves, full-screen media. Taking out a kit (say Stories) takes its blocks off these pages too.
## Picker covers (`showcase`)
A template can carry `showcase: { tagline, gradient: [colour, colour], stickers: [emoji…], shots: [{ icon, title, note, tint }] }`. The picker (onboarding and HQ's Starters) draws a bright gradient cover with floating emoji, the tagline and a tilted phone showing the shots. Plain views, no images to download. The emoji bob gently only on the picked card, only while the app is in front and the person allows motion. Without `showcase` the picker shows the plain card. `checkShowcase` keeps it in limits (tagline 60, 6 stickers, 3 shots).
## The switcher block
`switcher` shows one child block at a time under a segmented control: For you / Following, Posts / Photos / People, Activity / Saved, Menu / Reviews. Props: `labels` (one per child; a missing one uses the child's `title`), `look` (segments · tabs · pills), `start`. Only the open part is drawn, so the others' live data isn't loaded until opened. In Edit mode all parts show so each can be changed.
# Style tokens
Every look in Zvessa comes from a **style**: a small record of settings called tokens. Screens and
blocks never pick a colour, font or size of their own; they ask the style. So changing one token
changes every screen at once, and anyone (or their AI) can make a new look without code.
There are three built-in styles, made exactly the way anyone's own style is made:
| Style | id | Feel |
| --- | --- | --- |
| **Classic** | `classic` | Clean surfaces, soft corners, gentle shadows. chat-app's classic look. |
| **Futuristic** | `futuristic` | Deep background, glowing outlines, a grid with moving coloured lines. |
| **Tech** | `tech` | chat-app's futuristic look, value for value: near-black glass, thin lit lines, a quiet grid, Space Grotesk and mono numbers. |
This page lists every token: what it controls and its value in each built-in style. Values are
generated from `convex/lib/styles.ts`, so they match the app.
## The five tokens that change the most
If you only change a few things, change these. Ask the AI ("make the accent green") or set them in
`/look/changes`:
| Token | Example | What you see |
| --- | --- | --- |
| `look.style` | `"tech"` | The whole look at once. |
| `colors.accent` | `"#22c55e"` | Buttons, links, the open chat, glows. |
| `colors.bg` | `"#0b0d12"` | The page behind everything. |
| `shape.radius` | `8` | How round cards and bubbles are. |
| `type.body` | `"grotesk"` | The font for all words. |
```json
{ "look": { "style": "tech", "changes": { "colors": { "accent": "#22c55e" } } } }
```
## How tokens fit together
- **Groups.** Tokens sit in eight groups: `colors`, `type`, `shape`, `space`, `surface`,
`backdrop`, `motion` and `parts`. A style only lists what it changes; everything else comes from
Classic.
- **Layers.** Lowest first: Classic's base → the style (and what it `extends`) → the space's
`look.changes` → a page's own `look` → a person's own look on their device. Each layer has the
same shape, so any of them can change any token.
- **Schemes.** `schemes.dark` and `schemes.light` (or any name, like `night`) hold the colours that
differ between them. `colors` holds the ones that don't.
- **Colour values** can be `#rrggbb`, `#rrggbbaa`, `white`, `black`, `transparent`, another colour's
name (`accent`), `mix(a, b, 30%)` (30% of a, the rest b), `alpha(a, 20%)` or `contrast(a)`.
Formulas nest. Any new colour name you add (say `brand2`) can be used by blocks and mini-apps.
## 1. Colours
`colors` and `schemes.`. The five columns are the built-in styles in their schemes;
"·" means the style doesn't set it (screens fall back as described).
### Ground and panels
| Token | Controls | Classic light | Classic dark | Futuristic dark | Tech dark | Tech light |
| --- | --- | --- | --- | --- | --- | --- |
| `bg` | The page behind everything. | `#f6f7f9` | `#0d0f14` | `#06080c` | `#06080c` | `#edeceb` |
| `surface` | Cards, panes and sheets (Tech: see-through glass). | `#ffffff` | `#171a22` | `rgba(34, 211, 238, 0.04)` | `rgba(13, 19, 27, 0.82)` | `rgba(245, 245, 244, 0.82)` |
| `raised` | Things that sit on a card: menus, their bubbles, popovers. | `#ffffff` | `#1d212b` | `rgba(34, 211, 238, 0.09)` | `#121a24` | `#fffdf8` |
| `panel` | Tech only: the glass colour that surface (82%) and rowFill (55%) are made from. | · | · | · | `#0d131b` | `#f5f5f4` |
| `elev` | Tech only: sheets, menus and toasts that float over everything. | · | · | · | `#0a0f15` | `#fffdf8` |
| `glass` | Tech only: header and tab bar fill (bg at 66%, frosted on the web). | · | · | · | `rgba(6, 8, 12, 0.66)` | `rgba(237, 236, 235, 0.8)` |
| `rowFill` | Tech only: the faint card behind each chat list row. | · | · | · | `rgba(13, 19, 27, 0.55)` | `rgba(245, 245, 244, 0.55)` |
| `grid` | The backdrop grid lines (transparent = no grid). | `rgba(0, 0, 0, 0)` | `rgba(0, 0, 0, 0)` | `rgba(34, 211, 238, 0.07)` | `rgba(120, 200, 255, 0.035)` | `rgba(61, 107, 255, 0.07)` |
### Text
| Token | Controls | Classic light | Classic dark | Futuristic dark | Tech dark | Tech light |
| --- | --- | --- | --- | --- | --- | --- |
| `text` | Main words. | `#0f172a` | `#eef1f7` | `#e6f6ff` | `#e6edf3` | `#1b1d22` |
| `text2` | Second lines: previews, descriptions. | `#545a68` | `#abadb3` | `#a7b3bb` | `#b7c2cf` | `#464b55` |
| `text3` | Quiet words: times, hints, labels. | `#777c87` | `#898b91` | `#7a848a` | `#8c9aab` | `#636a75` |
### Lines
| Token | Controls | Classic light | Classic dark | Futuristic dark | Tech dark | Tech light |
| --- | --- | --- | --- | --- | --- | --- |
| `line` | Every thin line: card edges, dividers, outlined chips. | `rgba(15, 23, 42, 0.1)` | `rgba(255, 255, 255, 0.09)` | `rgba(34, 211, 238, 0.3)` | `rgba(120, 200, 240, 0.14)` | `rgba(59, 83, 145, 0.22)` |
| `lineTint` | Tech only: the hue lines are made from (chat-app's --fx-line). | · | · | · | `#78c8f0` | `#3b5391` |
| `lineStrong` | Tech only: a slightly brighter line (their bubbles). | · | · | · | `rgba(120, 200, 240, 0.2)` | `rgba(59, 83, 145, 0.3)` |
### Accent
| Token | Controls | Classic light | Classic dark | Futuristic dark | Tech dark | Tech light |
| --- | --- | --- | --- | --- | --- | --- |
| `accent` | The space's colour: buttons, links, the open row, glows. | `#3d6bff` | `#6d8dff` | `#22d3ee` | `#6d8dff` | `#3d6bff` |
| `accent2` | A second accent: gradients (Classic's own bubble runs accent → accent2). | `#5c65ff` | `#7979ff` | `#a78bfa` | `#8a76ff` | `#8a76ff` |
| `onAccent` | Text on a filled accent. | `#ffffff` | `#ffffff` | `#111827` | `#ffffff` | `#ffffff` |
| `accentSoft` | Tech only: the faint accent fill of selected things. | · | · | · | `rgba(109, 141, 255, 0.06)` | `rgba(61, 107, 255, 0.07)` |
| `accentLine` | Tech only: the lit accent outline (buttons, your bubbles). | · | · | · | `rgba(109, 141, 255, 0.6)` | `rgba(61, 107, 255, 0.6)` |
### Status
| Token | Controls | Classic light | Classic dark | Futuristic dark | Tech dark | Tech light |
| --- | --- | --- | --- | --- | --- | --- |
| `success` | Done, online, applied. | `#16a34a` | `#16a34a` | `#34d399` | `#4fe3a0` | `#12804f` |
| `warn` | Waiting, needs a look. | `#d97706` | `#d97706` | `#fbbf24` | `#f2a35e` | `#b8620f` |
| `danger` | Errors, failed replies, delete. | `#dc2626` | `#dc2626` | `#f87171` | `#ff6b6b` | `#cc3339` |
| `gold` | Coins, prizes, 1st place. | `#f59e0b` | `#f59e0b` | `#fbbf24` | `#f59e0b` | `#f59e0b` |
| `win` | Wins in games. | `#12a150` | `#12a150` | `#34d399` | `#4fe3a0` | `#4fe3a0` |
### Chat
| Token | Controls | Classic light | Classic dark | Futuristic dark | Tech dark | Tech light |
| --- | --- | --- | --- | --- | --- | --- |
| `bubbleMine` | Tech only: fill of your bubbles (Classic uses an accent gradient). | · | · | · | `rgba(109, 141, 255, 0.05)` | `#e0e5f6` |
| `bubbleTheirs` | Tech only: fill of other people's bubbles (Classic uses raised). | · | · | · | `rgba(255, 255, 255, 0.02)` | `rgba(255, 253, 248, 0.7)` |
| `field` | Tech only: the message box and search fill. | · | · | · | `rgba(255, 255, 255, 0.02)` | `rgba(255, 253, 248, 0.6)` |
| `note` | Tech only: team-only notes. | · | · | · | `#1c1810` | `#fbf2dc` |
| `noteText` | Tech only: words in a note. | · | · | · | `#f2c46e` | `#7a4f00` |
### Media
| Token | Controls | Classic light | Classic dark | Futuristic dark | Tech dark | Tech light |
| --- | --- | --- | --- | --- | --- | --- |
| `media` | Behind photos and videos. | `#000000` | `#000000` | `#000000` | `#000000` | `#000000` |
| `onMedia` | Words over photos and videos. | `#ffffff` | `#ffffff` | `#ffffff` | `#ffffff` | `#ffffff` |
## 2. Text
`type`. Fonts are names, never files: `system`, `rounded`, `serif`, `mono`, and two bundled with
the app, `grotesk` (Space Grotesk) and `jetbrains` (JetBrains Mono). They load only when a style
uses them (about 145 KB together).
| Token | Controls | Classic | Futuristic | Tech |
| --- | --- | --- | --- | --- |
| `type.body` | Font for all words. | `system` | `system` | `grotesk` |
| `type.display` | Font for titles and headings. | `system` | `mono` | `grotesk` |
| `type.mono` | Font for numbers, times, counts and status lines. | `mono` | `mono` | `jetbrains` |
| `type.scale` | Multiplies every text size (0.75 to 1.5). | `1` | `1` | `1` |
| `type.bodyWeight` | Weight of normal words (400 to 800). | `400` | `400` | `400` |
| `type.headingWeight` | Weight of titles. | `700` | `600` | `700` |
| `type.headingCase` | `upper` makes titles capitals. | `none` | `upper` | `none` |
| `type.tracking` | Letter spacing of titles, in px. | `0` | `1.5` | `0` |
**Text roles** (`type.roles`, from the "Layout and text blocks" work): every piece of text is one of
`display`, `title`, `headline`, `subtitle`, `body`, `callout`, `caption`, `label`, `overline`,
`number`, `quote`, `code`. Each role can set `size`, `weight`, `font` (`body`, `display`, `mono` or
a font name), `case`, `tracking`, `leading` and `tone` (a colour). A block names a role, never a
size. Tech's `label` and `overline` are chat-app's small caps line: 10.5 px, mono, uppercase, wide
tracking, in `text3`; its `number` role is mono.
## 3. Shape
`shape`.
| Token | Controls | Classic | Futuristic | Tech |
| --- | --- | --- | --- | --- |
| `shape.radius` | Corners of cards, sheets and bubbles (0 to 40). | `14` | `4` | `18` |
| `shape.radiusSmall` | Corners of buttons, fields and chips. | `10` | `2` | `12` |
| `shape.line` | Width of every line (0 to 4). | `1` | `1` | `1` |
| `shape.marks` | Marks on cards: `true`/`corners` = small L marks on all four corners; `lit` = one lit top-left corner (Tech). | `False` | `True` | `lit` |
## 4. Space
`space`.
| Token | Controls | Classic | Futuristic | Tech |
| --- | --- | --- | --- | --- |
| `space.density` | `compact` (6 px steps), `regular` (8), `roomy` (10). | `regular` | `regular` | `regular` |
## 5. Surfaces, edges and glow
`surface`. How boxes are drawn.
| Token | Controls | Classic | Futuristic | Tech |
| --- | --- | --- | --- | --- |
| `surface.treatment` | `fill` (solid), `outline` (a line, nearly empty inside), `glass` (see-through), `gradient`. | `fill` | `outline` | `glass` |
| `surface.depth` | `flat`, `shadow` (soft drop shadow) or `glow` (a light in the accent). | `shadow` | `glow` | `glow` |
| `surface.edge` | Header bottoms and tab bar tops: `line`, or `lit` = an accent line that fades out at both ends (chat-app's futuristic headers). | `line` | `line` | `lit` |
| `surface.glow` | How strong glows are, 0 to 1. Under 0.5 a soft halo (Futuristic); 0.5 and up a tight neon edge (Tech). | `0.22` | `0.22` | `1` |
## 6. Backdrop
`backdrop`. The ground behind pages and chats. It never moves while you read, stops when the app is in the background, and turns off when the device asks for less motion.
| Token | Controls | Classic | Futuristic | Tech |
| --- | --- | --- | --- | --- |
| `backdrop.kind` | `plain`, `grid`, `dots` or `gradient`. | `"plain"` | `"grid"` | `"grid"` |
| `backdrop.size` | Grid or dot spacing, in px. | `24` | `24` | `30` |
| `backdrop.beams` | A line of light that sweeps down the grid, again and again. | `false` | `true` | `false` |
| `backdrop.lights` | Now and then a short light runs along one grid line, across or down (chat-app's; one about every 20 s). | `false` | `false` | `true` |
| `backdrop.lines` | 1 to 4 colours the grid lines sweep through. | `[]` | `["#22d3ee", "#a78bfa", "#ff5c9a"]` | `[]` |
| `backdrop.glow` | 0 to 3 colours of soft light that drift behind everything. | `[]` | `["rgba(34, 211, 238, 0.5)"]` | `[]` |
| `backdrop.floor` | A lit floor of lines at the bottom that moves toward you. | `false` | `false` | `false` |
| `backdrop.streaks` | 0 to 3 thin light streaks that cross now and then. | `0` | `2` | `0` |
## 7. Motion
`motion`.
| Token | Controls | Classic | Futuristic | Tech |
| --- | --- | --- | --- | --- |
| `motion.speed` | `off`, `calm`, `normal` or `snappy`. | `normal` | `snappy` | `normal` |
| `motion.haptics` | Taps you feel: `off`, `light` or `full`. | `light` | `light` | `light` |
## 8. Parts
`parts.`. One piece of the screen restyled on its own, on top of `surface`. Each part can set
`treatment`, `depth`, `radius`, `bg`, `text` and `line` (colours or formulas).
| Part | What it is | Without a setting |
| --- | --- | --- |
| `pane` | A large area (a side pane). | `surface` |
| `card` | Cards and panels. | `raised` |
| `button` | Main buttons. | filled `accent` |
| `input` | Text fields. | `surface` |
| `bubbleMine` | Your chat bubbles. | Classic: accent → accent2 gradient |
| `bubbleTheirs` | Other people's bubbles. | Classic: `raised` with a faint shadow |
| `tabbar` | The bottom tab bar. | `surface` |
| `header` | Top bars. | `surface` |
| `row` | Chat list rows (and HQ, AI and page rows). | bare (no box) |
| `avatar` | Pictures without a photo. | a gradient in the name's colour |
| `badge` | Unread counts. | filled `accent` |
| `tile`, `chip` | App tiles and small pills (Layout work). | from `surface` |
What the built-in styles set:
**Futuristic**
| Part | treatment | depth | radius | bg | text | line |
| --- | --- | --- | --- | --- | --- | --- |
| `bubbleMine` | `outline` | · | · | `rgba(34, 211, 238, 0.08)` | `#e6f6ff` | `#22d3ee` |
| `button` | `outline` | · | · | `rgba(34, 211, 238, 0.1)` | `#22d3ee` | `#22d3ee` |
**Tech (dark)**
| Part | treatment | depth | radius | bg | text | line |
| --- | --- | --- | --- | --- | --- | --- |
| `card` | `glass` | `flat` | · | `rgba(13, 19, 27, 0.82)` | · | `rgba(120, 200, 240, 0.14)` |
| `pane` | `glass` | `flat` | · | `rgba(13, 19, 27, 0.82)` | · | `rgba(120, 200, 240, 0.14)` |
| `button` | `outline` | `glow` | `12` | `rgba(109, 141, 255, 0.06)` | `#6d8dff` | `rgba(109, 141, 255, 0.6)` |
| `input` | `outline` | `flat` | `12` | `rgba(255, 255, 255, 0.02)` | · | `rgba(109, 141, 255, 0.28)` |
| `bubbleMine` | `outline` | `glow` | · | `rgba(109, 141, 255, 0.05)` | `#e6edf3` | `rgba(109, 141, 255, 0.6)` |
| `bubbleTheirs` | `outline` | `flat` | · | `rgba(255, 255, 255, 0.02)` | `#e6edf3` | `rgba(120, 200, 240, 0.2)` |
| `header` | `glass` | `flat` | · | `rgba(6, 8, 12, 0.66)` | · | `rgba(0, 0, 0, 0)` |
| `tabbar` | `glass` | `flat` | `22` | `rgba(10, 15, 21, 0.9)` | · | `rgba(120, 200, 240, 0.14)` |
| `row` | `outline` | `flat` | `16` | `rgba(13, 19, 27, 0.55)` | · | `rgba(120, 200, 240, 0.08)` |
| `avatar` | `outline` | `glow` | · | · | · | · |
| `badge` | `outline` | `glow` | · | `rgba(109, 141, 255, 0.1)` | `#6d8dff` | `rgba(109, 141, 255, 0.65)` |
Classic sets no parts.
## Examples
**Tech with a green accent.** Everything else stays Tech:
```json
{ "look": { "style": "tech", "changes": { "colors": { "accent": "#22c55e" } } } }
```
**A style of your own, made from Tech.** List only what changes:
```json
{
"id": "midnight",
"name": "Midnight",
"extends": "tech",
"schemes": { "dark": { "bg": "#05060a", "accent": "#f59e0b" } },
"shape": { "radius": 12 },
"backdrop": { "lights": false }
}
```
**Classic, rounder and roomier, in a serif:**
```json
{ "look": { "style": "classic", "changes": { "shape": { "radius": 22 }, "space": { "density": "roomy" }, "type": { "display": "serif" } } } }
```
**A page with its own look** (a games page that glows), in `/pages/N/look`:
```json
{ "schemes": { "dark": { "bg": "#070814" } }, "backdrop": { "kind": "grid", "lines": ["#2dd4bf", "#a78bfa"], "glow": ["alpha(accent, 40%)"], "floor": true } }
```
## For developers
- Read the resolved style through `src/lib/workspace.tsx`: `useWorkspace().theme`, `surface(theme, part)`,
`bodyText`, `headingText`, `monoText`, `captionText`, `glowShadow`, `litEdges`, `fontFamily(font, weight)`.
- Chat pieces read `src/kits/talk/look.ts` (rows, counts, pills, search) and `src/kits/talk/chatLook.tsx`
(bubbles, message box, send button), so every chat in the app matches.
- `Marks` and `EdgeLine` in `src/components/ui.tsx` draw a card's lit corner and a header's lit line.
- Never write a hex value, font or fixed size in a screen; add a token instead, with a value for each built-in style.
# Public links and web pages
A space's pages open in any browser, without the app: a link-in-bio, a portfolio, a business
page, a menu, a guestbook. Nothing is on the web until the owner adds the **Public links** kit,
and then only what a visitor of the space may see.
## Addresses
| Address | When it works |
| --- | --- |
| `/w//` | Always (`` = `WEB_URL`, else the backend's `CONVEX_SITE_URL`) |
| `https://./` | When `WEB_ZONE` is set and the web proxy runs (`targets/web-proxy`) |
| `https:///` | Once its TXT record proves it, through the web proxy |
| `/embed//` | The same page for an iframe on another site |
| `/web/oembed?url=` | oEmbed JSON, so sites embed a page from its link |
The domain isn't decided yet, so every part of it is a backend setting: `WEB_ZONE`, `WEB_URL`,
`WEB_CNAME` (what own domains point at; default `domains.`), `APP_URL` (for "Open in the
app"; default `zvessa://`), `WEB_RESERVED` (more handles nobody may take), `WEB_PROXY_SECRET`,
and `CF_API_TOKEN` + `CF_ZONE_ID` for own domains through Cloudflare for SaaS.
## Settings: `/web`
Everything is a setting in the blueprint, changed like anything else (HQ → Web, the in-app AI, or
an outside AI over MCP with `propose_change`), approved by a person, undone from History.
```json
"web": {
"handle": "ann",
"domains": ["shop.ann.dev"],
"home": "links",
"routes": [
{ "path": "", "page": "links" },
{ "path": "work", "page": "portfolio", "title": "Ann's work", "description": "Portraits and prints", "image": "https://…" }
],
"card": { "title": "Ann's studio", "description": "…", "image": "https://…" },
"data": ["guestbook"],
"index": false,
"embed": "any",
"appLink": true,
"nav": true
}
```
| Setting | What it does | Default |
| --- | --- | --- |
| `handle` | The free address. 3-31 lowercase letters, digits, single dashes. One space per handle; one let go stays with its space for 7 days | none |
| `domains` | Up to 5 own addresses, each proven with a DNS record | none |
| `home` | The page at the address itself | the first page visitors see |
| `routes` | Which pages are on the web and at what path (up to 3 parts) | every page visitors see, at `/` |
| `card` | The share card (Open Graph, Twitter). A route's own title/description/image wins | the space's name and about, the page's first image |
| `data` | Collections whose records may show (`records.list`, `records.count`) | none: records stay in the app |
| `index` | Let search engines list the pages (public spaces only); adds robots.txt and sitemap.xml | false (pages say noindex) |
| `embed` | `"any"`, `"off"` or a list of https origins allowed to frame the pages | `"any"` |
| `appLink` | "Open in the app" on pages | true |
| `nav` | A menu of the pages when there's more than one | true |
Words on web pages are settings in `/words`: `webOpenApp`, `webMore`, `webMadeWith`,
`webNotFound`, `webLang`.
## What's safe, and how
- **Visibility, server side.** Only `public` and `link` spaces are on the web. Private and
friends-only spaces, kids spaces (never), a page or block hidden from visitors (`visibleTo`,
levels, badges), HQ, and a route to a hidden page all answer the same "Nothing here" (404), so
nobody can tell what exists. Pages are drawn from `blueprintFor(bp, "visitor")`, the same cut
the app sends a visitor.
- **Data is opt-in.** Records show only from collections in `/web/data`, and email and phone
fields never show on the web.
- **No code runs.** Pages are plain HTML with no script: `Content-Security-Policy: default-src
'none'; img-src https: data:; style-src 'unsafe-inline'; form-action 'none'`. Every word is
escaped; links are https or mailto only, outside links get `rel="noopener nofollow ugc"`.
Mini-apps, chats, forms and other live blocks stay in the app (the page offers "Open in the app").
- **Clickjacking.** Normal pages send `frame-ancestors 'none'`; only `/embed/…` follows `/web/embed`.
- **Own domains are proven** with a TXT record `_zvessa.` = `zvessa-verify=`
before they serve. One never proven frees up after 3 days. Live domains are re-checked daily.
- **Light.** Pages are a few KB, cached for 60 seconds, styled from the space's own style
(colours, type, corners, light and dark).
## Blocks
| Block | What it is |
| --- | --- |
| `web.share` | The space's (or one page's) web address with Copy and Share; hidden while not on the web |
| `hq.web` | HQ's Web panel: address, handle, own domains with DNS steps and status, pages on the web, search engines, embeds and embed code |
A link-in-bio is a `stack` of `button`s; a portfolio is headings, text, images and cards. Web
pages draw `stack`, `heading`, `text`, `image`, `button` (`#page-id` links become web paths),
`list`, `card`, `stat`, `divider`, `records.list`, `records.count` and `web.share`.
## Tools (in-app AI and MCP)
| Tool | Scope | What it does |
| --- | --- | --- |
| `get_web` | read | On the web or why not, the address, each page's link and embed link, embed code, each domain's status and DNS records |
| `check_handle` | read | Whether a handle is free and allowed |
| `check_web_domain` | read (manage) | Checks an own domain's DNS now |
| `preview_web_page` | read | What a visitor gets at a path (words, or the whole HTML), even before it's live |
Setting the handle, domains, routes, cards and the rest is `propose_change` on `/web`.
## App functions
`api.web.addresses.mine` (HQ panel), `.link({ page? })` (share block), `.available({ handle })`,
`.checkNow({ host })`. HTTP routes are in `convex/web/http.ts`; the page drawing is
`convex/lib/web/render.ts`; the settings and checks are `convex/lib/web/model.ts`.
## Not yet
Forms people fill in on the web without an account (needs spam protection), feeds and photo
books drawn on the web, and per-page analytics. QR codes are done: `web.share` takes `qr: true`
(and a `design`), and the `qr` block with `link: "web"` draws the address as a designed code, on
the page and in the app ([QR.md](https://docs.zvessa.com/more/qr.html)).