# Add a WorldsBay world

The starter uses the official `@worldsbay/api` npm package. Its `/client/runtime/sdk.js` endpoint is a browser bundle of that package, not a separate SDK implementation. Custom bundled games can import directly from `@worldsbay/api`.

## Connect to the public alpha

Open [the builder desk](https://worldsbay.com/connect) after saving an account. Register an HTTPS origin and download the verification JSON. Download the standalone multiplayer starter from that page, extract it, copy the provided settings into `.env`, put `worldsbay.json` next to `server.mjs`, install dependencies, and start the server. Reverse-proxy your own HTTPS hostname to its local port with WebSocket upgrades enabled. Choose **Verify & connect**. The world appears without a central restart.

Each generated `pb-<UUID>` ID is stable. Verification proves control of the HTTPS origin; a pending claim does not block another owner from proving control. Each account may register three alpha worlds. Keys are shown once, stored only as hashes centrally, and belong exclusively on the world server. `POST /api/worlds/:id/disable` revokes a world using the owner's central session. Rotation/re-enabling currently requires an operator; keep the original private configuration safe.

Use a registered world ID for a specific portal, `random` for an available destination, or `group:arcade-club` for a random approved member of a group. The Commons serves as the hub. The client includes a destination picker and a reference-scene preview; a custom third-party scene is not a remotely rendered live preview. Servers refresh the registry every 30 seconds. A downloaded runtime is a versioned snapshot; it does not automatically receive security updates.

## Tags and managed groups

The builder desk at `/connect` lets you assign up to 12 optional tags when registering a world or later under **Edit tags**. Choose suggestions such as racing, social, adventure, roleplay and puzzles, or enter custom tags separated by commas. Tags are normalized to lowercase with spaces replaced by hyphens and appear in the searchable world directory.

Once you have registered a world, use **Create a group** on the same page. Give it a display name and a unique, stable group ID, such as `arcade-club`. The creator owns the group. Under each verified world's **Group** section, choose a group and request to join. Adding your own world to your own group approves it immediately; other worlds need the group owner's approval. **Groups you manage** shows incoming requests with **Accept** and **Deny**, and approved members with **Remove world**. Use **Refresh groups and requests** to fetch new requests. A world owner can cancel a request or leave a group. Each world may have one current group or pending request; tags remain independent.

Only approved, connected registrations participate in managed group portals. Pending, denied, removed and disabled worlds are excluded. Removal applies to new group travel immediately, even if a world's local directory cache has not refreshed. It does not prevent direct visits to that world or disconnect players already there. Group IDs cannot be assigned through registration or metadata edits to bypass approval.

Use **Choose a portal destination** to generate a portal definition for a specific world, any random world, or a random world in a group:

```js
definition.portals = [
  { target: 'world-a', position: { x: 0, y: 0, z: -7.5 } },
  { target: 'group:arcade-club', position: { x: -7, y: 0, z: -4 } },
  { target: 'random', position: { x: 7, y: 0, z: -4 } },
];
```

The shared adapter accepts these same targets in `requestTravel(target)` and heartbeat `portals`. Group travel excludes the source and worlds reported full, then probes up to five randomly selected candidates. If none is available it returns 503 and the explorer stays in the current world; it never falls back to another group. Existing downloaded adapters need an updated starter/runtime to understand group targets.

The browser-session APIs are:

| Action                           | Endpoint                                                                        |
| -------------------------------- | ------------------------------------------------------------------------------- |
| Save optional tags               | `POST /api/worlds/:id/metadata` with `{ "tags": ["racing", "social"] }`         |
| Browse groups                    | `GET /api/groups`                                                               |
| Create a group                   | `POST /api/groups` with `{ "id": "arcade-club", "name": "The Arcade Club" }`    |
| Review owned groups and requests | `GET /api/my-groups`                                                            |
| Join or request membership       | `POST /api/groups/:id/join` with `{ "worldId": "yw-..." }`                      |
| Accept, deny, or remove a world  | `POST /api/groups/:id/worlds/:worldId/approve`, `/deny`, or `/remove` with `{}` |
| Leave or cancel a request        | `POST /api/groups/:id/worlds/:worldId/leave` with `{}`                          |

`GET /api/my-worlds` includes membership status for each owned world. Writes require the saved owner's central browser session and normal origin markers; world server keys cannot perform these actions. Group ownership and membership decisions persist in the central database.

The standalone starter includes the server adapter and browser runtime, so it runs without this checkout. Share your own game URL: visitors choose **Play as guest**, **Create account**, or **Sign in** in the game, and returning players resume their character there. Guests have a visible **Save account** button while playing. WorldsBay supplies the canonical identity and avatar behind the game's API; visiting Home is optional.

## Browser integration boundaries

- `/client/runtime/sdk.js` exports `WorldsBay`, `RoomConnection`, `RequestError`, and `openAccountPage` (plus the legacy `openAccountDialog` alias). It uses ordinary own-origin HTTP and WebSocket APIs with local cookies; it does not import Three.js. `enterSession`, `resumeSession`, `startGuestSession`, `readAccount`, `createAccountContext`, `completeAccount`, `signOut`, `refreshAppearance`, `getDestinations`, `requestTravel`, and `openStore` return promises. `RoomConnection.on` subscribes to typed state, snapshot, appearance, welcome, central-availability and error events and returns an unsubscribe function.
- `/client/runtime/three.js` adds scene/avatar presentation. `startWorld` supplies direct guest entry, hosted signup/signin and guest account saving, automatic remembered-session resume, in-game retry/reconnect, input prediction, remote interpolation, public inspection and optional wardrobe/portal navigation. Its logo opens the world menu. It returns a disposable runtime. `/client/runtime/runtime.css` supplies its UI styles.
- `packages/wire` defines versioned public definitions and bounded messages. `apps/world/server.ts` owns cookie-to-grant storage, central calls, direct guest/resume, ticket entry/store/travel and room admission. `packages/room` owns world-local authority. Renderers never decide ownership.

## Direct entry from any engine

The SDK talks to the game's server, so the browser never needs central-site cookies
or cross-site cookie access. The included adapter authenticates central calls with
the world's private credential. It stores the opaque remembered credential in a
30-day host-only HttpOnly cookie, and stores the short-lived world grant only on
the server. The remembered guest can therefore resume after a world-server restart.
Guest identities are local to the starting browser and world until the player
travels or saves the account. Different unrelated world URLs cannot silently read
each other's cookies or discover the player's central account.

```js
import { WorldsBay, RequestError } from '/client/runtime/sdk.js';
const worldsbay = new WorldsBay();

async function resume() {
  try {
    return await worldsbay.enterSession();
  } catch (error) {
    if (!(error instanceof RequestError) || error.status !== 401) throw error;
    return worldsbay.resumeSession();
  }
}

try {
  await joinGame(await resume());
} catch (error) {
  if (error instanceof RequestError && error.status === 401) showGuestButton();
  else showRetry(error);
}

// Wire this to the player's explicit Play as guest action.
async function playAsGuest() {
  await joinGame(await worldsbay.startGuestSession());
}
```

`joinGame`, `showGuestButton`, and `showRetry` are your game's presentation hooks;
the standalone `startWorld` runtime implements those states already. All three
session methods return the same `WorldSession` contract. Both POST methods accept
empty JSON and use the SDK's browser markers and exact-origin checks. Resume never
creates a player, and repeated guest requests retain the same valid player. If a
remembered credential is invalid, `startGuestSession()` fails instead of replacing
that player's progress. Offer an explicit **Start a new guest** confirmation in
the game, then call `startGuestSession({ newGuest: true })`. Keep network and service
failures in the retry flow. Account saving, wardrobe visits and travel remain
optional actions after play begins.

Before creating a guest, the SDK loads public `/api/config` once to seed the
server's HttpOnly credential and coalesces simultaneous calls from the same SDK
instance. A custom HTTP integration should likewise load configuration before
enabling Play and disable its Play button while entry is pending. Credentials are
never exposed in the configuration response or stored in JavaScript.

## Hosted accounts that return to your game

```js
import { WorldsBay, openAccountPage } from '/client/runtime/sdk.js';
const sdk = new WorldsBay();

async function createAccount() {
  await openAccountPage(sdk, { mode: 'signup' });
}

async function signIn() {
  await openAccountPage(sdk, { mode: 'signin' });
}
```

Wire these to visible entry buttons and an account menu after joining. Before
opening the page, clear held movement and prevent overlapping entry actions. The
browser visits the central account host, then returns automatically to your
game after signup, sign-in, or cancellation. Catch a rejected promise to show
navigation failures. If browser Back restores the game from its page cache, the
promise resolves so you can unlock controls. Account creation upgrades the bound
guest identity, preserving world progress. Signing in selects the saved account's
identity; it does not merge two players. `readAccount()` supplies `kind`, optional `username`, provider and
confirmation status for your UI. `signOut()` clears this world's session and
remembered identity, so return to the entry choices after explicit sign-out.

The SDK submits a ten-minute scoped context from `POST /api/account/context`
to central `/account/start` in a top-level POST form. Allow the exact central
origin in your Content-Security-Policy `form-action`; the included server already
does this. Central binds the flow to its own HttpOnly cookie and serves
`/account?flow=…`. The flow ID alone grants no authority. Passwords, email and
recovery keys are entered only on this central page. Never log or persist the
context or credentials. Existing central sessions offer **Continue as**; normal
form autocomplete attributes let password managers offer saved credentials for
that host. A successful login establishes central's first-party cookie and
returns the browser to `/auth/callback` on your registered origin with a fresh
one-use authorization code and state. The included adapter checks its first-party
browser binding and exchanges the code server-to-server with a private S256 PKCE
verifier, then creates the game session and redirects to `/`. Keep the verifier on
your server. The callback code expires after 60 seconds. The initiating browser
must retain its cookie; restarting the world during sign-in requires starting
sign-in again. Ordinary travel and verified email confirmation use `/enter`.
The old `openAccountDialog` name remains a compatibility alias for this navigation.

Local accounts use a username, password and recovery key. The account page requires
the player to acknowledge saving the key before continuing, and creates the
60-second completion ticket only after that acknowledgement. With email accounts enabled, forms use email and support confirmation resend and password
recovery. Existing username accounts can choose **Use username account** on the
sign-in or recovery form; new signup continues to use email. Pending email signup
can continue with its canonical guest while the
player checks their inbox; confirmation links use the existing central email
flow and return automatically to the originating game. Do not claim an email
provider is active when the deployment uses local accounts.
