Concepts
Auth model
Secret keys, publishable keys, widget session tokens, and embed tokens — which one to use, when, and why.
Hatched exposes four token types. They exist because different parts of your product have different trust boundaries, and mixing them up is the single most common reason integrations get shipped with secret-key leaks.
The four tokens
| Token | Prefix | Where it lives | Can do |
|---|---|---|---|
| Secret API key | hatch_live_*, hatch_test_* | Server (env var) | Everything. Full account access. |
| Publishable key | hatch_pk_* | Browser (safe) | Read buddies/operations, mint widget sessions. |
| Widget session token | wgt_sess_* | Browser | Scoped interactive actions (buy, equip) for one buddy. |
| Embed token | embed_* | Browser | Read-only buddy display. |
Decision tree
Is this code running on the browser?
├── No (Node, edge, server component, route handler)
│ → Secret API key (HATCHED_API_KEY env var)
│
└── Yes
├── Do you need mutation (send event, earn coin)?
│ → Call your own backend route with the secret key.
│ Never put a secret key in the browser bundle.
│
├── Do you need the user to interact with widgets?
│ → Server mints a widget session token,
│ browser renders <div data-hatched-widget="..." data-hatched-token="...">.
│
├── Do you only need to display the buddy?
│ → Server mints an embed token (cheaper, stateless).
│
└── Do you need raw API reads from a SPA/static site?
→ Publishable key in the browser + @hatched/sdk-js with { publishableKey }.Secret API key
import { HatchedClient } from '@hatched/sdk-js';
const hatched = new HatchedClient({
apiKey: process.env.HATCHED_API_KEY!,
});Rules.
- Load from an env variable. Never hard-code.
- Rotate on any suspected leak. Rotation is instant — old key returns 401 immediately.
- The SDK throws at construction if it detects a DOM environment. The
only way to suppress is
allowBrowser: true, intended exclusively for unit tests.
Publishable key
const hatched = new HatchedClient({
publishableKey: 'hatch_pk_xxxxxxxx',
});
const buddy = await hatched.buddies.get(buddyId); // ✅ ok
await hatched.events.send({ ... }); // ❌ PublishableKeyScopeErrorRules.
- Publishable keys are scoped: read-only endpoints (buddies,
operations) plus
widgetSessions.create. Every mutation endpoint returns403 publishable_key_scope. - Safe to commit to a browser bundle, include in
<meta>tags, or expose asNEXT_PUBLIC_*. - Per-key scope is configurable in Dashboard → Developers → API keys → Create publishable key → check the endpoints you want to allow.
Widget session token
The flow for an interactive widget (buddy, marketplace, celebrate):
- Browser asks your backend for a session.
- Backend calls
hatched.widgetSessions.create(...)with a secret key. - Backend returns
{ token, expiresAt }to the browser. - Browser mounts
<div data-hatched-widget="buddy" data-hatched-token="...">. - The widget talks to Hatched directly, signed with the session token.
const session = await hatched.widgetSessions.create({
buddyId: 'bdy_abc',
userId: 'user_42',
scopes: ['buddy:read', 'buddy:interact', 'marketplace:purchase'],
ttlSeconds: 60 * 15,
});Rules.
- Short-lived (minutes, not hours). Re-mint on focus or route change.
- Scoped to one
buddyId. If you switch buddies, re-mint. - Scoped to the exact list of widget scopes you pass. A session minted
without
marketplace:purchasecannot buy items even if the widget tries.
Embed token
Read-only sibling of widget session tokens. Stateless and cheap to mint — pass one per buddy avatar you render on a page.
const embed = await hatched.embedTokens.create({
buddyId: 'bdy_abc',
userId: 'user_42',
ttlSeconds: 60 * 60,
});What lives where
| Layer | Token |
|---|---|
.env / Vercel secrets / GitHub secrets | Secret API key |
NEXT_PUBLIC_HATCHED_PK / HTML <meta> | Publishable key |
Request to your /api/hatched/session endpoint | — returns widget session token |
data-hatched-token attribute | Widget session token or embed token |
What not to do
- ❌ Put a secret key in a
.env.productionfile that gets shipped to the browser via Vite/webpackDefinePlugin. Check your bundler output. - ❌ Use a session token to call the raw API from
fetch— session tokens are only accepted by the widget runtime. - ❌ Reuse a single session token across many users — tokens are user-bound.
- ❌ Assume a publishable key is "read-only enough" to skip scope review — check the scope set before publishing a new one.