# Call dabadata from your backend

Source: https://dabadata.com/docs/backend/ (HTML) - https://dabadata.com/docs/backend/index.md (this page as markdown)

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](https://dabadata.com/docs/backend/index.md#for-a-user) | An AI summary of my note | The user's token (forwarded) and the publishable key | Apply, as that user |
| [acts as the backend](https://dabadata.com/docs/backend/index.md#as-the-backend) | A Stripe webhook marks an order paid; a nightly import | A [restricted key](https://dabadata.com/docs/projects/index.md#restricted) | Apply, as the key's role |
| [reacts to a change](https://dabadata.com/docs/backend/index.md#react) | Mail a receipt when an order is paid | Nothing: dabadata calls you ([webhooks](https://dabadata.com/docs/webhooks/index.md)) | - |

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](https://dabadata.com/docs/backend/index.md#mint)), not for day-to-day backend work.

The examples use `fetch` and nothing else, so they run anywhere. The [JavaScript client](https://dabadata.com/docs/client/index.md) 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:

```ts
// 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](https://dabadata.com/docs/end-user-auth/index.md)) - ask dabadata; tokens are opaque, so there is nothing to check locally:

```ts
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](https://dabadata.com/docs/identity/index.md#providers).

## 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](https://dabadata.com/docs/projects/index.md#restricted) for it, with a role that may do just that job, and the cleanest way to grant it is an action:

```ts
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" } }],
  },
},
```

```bash
# 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_KEY
```

The 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:

```ts
// 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`):

```ts
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](https://dabadata.com/docs/webhooks/index.md) instead: dabadata calls your endpoint after the write commits, signs the request, retries it, and logs every attempt.

```ts
webhooks: {
  orderPaid: {
    on: { table: "orders", ops: ["update"], where: "status = 'paid'", changed: ["status"] },
    url: "https://shop.example.com/api/order-paid",
  },
},
```

```ts
// 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:

```ts
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](https://dabadata.com/docs/identity/index.md) 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 |
