# 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.

Source: https://docs.hatched.live/docs/guides/browser-usage

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](/docs/concepts/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

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

```tsx
'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:

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

```ts
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](/playground) — paste a publishable key, hit
"Get buddy", see the response inline.
