Browser usage (publishable keys)
Use @hatched/sdk-js in the browser with a publishable key — read buddies, mint read-only embed tokens, no server round-trip.
For pages where you only need to read buddy state or mint read-only embed tokens, a publishable key lets you talk to Hatched directly from the browser — no server endpoint of your own, no secret-key leak risk.
Decide: is this the right tool?
| You want to... | Use |
|---|---|
| Read buddy state on a static site | Publishable key with read:buddies |
| Show a read-only buddy widget | Mint an embed token with a publishable key |
| Let a user buy/equip/track in widgets | Widget session minted by your backend |
Send events (lesson_completed, etc.) | Secret key on your server |
| Spend coins, equip items via API | Secret key on your server |
| Manage webhook configs | Secret key on your server |
If you need mutations, keep your secret key on the server. See Auth model.
Create a publishable key
Dashboard → Developers → API keys → Create publishable key.
- Pick a label.
- Confirm the scopes (default:
read:operations+write:embed-tokens). - Copy the
hatch_pk_*value. Unlike secret keys, it's safe to put in client-side config (NEXT_PUBLIC_*,<meta>tags, etc.).
Initialise the client in the browser
import { HatchedClient } from '@hatched/sdk-js';
const hatched = new HatchedClient({
publishableKey: process.env.NEXT_PUBLIC_HATCHED_PK!,
// Set this for staging publishable keys.
baseUrl: process.env.NEXT_PUBLIC_HATCHED_API_BASE_URL,
});The server-only runtime guard is disabled for publishable-key clients, so this works in a React client component, a Vite SPA, a static landing page, anywhere.
Read buddy state
'use client';
import { useEffect, useState } from 'react';
import { HatchedClient, type Buddy } from '@hatched/sdk-js';
const hatched = new HatchedClient({
publishableKey: process.env.NEXT_PUBLIC_HATCHED_PK!,
});
export function BuddyBadge({ buddyId }: { buddyId: string }) {
const [buddy, setBuddy] = useState<Buddy | null>(null);
useEffect(() => {
hatched.buddies.get(buddyId).then(setBuddy).catch(console.error);
}, [buddyId]);
if (!buddy) return null;
return (
<div>
<img src={buddy.thumbUrl ?? undefined} alt={buddy.name} />
<div>
Level {buddy.evolutionStage} · {buddy.coins} coins
</div>
</div>
);
}Mint a read-only embed token in the browser
Publishable keys can mint their own embed tokens — no server endpoint needed:
const embed = await hatched.embedTokens.create({
buddyId: 'bdy_abc',
userId: currentUserId,
ttlSeconds: 900,
});
// Render <div data-hatched-mount="buddy"></div> before calling init.
window.__HATCHED_WIDGET__?.init({ token: embed.token });What fails and what you'll see
Attempting a mutation from a publishable-key client fails before the
network call with PublishableKeyScopeError — no latency cost:
import { PublishableKeyScopeError } from '@hatched/sdk-js';
try {
await hatched.events.send({ ... }); // server-only
} catch (err) {
if (err instanceof PublishableKeyScopeError) {
console.error('move this call to your backend with HATCHED_API_KEY');
}
}If you somehow bypass the SDK and hit the API directly, you'll get
403 publishable_key_scope with the same semantic.
Rotation
Publishable keys rotate like secret keys — Dashboard → Developers → API
keys → revoke. Revocation is instant; browser sessions error on their
next call and you deploy a new NEXT_PUBLIC_HATCHED_PK.
Try it
Head to the Playground — paste a publishable key, hit "Get buddy", see the response inline.
Edge runtimes
Run @hatched/sdk-js on Cloudflare Workers, Vercel Edge, Deno, and Bun — fetch overrides, AbortSignal, and the crypto caveat.
Best practices
Patterns for a Hatched integration that scales — designing the economy, sending events safely, handling webhooks reliably, and staying multi-tenant clean.