Recipes
Complete copy-paste examples: a contact form, a todo app, a public JSON feed for a static site, and a backup script.
Working examples you can paste in and adapt. Each one says what it needs.
Set these first:
BAAS_URL=https://www.wrongnotebook.com/v1
BAAS_KEY=pb_live_… # keep this on the server
BAAS_WS=… # from GET /v1/meA contact form that stores submissions
Needs: a collection messages, and a key with records:create only. A
create-only key is the whole trick: if it leaks, nobody can read your submissions.
// app/api/contact/route.js — Next.js App Router
export async function POST(request) {
const form = await request.formData();
const name = String(form.get('name') ?? '').trim();
const email = String(form.get('email') ?? '').trim();
const message = String(form.get('message') ?? '').trim();
if (!name || !email || !message)
return Response.json({ error: 'Please fill in every field.' }, { status: 400 });
const res = await fetch(
`${process.env.BAAS_URL}/workspaces/${process.env.BAAS_WS}/collections/messages/records`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.BAAS_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
data: { name, email, message, receivedAt: new Date().toISOString() },
}),
},
);
if (!res.ok) {
const body = await res.json();
console.error('contact form failed', body.error?.requestId, body.error?.code);
return Response.json({ error: 'Could not send. Please try again.' }, { status: 502 });
}
return Response.json({ ok: true });
}Read the messages in the dashboard, or add a webhook so each one pings you in Slack.
A todo app
Needs: a collection todos, and a key with records:read, records:create,
records:update, records:delete.
const BASE = `${process.env.BAAS_URL}/workspaces/${process.env.BAAS_WS}/collections/todos`;
const headers = {
Authorization: `Bearer ${process.env.BAAS_KEY}`,
'Content-Type': 'application/json',
};
export async function listTodos({ done } = {}) {
const query = new URLSearchParams({ sort: '-createdAt', limit: '100' });
if (done !== undefined) query.set('filter', `data.done == ${done}`);
const res = await fetch(`${BASE}/records?${query}`, { headers });
const body = await res.json();
if (!res.ok) throw new Error(body.error.message);
return body.data.map((row) => ({ id: row.id, version: row.version, ...row.data }));
}
export async function addTodo(title) {
const res = await fetch(`${BASE}/records`, {
method: 'POST',
headers,
body: JSON.stringify({ data: { title, done: false } }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
return res.json();
}
export async function toggleTodo(id, done, version) {
const res = await fetch(`${BASE}/records/${id}`, {
method: 'PATCH',
headers,
// expectedVersion means a stale tab cannot silently overwrite a newer change
body: JSON.stringify({ data: { done }, expectedVersion: version }),
});
if (res.status === 409) throw new Error('This task changed elsewhere. Refresh and retry.');
if (!res.ok) throw new Error((await res.json()).error.message);
return res.json();
}
export async function removeTodo(id) {
const res = await fetch(`${BASE}/records/${id}`, { method: 'DELETE', headers });
if (!res.ok) throw new Error((await res.json()).error.message);
}Deleting is a soft delete, so a mistake can be undone — see Records.
A public JSON feed for a static site
Needs: a collection with visibility Public read. No key at all, so this is safe to call straight from the browser or at build time.
// Next.js page, rebuilt every 10 minutes
export const revalidate = 600;
export default async function Changelog() {
const res = await fetch(
'https://www.wrongnotebook.com/v1/public/collections/COLLECTION_ID/records?sort=-createdAt&limit=20',
);
const { data } = await res.json();
return (
<ul>
{data.map((entry) => (
<li key={entry.id}>
<h2>{entry.data.title}</h2>
<p>{entry.data.summary}</p>
</li>
))}
</ul>
);
}Public collections use the collection's UUID, not its slug. Remember that everything in one is readable by anyone.
Rebuild a static site when content changes
Needs: a webhook pointing at this route, subscribed to
record.created, record.updated and record.deleted.
// app/api/hooks/baas/route.js
import crypto from 'node:crypto';
export async function POST(request) {
const raw = await request.text();
const timestamp = request.headers.get('x-baas-timestamp') ?? '0';
const signature = request.headers.get('x-baas-signature') ?? '';
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300)
return new Response('stale', { status: 400 });
const expected =
'v1=' +
crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(`${timestamp}.${raw}`)
.digest('hex');
if (
signature.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
)
return new Response('bad signature', { status: 401 });
// Reply fast, then do the work.
await fetch(process.env.DEPLOY_HOOK_URL, { method: 'POST' });
return new Response(null, { status: 204 });
}Back everything up on a schedule
Needs: a key with records:read. Run it from cron or a scheduled job.
#!/usr/bin/env bash
set -euo pipefail
DATE=$(date +%F)
mkdir -p "backups/$DATE"
for SLUG in todos messages bookmarks; do
curl -sS --fail \
"$BAAS_URL/workspaces/$BAAS_WS/collections/$SLUG/export?format=ndjson" \
-H "Authorization: Bearer $BAAS_KEY" \
-o "backups/$DATE/$SLUG.ndjson"
echo "saved $SLUG"
doneNDJSON can be imported straight back, so this doubles as a restore point. See import and export.
Move every record through a change
Needs: records:read and records:update. This walks the whole collection by
cursor rather than loading it all into memory.
let cursor = null;
do {
const query = new URLSearchParams({ limit: '100' });
if (cursor) query.set('cursor', cursor);
const page = await (await fetch(`${BASE}/records?${query}`, { headers })).json();
for (const row of page.data) {
if (row.data.priority) continue; // already done
await fetch(`${BASE}/records/${row.id}`, {
method: 'PATCH',
headers,
body: JSON.stringify({ data: { priority: 'medium' } }),
});
}
cursor = page.pagination.nextCursor;
} while (cursor);Try it on a duplicate of the collection first.
Next steps
- Filtering and sorting — narrowing down what you fetch.
- Errors — handling failures properly.
View this page as Markdown — handy for copying into an editor or an AI assistant.