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.
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
| Option | Default | Meaning |
|---|---|---|
expiresInSeconds | 300 | How long the token works. Between 30 and 3600. |
maxRenders | 5 | The most renders this token can start. Between 1 and 100. |
maxCost | 2 | The most it can spend across all of them, in USD. 100 at most. |
allowedOrigins | none | The sites allowed to use it. Set this in production. |
subject | none | Your 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:
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 lives | Your server only | The browser |
| Lifetime | Until you revoke it | Minutes, an hour at most |
| Start and read renders | Yes | Yes, within its limits |
| Create and revoke tokens | Yes | No |
| List or delete renders and bakes | Yes | No |
| Import assets by URL | Yes | No |
| Restricted by origin | No | Yes, with allowedOrigins |
| Capped on spend and render count | No | Yes |
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.
| Owner | Member | |
|---|---|---|
| See renders, bakes, keys and balance | Yes | Yes |
| Start renders on the account's credit | Yes | Yes |
| Create secret keys for the account | Yes | Yes |
| Add credit, change the plan, delete the account | Yes | No |
| Add or remove team members | Yes | No |
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.
