# Architecture and API contract

## Three applications

```mermaid
flowchart LR
  Browser[Player browser] --> C[Central :4000]
  Browser --> A[World A :3001]
  Browser --> B[World B :3002]
  A -->|World A key + limited grant| C
  B -->|World B key + limited grant| C
  C --> DB[(SQLite)]
  C --> Assets[Approved glTF assets]
```

All browser navigation is full-page navigation. World coordinates, authoritative movement, collision bounds, and spawn points are local to each world. Each world hosts one `ws` room; central never receives movement packets. No world sends its inventory to another world. Central is required for entry, travel, and canonical appearance refresh; accepted room sessions keep local gameplay during an outage until their original grants expire.

## Entry protocol

1. The authenticated central launcher issues a random 256-bit ticket with destination, player, expiry, and optional source/referral. It stores only its SHA-256 digest.
2. The browser navigates to the registered destination's `/enter?ticket=...`.
3. The destination calls central `/internal/exchange` with its server credential. One SQLite transaction checks destination/expiry/use and creates a one-hour limited world grant. Concurrent replay has exactly one winner.
4. The world creates a random, host-only, HttpOnly local cookie mapped to the grant in memory. Before returning the cookie, it acknowledges `/internal/accept`; failure discards that local session.
5. Acceptance is idempotent. A ticket with a source world records a completed event and sets the incoming referral. Launcher and store-return tickets have no source and do not add events.
6. The world redirects to `/`, removing the ticket before scene resources load, and reads the appearance centrally using its bound grant.

Acceptance means the destination server completed the entry handshake; it does not guarantee that a player's GPU loaded the scene. If an HTTP response is lost after central accepts a handshake, a travel event can exist even though the browser needs to retry. Inventory does not depend on that event and cannot be lost through travel.

## Browser APIs

All mutation requests are JSON, carry `X-Pocketbeyond: 1`, and require the matching `Origin`. Browser authentication uses same-site, host-only, HttpOnly cookies, never localStorage or URL credentials.

| Central route                | Method     | Input / result                                                                               |
| ---------------------------- | ---------- | -------------------------------------------------------------------------------------------- |
| `/api/bootstrap`             | GET        | Public demo accounts, registered worlds, simulator availability                              |
| `/api/login`                 | POST       | `{ playerId }`; selects a seeded demo identity                                               |
| `/api/logout`                | POST       | Revokes the current browser session                                                          |
| `/api/me`                    | GET        | Versioned appearance, inventory IDs, pinned owned items, current catalogue and return world  |
| `/api/appearance`            | POST       | `{ color }`; one of four supported starter colors                                            |
| `/api/claim`                 | POST       | `{ itemId }`; centrally priced free items only                                               |
| `/api/equip`, `/api/unequip` | POST       | `{ itemId }`; ownership/profile checks; one item per slot                                    |
| `/api/launch`                | POST       | `{ worldId }`; probes registered destination, clears old store context, returns an entry URL |
| `/api/store-context`         | POST       | `{ token }`; requires an existing matching browser identity                                  |
| `/api/return`                | POST       | Fresh ticket to the server-recorded store return destination                                 |
| `/api/orders`                | GET / POST | Own orders / create `{ itemId }` using canonical price                                       |
| `/api/orders/:id/confirm`    | POST       | Development-only idempotent simulated confirmation                                           |
| `/api/activity`              | GET        | Own accepted travels and orders, including referral IDs                                      |

| World route         | Method    | Result                                                                                 |
| ------------------- | --------- | -------------------------------------------------------------------------------------- |
| `/enter`            | GET       | Server-side ticket exchange and redirect                                               |
| `/api/config`       | GET       | Public world metadata and Home URL                                                     |
| `/api/session`      | GET       | World metadata, destinations, Home URL, canonical appearance                           |
| `/api/appearance`   | GET       | Fresh canonical appearance                                                             |
| `/api/travel`       | POST      | `{ worldId }`; health probe then destination ticket request                            |
| `/api/store`        | POST      | Optional `{ selectedItem }`; wardrobe URL through this player's identity-bound context |
| `/api/collection`   | GET       | Centrally listed products, optionally filtered by configured central item IDs          |
| `/api/room-metrics` | GET       | Own-session aggregate room, traffic, simulation and event-loop diagnostics             |
| `/room`             | WebSocket | Exact own Origin and accepted local cookie; query strings rejected                     |

All three servers expose `/health`. Unknown/unowned items, invalid origins, invalid tickets, and expired sessions produce bounded JSON errors. Worlds show a Home recovery screen when entry/appearance cannot be loaded. Failed portal probes leave the source scene available.

## Server APIs

Central internal requests require `Authorization: Bearer <world key>` and `X-World-ID`. Session operations additionally require `X-World-Session: <limited grant>`. Any browser `Origin` is rejected on internal routes. Server keys do not authorize browser account endpoints.

| Route                      | Purpose                                                                                   |
| -------------------------- | ----------------------------------------------------------------------------------------- |
| `POST /internal/exchange`  | Consume `{ token }` for the authenticated destination                                     |
| `POST /internal/accept`    | Idempotently acknowledge accepted local entry                                             |
| `GET /internal/appearance` | Read the grant's player, profile, approved asset references and equipped items            |
| `POST /internal/travel`    | Request `{ worldId }` from the authenticated source                                       |
| `POST /internal/store`     | Optional `{ selectedItem }` hint; five-minute context for the grant's own player/referral |
| `GET /internal/catalogue`  | Read centrally listed products using this world's accepted grant                          |

There is no inventory-write or payment-confirmation internal API.

## Payment and referral semantics

Prices use integer XNO units in this tiny catalogue; real Nano raw amounts/decimal pricing are deferred with the real payment adapter. Order identity, player, item, price, and referral are captured centrally. The partial unique index permits only one pending order for a player/product. Existing pending orders keep their original referral rather than being rewritten by later visits.

Simulated confirmation performs synchronous local adapter verification, entitlement insertion and paid-state update in one SQLite transaction. The entitlement composite primary key also protects against duplicates. Repeated confirmation of a paid order returns the paid order with `delivered: false`. A future network-backed Nano verifier must do asynchronous verification outside the transaction, then re-read/finalize inside a short transaction. There are no wallet keys, balance tables or payouts.

An eligible referral is the accepted incoming transition to the world that opened the store, belongs to the same player, and occurred less than 30 minutes before order creation. Store return reuses that referral without creating travel. The attribution is inspectable in the UI and database; it has no monetary effect.

## Persistence and operational limits

Durable and ephemeral records are separate tables in one SQLite file. Expired authorization rows are pruned once per minute. Foreign keys protect equipped entitlements and referenced worlds/players. Sessions are bounded (entry 60 seconds, store context five minutes, world grant one hour, central browser session 24 hours).

Numbered migrations 1–4 preserve populated v0.1 data, add appearance revisions and immutable asset/purchase/equipment pins, and add creator jobs/attempts/operator sessions. Startup inserts missing seeds without overwriting existing catalogue records. Central is the only running database writer; asset workers return bounded results. `npm run migrate` takes a SQLite backup before upgrade. Backups also need immutable assets, private creator inputs and deployment keys. This is not a multi-writer production database design.

## Rooms and presentation

`packages/wire` defines runtime protocol 1 and public world definition version 1. `packages/sdk` exposes renderer-independent own-world HTTP and typed room events; `packages/three` adds presentation. Inputs contain sequence, normalized bounded axes, facing, jump/run and approved interaction/emote IDs. Unknown fields, claimed positions/results and arbitrary asset URLs are rejected. Message size is limited to 2,048 bytes, rate to 45 messages/second and forward sequence gap to 120. One room allows 16 actors by default.

Movement uses a 20 Hz fixed step with at most five catch-up steps; snapshots run at 10 Hz. The controlling player predicts/reconciles bounded input history; others interpolate bounded snapshots. The server controls the showroom switch and the race's lobby/countdown/running/results/reset phases, ordered checkpoints and times. Local game results have no shared economic authority.

At most one actor per player per world is active. A new connection replaces the old generation; stale cleanup cannot remove it. Same-grant reconnect preserves local pose with reset input acknowledgements; a fresh entry uses the world's spawn. Heartbeats run every ten seconds and idle peers expire after thirty seconds. Browser reconnect has five bounded attempts. Original one-hour grant expiry is checked during participation, not only at admission. Central logout revokes its browser cookie, not all already issued world grants.

Canonical appearance reads are deduplicated per grant and normally refreshed every four seconds outside simulation work. Only newer revisions are broadcast; old HTTP/asset results cannot overwrite new presentation. Public snapshots contain opaque actor IDs, display name, pose, worn item revisions and stable animation IDs/times. They exclude orders and unworn inventory. Scene-ready and room-joined counters are distinct from central acceptance and are not proof of human engagement.

## Creator APIs and revisions

The `/creator` desk is a public shell with a separate operator login. `/operator/api/login` sets a two-hour HttpOnly Strict cookie scoped to `/operator`; subsequent browser mutations also require exact Origin and `X-Pocketbeyond`. The local import CLI uses the separate operator bearer key without a browser Origin. Demo cookies and world keys cannot publish.

| Operator route                                | Method | Purpose                                                              |
| --------------------------------------------- | ------ | -------------------------------------------------------------------- |
| `/operator/api/login`, `/operator/api/logout` | POST   | Separate local operator session                                      |
| `/operator/api/import`                        | POST   | Confined CLI manifest, self-contained GLB and original source bundle |
| `/operator/api/drafts`                        | GET    | Persistent drafts and publication state                              |
| `/operator/api/drafts/:id`                    | GET    | Draft, immutable attempt history and report                          |
| `/operator/api/drafts/:id/metadata`           | POST   | Edit allowed metadata; invalidates prior validation                  |
| `/operator/api/drafts/:id/process`            | POST   | Start bounded child work and return 202                              |
| `/operator/api/drafts/:id/preview`            | GET    | Operator-only validated GLB                                          |
| `/operator/api/drafts/:id/publish`            | POST   | Complete immutable file write, then publish in a short transaction   |
| `/operator/api/products/:id/listing`          | POST   | `{ listed }`; future sales only, owned assets remain available       |

The fixed rig/material/coverage profile and optimizer preservation checks are documented in [RIG-PROFILE.md](RIG-PROFILE.md) and [CREATOR-GUIDE.md](CREATOR-GUIDE.md). Product IDs remain stable while assets are content-addressed. Entitlements, equipment and pending orders pin approved revisions. Ordinary delisting does not replace or delete those revisions. File writes and SQLite are not one atomic transaction; complete unreferenced files may remain after interruption.

Additional worlds are local operator-owned configuration plus a private key, never browser registration. [WORLD-STARTER.md](WORLD-STARTER.md) documents the runnable JavaScript project and build-time module audit.
