# First user bootstrap

> The complete first-run path — from a published config to a mounted widget — in both the SDK and raw HTTP. The one flow you can't skip steps in.

Source: https://docs.hatched.live/docs/guides/first-user-bootstrap

A widget token is scoped to a **buddy**, and a buddy starts life as an **egg**.
So before you can render anything for `user_42`, you have to walk a short chain:

```text
published config  →  reuse-or-create egg  →  mark ready  →  hatch
   →  poll the hatch operation  →  read & persist buddy_id  →  widget session token  →  mount widget.js
```

You can't shortcut from `user_id` straight to a widget session token —
`POST /widget-sessions` requires an existing `buddy_id`. This page is that
chain, end to end, idempotency and latency included. (For everyday calls once
the buddy exists, see [Getting started](/docs/guides/getting-started); for the
full SDK surface, [SDK quickstart](/docs/guides/sdk-quickstart).)

> **Casing:** the raw HTTP API is `snake_case` (`user_id`, `buddy_id`,
> `ttl_seconds`). The SDK is the only place you write camelCase — it converts on
> the wire. The ` ```http ` blocks below are `snake_case`; the ` ```ts ` blocks
> are SDK calls.

## 0. Publish a config version first

Picking a dashboard preset during onboarding publishes your first config version
automatically — so most integrations never see this. You hit it only if you
built a config from scratch or haven't published yet: `POST /eggs` then returns
`409` with `error.code: "no_published_config"` (SDK: `NoPublishedConfigError`),
and `error.details.publish_url` links straight to the dashboard publish page.
Open the rules editor and hit **Publish** — that freezes your draft into the
snapshot every new buddy gets pinned to.

Three follow-on gotchas:

- **Draft vs published.** Streaks, paths, badges, coin rules, and marketplace
  items only reach widgets once they're in the *published* snapshot. If the
  dashboard shows an active `daily_quizzer` streak but `/widget/streak/daily_quizzer`
  returns `404`, that definition is still on a *draft* config — publish again.
- **Existing buddies stay pinned.** Publishing a new version does not move
  buddies you already created; they keep running on their old (possibly empty)
  snapshot until you migrate them from the dashboard. During integration testing
  it's usually cleanest to publish first, then create a fresh test buddy.
- **Key / environment match.** `hatch_test_*` keys talk to staging, `hatch_live_*`
  to production — and a config published in one environment isn't visible from
  the other. Make sure your key and your published config are in the same place.

## 1. Reuse an existing buddy before creating anything

The cardinal rule: **one buddy per (customer, user) — look it up before you
create.** React Strict Mode, focus re-fetches, hot reloads, and retry logic all
love to call your bootstrap twice; without a guard you'll create a second egg,
then a third, and eventually hit `409` with `error.code: "active_egg_limit"`
(SDK: `ActiveEggLimitError`) — `error.details.active` then lists the eggs you
already have, and `error.details.max` the cap. (`POST /eggs?ensure=true` reuses
one of those instead of failing — see §2.)

**With the SDK:**

```ts
import { HatchedClient } from '@hatched/sdk-js';

const hatched = new HatchedClient({ apiKey: process.env.HATCHED_API_KEY! });

async function ensureBuddyId(userId: string): Promise<string> {
  // 1. Already stored against this app user? Use it.
  const stored = await loadStoredBuddyId(userId); // your DB column / profile field / localStorage
  if (stored) return stored;

  // 2. Hatched already has a buddy for this user? Reuse it.
  const existing = await hatched.buddies.list({ userId, status: 'active' });
  if (existing.data.length > 0) {
    const buddyId = existing.data[0].buddyId;
    await saveStoredBuddyId(userId, buddyId);
    return buddyId;
  }

  // 3. Nothing yet — create one (next section).
  return createAndHatch(userId);
}
```

**With raw HTTP:**

```http
GET /api/v1/buddies?user_id=user_42&status=active
Authorization: Bearer hatch_test_…

# → { "data": [ { "buddy_id": "…", "user_id": "user_42", … } ], "meta": { … } }
# If data is non-empty, store data[0].buddy_id and skip egg creation.
```

## 2. Create the egg, mark it ready, hatch

Only reach this when §1 found nothing. **Guard the create call so it runs at
most once per user** — a module-level in-flight map, a DB unique constraint on
`(app_user_id)`, or a key you generate yourself (`egg:bootstrap:user_42`). Don't
put it anywhere a React effect, focus handler, or hot reload will re-run it.

Pass **`ensure: true`** (raw HTTP: `?ensure=true`): instead of always creating a
new egg, it returns this user's most recent `waiting`/`ready` egg if one already
exists. That makes the call idempotent for the bootstrap path and means a retry
after a crashed first attempt picks the half-finished egg back up instead of
hitting the active-egg cap.

**With the SDK:**

```ts
async function createAndHatch(userId: string): Promise<string> {
  // ensure:true → reuse this user's existing waiting/ready egg if there is one.
  const egg = await hatched.eggs.create({ userId, ensure: true }); // POST /eggs?ensure=true
  if (egg.status === 'waiting') {
    await hatched.eggs.updateStatus(egg.eggId, 'ready');           // PATCH /eggs/:id/status
  }
  const op = await hatched.eggs.hatch(egg.eggId);                  // POST /eggs/:id/hatch → { operationId }

  // Hatch is async — image generation runs 5–45s. Don't block the UI on it.
  const result = await hatched.operations.wait(op.operationId, { intervalMs: 2_000 });
  if (result.status !== 'completed') throw new Error(`hatch ${result.status}`);

  const buddyId = result.result.buddyId;
  await saveStoredBuddyId(userId, buddyId);                        // ← persist immediately
  return buddyId;
}
```

**With raw HTTP:**

```http
POST /api/v1/eggs?ensure=true
{ "user_id": "user_42", "metadata": {} }
# → { "egg_id": "…", "status": "waiting", "buddy_id": null, … }
# (status may already be "ready" if you're reusing an egg — skip the PATCH then.)

PATCH /api/v1/eggs/{egg_id}/status
{ "status": "ready" }

POST /api/v1/eggs/{egg_id}/hatch
# → { "operation_id": "op_…", "status": "pending" }

# Poll every ~2s until status is "completed" (typically 5–45s):
GET /api/v1/operations/{operation_id}
# → { "operation_id": "op_…", "status": "completed", "result": { "buddy_id": "…" }, … }
```

`GET /api/v1/eggs/{egg_id}` (and `GET /api/v1/eggs`) echo `buddy_id` once the egg
reaches `status: "hatched"` — it's `null` before that. Persisting the
`result.buddy_id` from the operation here is still the right move (you have it
sooner, and you avoid an extra round trip).

While the hatch operation is `pending`, show a loading state — a placeholder
egg, a spinner, "your buddy is hatching…". Don't block the first widget render
indefinitely; mount it once you have `buddy_id` and a session token, and let the
widget show its own loading state for the artwork.

## 3. Mint the widget session token

`POST /widget-sessions` needs the `buddy_id` from step 2 (or §1) **and**
`scopes`. Send all the scopes the widgets on that page actually use.

**With the SDK:**

```ts
const session = await hatched.widgetSessions.create({
  buddyId,
  userId: 'user_42',
  scopes: ['read', 'events:track', 'marketplace:browse', 'marketplace:purchase', 'items:equip'],
  ttlSeconds: 3600,
});
// → { token, sessionId, expiresAt, scopes }
```

**With raw HTTP:**

```http
POST /api/v1/widget-sessions
{
  "user_id": "user_42",
  "buddy_id": "uuid-from-operation-result",
  "scopes": ["read", "events:track", "marketplace:browse", "marketplace:purchase", "items:equip"],
  "ttl_seconds": 3600
}
# → { "token": "wgt_…", "session_id": "…", "expires_at": "…", "scopes": [ … ] }
```

This is **not** the embed-token endpoint. `POST /embed-tokens` is the *read-only*
path — it rejects `scopes` and returns `mode: read-only`. Use a session token
(`data-session-token`) when the widgets need to track events, purchase, or
equip; use an embed token (`data-embed-token`) only for purely display mounts.
See [Auth model](/docs/concepts/auth-model#session-token-vs-embed-token).

## 4. Mount the widget

```html
<script
  src="https://cdn.hatched.live/widget.js"
  data-session-token="{{session.token}}"
  defer
></script>

<div data-hatched-mount="buddy"></div>
```

The token is short-lived; re-mint it on each page load (or just before it
expires). The widget reads buddy state from `/widget/state` and renders artwork
when it's ready.

## Persist `buddy_id` — this is not optional

After the first successful hatch, **store `buddy_id` against your app user** and
read it back on every subsequent load:

- **Authenticated apps** — a column or profile-metadata field on your user
  record (`users.hatched_buddy_id`).
- **Anonymous demos** — `localStorage` keyed by your local user id.

On every widget load: stored `buddy_id` → use it; else `GET /buddies?user_id=…`
→ reuse the first active buddy; **only then** create an egg. Never create an egg
on app mount, focus, hot reload, or a failed widget mount.

## Common pitfalls

| Symptom | Cause | Fix |
| --- | --- | --- |
| `property userId should not exist` | Sent camelCase to the raw HTTP API | The raw API is `snake_case` (`user_id`). Use `snake_case`, or use `@hatched/sdk-js` (it converts for you). |
| `buddy_id must be a UUID` | Passed `user_id` (or nothing) where `buddy_id` was expected | Mint the session with the `buddy_id` from the hatch operation's `result`, not the `user_id`. |
| `property scopes should not exist` | Sent `scopes` to `POST /embed-tokens` | That's the read-only endpoint. Use `POST /widget-sessions` for scoped/interactive tokens. |
| `409 no_published_config` (`NoPublishedConfigError`) | No published config yet | Publish a config version in the dashboard — `err.details.publish_url` links there. Check your key's environment matches where you published. |
| `409 active_egg_limit` (`ActiveEggLimitError`) | Bootstrap ran multiple times (Strict Mode, focus, retries) and filled the per-user egg cap | `err.details.active` lists the existing eggs — hatch or cancel one, or retry the create with `?ensure=true` to reuse it. Better: guard egg creation and reuse via stored `buddy_id` → `GET /buddies?user_id=…` before ever calling `POST /eggs`. |
| `/widget/streak/<key>` or `/widget/path/<key>` returns 404 although the dashboard shows the definition | The definition is on a *draft* config, not the published snapshot | Publish again so the snapshot includes it; migrate or recreate the buddy if it was pinned before the publish. |
| Hatch takes 20–45s and the UI hangs | Treating hatch as synchronous | It's an operation — poll `GET /operations/:id` every ~2s, show a loading state, mount the widget once `buddy_id` is available. |

## Related

- [Getting started](/docs/guides/getting-started) — the happy path once the buddy exists.
- [Auth model](/docs/concepts/auth-model) — session token vs embed token vs publishable key.
- [Best practices](/docs/guides/best-practices) — idempotency, multi-tenant ids, and more.
- [Troubleshooting](/docs/guides/troubleshooting) — diagnosing the errors above.
