Examples
Example 1 — Guestbook
Section titled “Example 1 — Guestbook”Basic
A public message wall. One collection, plain CRUD — the smallest thing that’s actually useful.
Uses: collections · records · filter · sort · public rules
-
Create the collection One base collection with two fields. Public
list_rule/create_rule(null) so anyone can sign the book; everything else stays admin-only.POST /api/v1/collections {"name": "messages", "type": "base","list_rule": null, "create_rule": null,"fields": [{ "name": "name", "type": "text", "options": { "max": 60 } },{ "name": "body", "type": "text", "required": true, "options": { "max": 280 } }]} -
Sign the book
POST /api/v1/messages curl -X POST .../api/v1/messages \-H 'content-type: application/json' \-d '{"name":"Ada","body":"First!"}'→ { "data": { "id": "msg_1", "name": "Ada", "body": "First!", "created": 1751500000 } } -
Read the wall Newest first, 20 per page, optionally filtered.
GET /api/v1/messages GET /api/v1/messages?sort=-created&perPage=20&filter=body ~ 'hello'→ { "data": [ … ], "page": 1, "perPage": 20, "totalItems": 42, "totalPages": 3 }
Example 2 — Blog with authors
Section titled “Example 2 — Blog with authors”Intermediate
A multi-user blog: authenticated authors, posts they own, threaded comments, and live updates.
Uses: auth · 3 collections · relations · ownership rules · expand · file upload · realtime
-
An auth collection for authors
POST /api/v1/collections { "name": "users", "type": "auth","fields": [ { "name": "name", "type": "text" } ] } # email/verified are implicit -
Posts, owned by their author A
relationtousers, afilecover, and rules so anyone can read published posts while only the author edits their own.POST /api/v1/collections {"name": "posts", "type": "base","list_rule": "status = 'published' || author = @request.auth.id","view_rule": "status = 'published' || author = @request.auth.id","create_rule": "@request.auth.id != \"\"","update_rule": "author = @request.auth.id","delete_rule": "author = @request.auth.id","fields": [{ "name": "title", "type": "text", "required": true },{ "name": "body", "type": "editor" },{ "name": "cover", "type": "file", "options": { "mimeTypes": ["image/*"], "maxSize": 5000000 } },{ "name": "author", "type": "relation", "collection": "users" },{ "name": "status", "type": "select", "options": { "values": ["draft","published"] } }]} -
Comments, related to posts
POST /api/v1/collections { "name": "comments", "type": "base","list_rule": null,"create_rule": "@request.auth.id != \"\"","delete_rule": "author = @request.auth.id","fields": [{ "name": "post", "type": "relation", "collection": "posts" },{ "name": "author", "type": "relation", "collection": "users" },{ "name": "body", "type": "text", "required": true }] } -
Register, log in, publish
Author flow POST /api/v1/auth/users/register { "email": "ada@ex.com", "password": "…", "name": "Ada" }POST /api/v1/auth/users/login { "email": "ada@ex.com", "password": "…" }→ tokenPOST /api/v1/posts # Bearer token{ "title": "Hello", "author": "<my-id>", "status": "published" }# upload a cover into the file fieldPOST /api/v1/files/posts/<post-id>/cover -F "file=@cover.jpg" -
Render the feed with everything expanded One request returns published posts with their author and comments attached.
GET /api/v1/posts?expand=author,comments_via_post&sort=-created { "data": [ {"id": "post_1", "title": "Hello", "cover": "a1b2.jpg","expand": {"author": { "id": "usr_9", "name": "Ada" },"comments_via_post": [ { "id": "cmt_1", "body": "Nice!" } ]} } ], "page": 1, "perPage": 30, "totalItems": 1 } -
Live comments Subscribe over WebSocket so new comments appear without polling.
Realtime (browser) const ws = new WebSocket("wss://api.example.com/realtime");ws.onopen = () => {ws.send(JSON.stringify({ type: "auth", token })); // see only what rules allowws.send(JSON.stringify({ type: "subscribe", topics: ["comments.create"] }));};ws.onmessage = e => {const ev = JSON.parse(e.data);if (ev.type === "create") addComment(ev.record);};
Example 3 — AI help desk
Section titled “Example 3 — AI help desk”Advanced
A support desk that validates and embeds tickets on write, notifies your team, streams live updates to a browser dashboard, and answers with both keyword and semantic search.
Uses: 5 collections · CORS · hooks · queue worker · webhooks · full-text + vector search · feature flags · cron · realtime
Data model
Section titled “Data model”| Collection | Type | Purpose |
|---|---|---|
users |
auth | Customers who open tickets |
agents |
auth | Support staff (separate login) |
tickets |
base | Subject, body, status, priority, opener, embedding (vector) |
ticket_messages |
base | The thread — relation to ticket + author |
canned_replies |
base | Reusable macros for agents |
-
Open CORS for your dashboard The browser dashboard lives on another origin, so allow it (this also gates WebSocket/SSE origins).
PATCH /api/v1/admin/settings { "cors.origins": "https://support.acme.com","cors.credentials": "1","app.url": "https://support.acme.com" } -
The tickets collection with a vector field
POST /api/v1/collections (excerpt) { "name": "tickets", "type": "base","list_rule": "opener = @request.auth.id || @request.auth.type = 'admin'","fields": [{ "name": "subject", "type": "text", "required": true, "options": { "searchable": true } },{ "name": "body", "type": "editor", "options": { "searchable": true } },{ "name": "status", "type": "select", "options": { "values": ["open","pending","closed"] } },{ "name": "priority","type": "select", "options": { "values": ["low","normal","high"] } },{ "name": "opener", "type": "relation", "collection": "users" },{ "name": "embedding","type": "vector", "options": { "dimensions": 1536 } }] } -
Enrich on write with a hook A
beforeCreatehook validates, defaults the priority, and computes an embedding by calling your model provider through the SSRF-guardedhelpers.http.Hook: tickets · beforeCreate if (!ctx.record.subject) ctx.helpers.abort("subject is required");ctx.record.status = "open";ctx.record.priority = ctx.record.priority || "normal";// embed subject+body for semantic searchconst res = await ctx.helpers.http.postJson("https://api.openai.com/v1/embeddings",{ model: "text-embedding-3-small", input: ctx.record.subject + "\n" + ctx.record.body },{ authorization: "Bearer " + ctx.helpers.os.env("OPENAI_KEY") });ctx.record.embedding = res.json.data[0].embedding; -
Notify the team after write An
afterCreatehook queues an email and pings Slack — both non-blocking.Hook: tickets · afterCreate await ctx.helpers.enqueue("emails",{ to: "support@acme.com", ticket: ctx.record.id, subject: ctx.record.subject },{ uniqueKey: "new-ticket-" + ctx.record.id });await ctx.helpers.webhooks.dispatch("ticket.opened",{ id: ctx.record.id, subject: ctx.record.subject, priority: ctx.record.priority }); -
A queue worker to send the mail Register a worker on the
emailsqueue (retries + backoff are built in).Worker: queue emails await ctx.helpers.mails.send({to: ctx.payload.to,subject: "New ticket: " + ctx.payload.subject,html: `<p>Ticket <b>${ctx.payload.ticket}</b> was opened.</p>`});Register the Slack webhook once (admin):
POST /admin/webhookswith{ "url":"https://hooks.slack.com/…", "events":["ticket.opened"] }. -
Search — keyword and semantic Full-text search is instant on the
searchablefields. For “find similar tickets,” embed the query and rank by vector distance — gated behind a feature flag so you can dark-launch it.Two ways to search # keywordGET /api/v1/tickets?search=login not working&filter=status='open'# semantic — behind a flag your app checks firstPOST /api/v1/flags/evaluate { "context": { "userId": "agent_2" }, "keys": ["semantic_search"] }GET /api/v1/tickets?nearVector=[…]&nearVectorField=embedding&nearLimit=5 -
Live agent dashboard Agents watch every ticket change over SSE with automatic reconnect + replay.
Realtime dashboard const es = new EventSource("https://api.acme.com/api/v1/realtime");es.addEventListener("connect", e => {const { clientId } = JSON.parse(e.data);fetch("https://api.acme.com/api/v1/realtime", { method: "POST",headers: { "content-type": "application/json" },body: JSON.stringify({ clientId, topics: ["tickets", "ticket_messages.create"] }) });});es.onmessage = e => applyChange(JSON.parse(e.data));// on reconnect the browser sends Last-Event-ID and misses nothing -
Auto-close stale tickets A nightly cron job closes anything untouched for a week.
POST /api/v1/admin/jobs { "name": "auto-close", "cron": "0 3 * * *", "mode": "inline","code": "const cutoff = ctx.scheduledAt - 86400*7; ctx.helpers.db.exec(\"UPDATE cw_tickets SET status='closed' WHERE status='pending' AND updated_at < ?\", cutoff);" }