Auth & keys

Your secret key (bk_sk_…) stays on your server. Your users render from the browser with short-lived tokens (bk_pt_…) that your server creates, so the key is never exposed.

Let your users render without exposing your API key

Add one route to your server. It uses your secret key to create a token, and the token is the only thing the browser sees.

app/api/bakery3-token/route.ts
import { Bakery3Server } from '@oeave/bakery3/node';

const bakery3 = new Bakery3Server(process.env.BAKERY3_SECRET_KEY!);

export async function POST() {
  return Response.json(
    await bakery3.tokens.create({
      expiresInSeconds: 300,
      maxRenders: 1,
      maxCost: 1,
    })
  );
}
// responds with:
// { token: 'bk_pt_…', expiresAt: 1735689600000, projectId: '…' }

This works from any site, localhost included. Before you deploy, add allowedOrigins with your own site, as in Restrict a token to your site.

Then point the SDK at that route:

const bakery3 = createBakery3({
  token: () => fetch('/api/bakery3-token', { method: 'POST' }).then((r) => r.json()),
});

token is a function, so the SDK can call it again when a token expires. You write nothing for renewal. The function can return the object above or just the token string. If your route answers with an error page or { error }, the SDK says so and points at the route, not at the API.

The route is the one place a stranger could ask for tokens, so treat it like a checkout button: hand a token only to a visitor you would let render (a signed in customer, a session that has a product open), and rate limit it the way you rate limit sign in. Every token it creates spends from your balance up to its maxCost, and allowedOrigins does not stop a script that calls the route directly.

Limit what a token can do

OptionDefaultMeaning
expiresInSeconds300How long the token works. Between 30 and 3600.
maxRenders5The most renders this token can start. Between 1 and 100.
maxCost2The most it can spend across all of them, in USD. 100 at most.
allowedOriginsnoneThe sites allowed to use it. Set this in production.
subjectnoneYour own user or session id, shown in your usage.

A token works in the project of the secret key that created it.

For a single "Render" button, create one token per click with maxRenders: 1 and maxCost set to what that one render may cost. A leaked token is then worth one render at most.

Once a token has started its maxRenders renders, or has expired, it can start nothing new. It can still read and cancel the renders it started, for 24 hours after it expires, so the page that started a render sees it finish. The SDK follows each render with the token that started it.

A batch or a set of pictures started from the browser is added to with the token that opened it, so give that token a maxRenders and a maxCost that cover every picture, and an expiresInSeconds long enough to send them all.

A render that could cost more than the token has left is refused before it starts:

RenderBudgetExceeded: This render could cost up to $0.16, and this browser token allows $0.10 more.

Show a render the page did not start

A browser token reads only the renders it started. A render started from your server, by another visitor, or in a session whose token the page no longer has answers getRender(id) with an error that says so. Read it on your server with the secret key and send the page what it needs:

app/api/renders/[id]/route.ts
import { Bakery3Server } from '@oeave/bakery3/node';

const bakery3 = new Bakery3Server(process.env.BAKERY3_SECRET_KEY!);

export async function GET(_: Request, { params }: { params: Promise<{ id: string }> }) {
  const render = await bakery3.renders.get((await params).id);
  return Response.json({ state: render.state, url: render.result?.url ?? null });
}

Check that the visitor may see that render before you answer, as you would for any other record of yours.

Restrict a token to your site

Without allowedOrigins, anyone who copies a token out of the network tab can use it from anywhere until it expires. With it, the token only works from the sites you list.

allowedOrigins: ['https://shop.example.com', 'https://staging.example.com'];

We check the request's Origin header. A request from any other site fails with OriginNotAllowed. Leaving the list out is fine for local development and wrong in production.

Try it locally without a token route

const bakery3 = createBakery3({ apiKey: 'bk_sk_…' });

This works, and logs a warning in the console that names the key by its first 12 characters, the way the Keys page lists it. Pass either token or apiKey, never both.

Secret keys and browser tokens compared

Secret key bk_sk_…Browser token bk_pt_…
Where it livesYour server onlyThe browser
LifetimeUntil you revoke itMinutes, an hour at most
Start and read rendersYesYes, within its limits
Create and revoke tokensYesNo
List or delete renders and bakesYesNo
Import assets by URLYesNo
Restricted by originNoYes, with allowedOrigins
Capped on spend and render countNoYes

We only store a hash of each key. A lost key cannot be recovered, only replaced. Create and revoke secret keys on the Keys page.

Confirm your email before your first key

We email a link when you sign up. Open it and press Confirm email address, and two things happen: your starting credit lands on the account, and the Keys page lets you create a secret key. Until then the page says so and the console shows a button that sends the link again.

Nothing else waits on it. You can sign in, look around, check a scene for compatibility and get a cost estimate before you confirm.

What needs a secret key

@oeave/bakery3/node only takes a secret key. new Bakery3Server() checks it on its first request, not when it is created, so a build where the variable is not set still succeeds. That first request fails if the key is missing or is a bk_pt_ token. It also warns if it ends up in a browser bundle. Treat that warning as a leaked key and move the import to server-only code.

A route that needs a secret key answers a browser token like this:

403 Unauthorized
This endpoint requires a secret key.

Why: Browser tokens are deliberately incapable of billing and
     administrative operations.
Fix: Call this from your server with your bk_sk_ key, using
     @oeave/bakery3/node.

Revoke a token early

await bakery3.tokens.revoke(token);

The token stops working at once, before its expiry.

Teams

An account on a plan can add other people to its team, by email, from the Team page in the console. Pay-as-you-go accounts have one person.

OwnerMember
See renders, bakes, keys and balanceYesYes
Start renders on the account's creditYesYes
Create secret keys for the accountYesYes
Add credit, change the plan, delete the accountYesNo
Add or remove team membersYesNo

Someone without a Bakery3 account yet joins the team when they sign up with the invited address. Someone on more than one team picks the account with the switcher in the console header.

Removing a member revokes every secret key they created. The API knows nothing about teams: a key belongs to an account, and every request it makes is that account's.