# accounts

Accounts Worker served on the `accounts` subdomain (`accounts.recflare.net`) — a Hono
app for account reads, profile mutations and lookups. Accounts live in the shared
`recflare` D1 database, whose `account` schema and migrations are owned by the `auth`
worker; this worker binds it read/write.

## Routes

| Method | Path                           | Auth | Description                                      |
| ------ | ------------------------------ | ---- | ------------------------------------------------ |
| GET    | `/`                            |      | Health check                                     |
| GET    | `/account/me`                  | ✓    | The caller's own account (private self DTO)      |
| GET    | `/account/search?name=`        |      | Prefix-search accounts by username               |
| GET    | `/account/bulk?id=1&id=2,3`    |      | Look up many accounts by id                      |
| GET    | `/account/:id`                 |      | A single public account                          |
| GET    | `/account/:id/bio`             |      | A player's bio                                   |
| POST   | `/account/create`              |      | Create an account → `{ success, value }`         |
| GET    | `/parentalcontrol/me`          | ✓    | The caller's parental-control flags              |
| GET    | `/accountprivacysettings/:id`  |      | An account's privacy settings                    |
| PUT    | `/account/me/displayname`      | ✓    | Set display name                                 |
| PUT    | `/account/me/username`         | ✓    | Change username (unique + change remaining)      |
| POST   | `/account/me/email`            | ✓    | Set email                                        |
| POST   | `/account/me/phone`            | ✓    | Set phone number                                 |
| PUT    | `/account/me/identityflags`    | ✓    | Set identity flags bitmask                       |
| PUT    | `/account/me/personalpronouns` | ✓    | Set personal pronouns (posted as `pronounFlags`) |
| PUT    | `/account/me/bio`              | ✓    | Set bio                                          |
| PUT    | `/account/me/profileimage`     | ✓    | Set avatar object key                            |
| GET    | `/openapi.json`                |      | Generated OpenAPI 3.1 spec (see below)           |

Auth-gated routes validate the Bearer JWT issued by the `auth` worker and return an
empty-body 401 when it's missing or invalid.

## API documentation

`GET /openapi.json` serves a spec generated from `describeRoute` blocks that sit
alongside each handler, with the schemas in `src/openapi.ts`.

**The spec is descriptive, not enforced.** Nothing validates requests against it — same
rationale as the `auth` worker: this serves a protocol reverse-engineered from the Rec
Room client, the handlers are lenient (form fields are read as
`typeof value === 'string' ? value : ''`), and reads fall back to a synthesized default
account rather than 404. Read a "required" field as _the client always sends it_, not
_the server rejects it if absent_.

A test asserts that every route the worker serves appears in the spec, so adding a
route without documenting it fails rather than silently shipping an incomplete spec.

## Account shapes

Two DTOs, both camelCase:

- **Public** (`toAccountDto`) — returned for any account. Excludes private fields.
- **Self** (`toSelfAccountDto`, the `/account/me` shape) — the public DTO plus
  owner-only `email`, `birthday` and `availableUsernameChanges`.

Two client-deserializer quirks are load-bearing and deliberate:

- `juniorState` / `parentAccountId` are **omitted entirely** when unset — emitting
  `null` makes the client's enum parser throw. `email` / `birthday` aren't enums, so
  they're kept as `null`.
- `GET /accountprivacysettings/:id` never returns a bare `{}` — that fails the client's
  deserializer ("Deserialization returned null"), so the id is echoed back with recent
  history reported visible. Nothing stores per-player privacy yet.

## Missing rows fall back to defaults

Account reads (`/account/me`, `/account/:id`, `/account/bulk`) never 404 on an unknown
id — they synthesize a default account (`defaultAccount`) so every requested id is
present in the response. `bulk` in particular guarantees one entry per requested id.

## Notifications

Profile mutations persist to the account row and then push through the shared
notifications hub (a single global Durable Object owned by the `notify` worker): the
owner receives `SelfAccountUpdate` + `AccountUpdate`, and every connected client
receives an `AccountUpdate` broadcast. Hub failures are logged and swallowed — the
write has already committed, so a hub hiccup must not fail the request.

This matters most for the mutations whose HTTP response carries no account body
(`personalpronouns`, `identityflags`): the client only learns the new value from the
pushed update, and since those fields are in the _public_ DTO, every other client needs
the broadcast too. `email` and `phone` are private, so they persist without a push.

## Bindings

| Binding                      | Type           | Notes                                                        |
| ---------------------------- | -------------- | ------------------------------------------------------------ |
| `DB`                         | D1             | Shared `recflare` database; `account` schema owned by `auth` |
| `JWT_SECRET`                 | Secrets Store  | Shared HS256 signing key (see the `auth` README)             |
| `RECFLARE_NOTIFICATIONS_HUB` | Durable Object | Cross-worker RPC to the `notify` worker's hub                |

This worker has no migrations of its own — the `account` table is created and migrated
by `auth` (`apps/auth/migrations/`).

## Known gaps

- `POST /account/create` parses `platformId` but doesn't yet persist it, and doesn't
  create the dorm Room/SubRoom a new account should get.
