Concepts
Call dabadata from your backend
Your own server code - a Vercel function, a Cloudflare Worker, a Lambda, a box of your own - talks to dabadata over plain HTTPS. There are three situations, and each has its own credential: acting for a signed-in user, acting as your backend, and reacting to a change. Pick the one that fits and you never need the secret key in a request path.
| Your code ... | Example | Sends | Rules |
|---|---|---|---|
| acts for a signed-in user | An AI summary of my note | The user's token (forwarded) and the publishable key | Apply, as that user |
| acts as the backend | A Stripe webhook marks an order paid; a nightly import | A restricted key | Apply, as the key's role |
| reacts to a change | Mail a receipt when an order is paid | Nothing: dabadata calls you (webhooks) | - |
The secret key bypasses every rule and manages the project. Keep it for deploys, migrations and minting user tokens after your own sign-in (below), not for day-to-day backend work.
The examples use fetch and nothing else, so they run anywhere. The JavaScript client works on a server too: pass the credential as token (it is kept in memory where there is no localStorage); subscribe is the one thing a key cannot do (keys never open a live socket).
Acting for a signed-in user
The browser already holds the user's token. Send it along with the request to your function, and forward it to dabadata with the publishable key: every read and write runs under that user's rules, and the trace shows the user, not your server.
A Next.js route on Vercel that summarises one of the user's notes:
// app/api/summary/route.ts
const DABADATA = "https://dabadata.com";
const KEY = process.env.DABADATA_PUBLISHABLE_KEY!; // pk_live_...
export async function POST(req: Request) {
const auth = req.headers.get("authorization"); // "Bearer <the user's token>"
if (!auth) return new Response("sign in first", { status: 401 });
const { noteId } = await req.json();
// Read as the user: their read rule decides (a 404 or 403 is theirs, not yours).
const res = await fetch(`${DABADATA}/v1/db/notes/${encodeURIComponent(noteId)}`, {
headers: { "X-Api-Key": KEY, Authorization: auth },
});
if (!res.ok) return new Response(await res.text(), { status: res.status });
const { row } = await res.json();
const summary = await summarize(row.body); // your model call
// Write back as the user, under their update rule.
await fetch(`${DABADATA}/v1/mutate`, {
method: "POST",
headers: { "X-Api-Key": KEY, Authorization: auth, "Content-Type": "application/json" },
body: JSON.stringify({ table: "notes", op: "update", id: noteId, values: { summary } }),
});
return Response.json({ summary });
}To know who the user is - for a hosted sign-in (end-user accounts) - ask dabadata; tokens are opaque, so there is nothing to check locally:
const me = await fetch("https://dabadata.com/v1/auth/users/me", {
headers: { "X-Api-Key": KEY, Authorization: auth },
});
if (me.status === 401) return new Response("signed out", { status: 401 });
const { user } = await me.json(); // { id, email, name, role, ... }With a provider's JWT (Clerk, Auth0, Firebase, your own) verify it with the provider's library as you do today, and forward it the same way: dabadata checks it against your issuers.
Acting as the backend
Some work has no user behind it: a payment provider's webhook, a cron job, an import. Make a restricted key for it, with a role that may do just that job, and the cleanest way to grant it is an action:
roles: ["admin", "member", "payments"],
actions: {
mark_paid: {
allow: { role: "payments" },
params: { order: { type: "ref", table: "orders" }, paymentId: { type: "text" } },
steps: [{ op: "update", table: "orders", id: "$params.order", values: { status: "paid", payment_id: "$params.paymentId" } }],
},
},# Once, with the secret key (or on the console: Keys, New restricted key):
curl -X POST https://dabadata.com/v1/project/keys/restricted \
-H "Authorization: Bearer $DABADATA_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"stripe-webhook","role":"payments"}'
# -> { "key": "rk_live_...", ... } store it as DABADATA_PAYMENTS_KEYThe Stripe webhook on Vercel then calls the action. If the key leaks, it can mark orders paid and nothing else, and you revoke it alone:
// app/api/stripe/route.ts
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: Request) {
const body = await req.text();
const event = stripe.webhooks.constructEvent(body, req.headers.get("stripe-signature")!, process.env.STRIPE_WEBHOOK_SECRET!);
if (event.type === "checkout.session.completed") {
const s = event.data.object;
const res = await fetch("https://dabadata.com/v1/actions/mark_paid", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.DABADATA_PAYMENTS_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ params: { order: s.client_reference_id, paymentId: s.payment_intent } }),
});
if (!res.ok) return new Response(await res.text(), { status: 500 }); // Stripe retries
}
return new Response(null, { status: 204 });
}A nightly job as a Cloudflare Worker with a Cron Trigger, using its own key (role importer):
export default {
async scheduled(_event: ScheduledEvent, env: { DABADATA_IMPORT_KEY: string }) {
const rates = await (await fetch("https://api.example.com/rates")).json();
const res = await fetch("https://dabadata.com/v1/transact", {
method: "POST",
headers: { Authorization: `Bearer ${env.DABADATA_IMPORT_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
ops: rates.map((r: { code: string; rate: number }) => ({
table: "rates", op: "update", id: r.code, values: { rate: r.rate },
})),
}),
});
if (!res.ok) throw new Error(`dabadata ${res.status}: ${await res.text()}`);
},
};Every call shows up in the trace as the user key:stripe-webhook with role payments, so "who changed this order?" has an answer. A refusal comes back with the rule's explanation, as for anyone else.
Reacting to a change
A serverless function cannot keep a live socket open, and keys cannot open one. Declare a webhook instead: dabadata calls your endpoint after the write commits, signs the request, retries it, and logs every attempt.
webhooks: {
orderPaid: {
on: { table: "orders", ops: ["update"], where: "status = 'paid'", changed: ["status"] },
url: "https://shop.example.com/api/order-paid",
},
},// app/api/order-paid/route.ts
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.DABADATA_WEBHOOK_SECRET!); // the hook's whsec_... secret
export async function POST(req: Request) {
const body = await req.text();
let event: any;
try {
event = wh.verify(body, Object.fromEntries(req.headers));
} catch {
return new Response("bad signature", { status: 400 });
}
if (await alreadyHandled(req.headers.get("webhook-id")!)) return new Response(null, { status: 204 }); // retries reuse the id
await mailReceipt(event.data.row);
return new Response(null, { status: 204 });
}When the handler has to write back - "receipt sent" - give it a restricted key too, as in the previous section.
Signing users in with your own backend
If your backend signs people in its own way (not dabadata's hosted sign-in, not a provider JWT), it mints a dabadata user token for each signed-in user with the secret key, and hands the pair to the browser. This is the one routine job that needs the secret key, and it never leaves your server:
export async function dabadataTokenFor(user: { id: string; name: string }) {
const res = await fetch("https://dabadata.com/v1/users/token", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.DABADATA_SECRET_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ id: user.id, name: user.name, role: "member" }),
});
if (!res.ok) throw new Error(`dabadata: ${res.status} ${await res.text()}`);
return res.json(); // { token, expiresAt, refreshToken, refreshExpiresAt }
}See Identity for refresh tokens and revoking. The console's Keys page has these snippets filled in with your project's values (Snippets, Node backend).
Which credential where
| Credential | Where it lives | What it can do |
|---|---|---|
Publishable key pk_ | Browsers, apps, and next to a forwarded user token | Names the environment; anonymous on its own |
| User token | The signed-in user's browser; forwarded by your functions | What the user's rules allow |
Restricted key rk_ | One backend job's environment variables | What its role's rules and actions allow; no sockets, no project management |
Webhook signing secret whsec_ | The endpoint that receives a hook | Nothing on dabadata: it only proves a request came from it |
Secret key sk_ | Your deploy pipeline and the token-minting route | Everything, bypassing rules |