~6 min readgrounded in apps/worker/src/{upsert,get,list,retrieve,delete}.ts · apps/worker/migrations/0003_tiny_v2.sql · apps/web/app/[slug]/page.tsx · apps/web/app/api/{tiny,control,chat,delete}/route.ts
What a tiny is·
A tiny is a named AI that lives at a URL on your deployment. https://<your-app>/luna renders it, POST /api/chat with x-tiny-name: luna talks to it, and the rest of the universe can find it by meaning. Underneath, it is one row and one JSON document:
- D1
tinys—name(the slug, primary key),user_id,system_prompt,system_knowledge,private,active,created,updated. This table is the only source of truth for existence and ownership. - KV
tinykeyed by the same slug — the full document: everything below, including skills and branding.
name is slugified strictly (slugify(lower, strict)): letters, digits and hyphens; a name that slugifies to nothing is refused, so nothing can ever be created at /. Private tinys are also removed from the vector index, so they cannot surface in universe search.
The record·
Fields a tiny can carry, as POST /upsert accepts them (apps/worker/src/upsert.ts). The agent's create_ai / modify_ai tools and the edit UI all end up here.
| Field | What it is |
|---|---|
name |
the slug and the URL |
systemPrompt |
who the tiny is — becomes the head of the system prompt on every turn |
systemKnowledge |
what the tiny knows — appended after the prompt |
data |
free-form data the tiny may consult |
worker + schema + skills |
an OpenAPI URL; the worker fetches the schema and parses operations into skills, which are mounted as tools whenever this tiny is chatted with or retrieved into another tiny's turn |
mcpServers |
{ "<name>": { url, headers?, disabled? } } — MCP servers mounted at chat time; headers are owner secrets |
hook |
a webhook URL that receives the tiny's messages |
private |
hidden from list, search and other users; only the owner (or a caller with the legacy key) may chat |
hero, logo, theme |
https media URLs and {accent, bg} hex colours for the landing page and browser chrome |
tagline, chips |
landing subtitle (≤ 200 chars) and 1–4 starter suggestions (1–60 chars each) |
intro_vibe |
haptic pattern played when the tiny opens on a phone — one of tap double success warning error heartbeat sos long escalate wave |
voice |
the realtime voice for speech sessions — alloy ash ballad coral echo sage shimmer verse marin cedar, default marin |
Updates are partial-safe: every optional field is preserved when the body omits it (undefined = keep), '' clears explicitly, and an invalid value preserves the existing one rather than silently truncating. That is what lets modify_ai refine just the prompt without wiping a tiny's skills or theme.
Who may read what·
POST /api/tiny {name, key?} in the app and GET /get?name= in the worker return the same document with three levels of exposure:
- Visitor, public tiny — everything, including
systemPromptandsystemKnowledge. Public tinys are open by design: the prompt is the product. Two things are masked:hookbecomes"[configured]"(webhook URLs often embed tokens) andmcpServers[*].headersbecomes"[REDACTED]". - Visitor, private tiny — only the shell:
name,private: true, the branding (hero, logo, theme, tagline, chips — cosmetic, not secret), the vCard and QR URLs. Prompt, knowledge, data, worker and skills come back empty. The landing page renders a branded lock. - Owner (session
sub=user_id, forwarded by the app with the internal key) — everything, unmasked.isAuthorized: truein the response tells the UI to show the edit surfaces.
The endpoint that accepts a private-tiny key is rate-limited at the base per-IP allowance with no widening for signed-in callers — the window is the brute-force budget against someone else's key. See Identity.
Creating, changing, deleting·
| Action | Surface | Rule |
|---|---|---|
| Create | create_ai tool, or the edit UI via POST /api/control → worker POST /upsert |
login required; a name with no tinys row is free to claim, and the insert is ON CONFLICT DO NOTHING so two racing creators cannot both win |
| Update | modify_ai, customize_page, set_price, or /api/control → /upsert |
only if the caller's user id matches the row's owner; sets updated, re-embeds public tinys, deletes private ones from the index |
| Delete | DELETE /api/delete → worker DELETE /tiny |
owner only, permanent — the row, the document and the vector |
Pricing (set_price) puts a tiny behind x402 — see Payments. Page customisation (customize_page) writes the branding fields above.
The landing page·
/<slug> (apps/web/app/[slug]/page.tsx) is the tiny's homepage: hero, logo, tagline, starter chips, the chat, and a themed /og/<slug> card for link previews. The address bar tints to the tiny's theme.bg. /@<github_login> at the same route is a builder profile, not a tiny — public tinys plus forged tools for that user. Unknown slugs render the friendly not-found page, including on malformed percent-encoded scanner traffic.
The tiny's document is embedded in the page as JSON for the client, with < > & and U+2028/2029 escaped so a prompt containing </script> cannot break out of the tag. Prompt, knowledge and data are treated as attacker-authored text throughout the app — they steer the model, never the page's code.
How tinys find each other·
Public, active tinys are embedded (text-embedding-3-small) into a Vectorize index on every upsert. Three surfaces read it:
GET /list— the public directory, served from thetinystable (public + active only, never raw KV keys, which once exposed private names). The agent'slist_tinytool.POST /retrieve— semantic search over public tinys,topK: 9, with a second filter againstprivateafter the vector query so a stale vector can never leak a tiny that went private. The agent'sretrievetool — and its results' skills are mounted as tools for the rest of the turn, so a tiny that finds a weather tiny can call its operations directly.ask_tiny— a nested agent with the other tiny's prompt, knowledge and data answers a message. This is how tinys consult each other; the platform records aconsultedsocial edge each time.
Every chat turn runs a universe search for the current query and puts the related tinys into the system prompt as "related tinys whose skills are already mounted as your tools" — the collective-intelligence part: a tiny is never alone. /universe renders the same directory for people. The agent loop lists the exact tools.