openapi: 3.1.0
info:
  title: Bigscreen Cloud Admin API
  version: '0'
  summary: Internal admin API on cloud_api — rooms, sessions, media servers, social.
  description: |
    Admin endpoints exposed by [`cloud/cloud_api/cloud_api.ts`](../../cloud/cloud_api/cloud_api.ts)
    under the `/admin/*` prefix. These let staff inspect and mutate the live
    multiplayer state — rooms, occupants, media servers, screens, shared
    activities, social profiles, and notifications. Powered by the
    `cloud-admin-api` bearer key and an admin access token.

    > Distinct from the bigger [`Bigscreen Admin API`](./scalar/) on
    > `apps/admin_api`, which covers the rest of the admin surface
    > (factory pipeline, fabricator, shipping, OAuth, etc.). The two share
    > some component schemas (e.g. `AdminAccount`) but are separate
    > deployments with separate API keys.

    ## Auth at a glance

    Every `/admin/*` request needs **two** headers:

    | Header | Value |
    |--------|-------|
    | `Authorization` | `Bearer <cloud-admin-api key>` |
    | `x-access-token` | A JWT obtained from the public auth API |

    The JWT's account must hold one of the access policies the endpoint
    requires (`Moderator`, `Admin`, `SuperUser`). Policies are listed under
    each endpoint's `x-access-policies` extension.

    ## Spec layout

    The OpenAPI source is split per handler module under
    `docs/openapi/cloud-modules/*.yaml`, joined by `$ref` from this root
    file. The bundled artifact at `docs/openapi/cloud-admin-api.bundled.yaml`
    is what the Scalar renderer reads — regenerate it with
    `npx -y @redocly/cli@latest bundle docs/openapi/cloud-admin-api.yaml -o docs/openapi/cloud-admin-api.bundled.yaml`
    after any edit.
servers:
  - url: http://localhost:3002
    description: Local dev (started by `yarn dev` — port `CLOUD_API_SERVER_PORT` defaults to 3002).
  - url: https://cloud-api.internal.bigscreenvr.com
    description: Production.
security:
  - cloudAdminApiKey: []
    cloudAdminAccessToken: []
tags:
  - name: Stats
    description: |
      Realtime counts, schema enumerations, and other readouts that don't
      fit elsewhere.
  - name: Rooms
    description: |
      Room CRUD over the persistent `StoredRoom` table plus realtime
      inspection / control of `LiveRoom` instances. Drives the Arda rooms
      dashboard.
  - name: Accounts
    description: |
      Live-user functions on a Bigscreen account — listing connections,
      messaging, force-disconnect, and a per-account room listing.
  - name: Network
    description: |
      Cluster operations — media-server registry / activation status, plus
      a SuperUser-only Redis introspection toolkit (prefix-allowlisted,
      audit-logged).
  - name: Screens
    description: |
      Screen CRUD — fetch a screen, change its media session, update its
      settings, or delete it.
  - name: Activities
    description: |
      Shared activities (games, co-watching, etc.) — admin CRUD plus
      per-account listing.
  - name: Social
    description: |
      Social graph admin — profile lookup, history, graph (friend / follow /
      block) inspection and mutation, stats, and notifications.
paths:
  /admin/statistics:
    get:
      tags:
        - Stats
      operationId: getCloudStatistics
      summary: Realtime cluster statistics
      description: Aggregate counts — online users and rooms by visibility — for the live cluster.
      responses:
        '200':
          description: Stats payload.
          content:
            application/json:
              schema:
                type: object
                required:
                  - onlineUserCount
                  - roomCount
                  - publicRoomCount
                  - friendsOnlyRoomCount
                properties:
                  onlineUserCount:
                    type: integer
                    description: Active VR / website connections.
                  roomCount:
                    type: integer
                    description: Total live rooms across all visibilities.
                  publicRoomCount:
                    type: integer
                  friendsOnlyRoomCount:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-access-policies:
        - Moderator
  /admin/schemas:
    get:
      tags:
        - Stats
      operationId: getCloudSchemas
      summary: Cloud schema enumerations
      description: |
        Returns the room policy enums (`MicrophonePolicy`, `MovementSoundsPolicy`,
        `DrawingVisibility`, etc.) plus `RoomType`, `RoomVisibility`, and
        `RoomInstancePolicy`. Used by Arda to populate dropdowns in the room admin
        panels.
      responses:
        '200':
          description: Schema enumeration payload.
          content:
            application/json:
              schema:
                type: object
                description: Each top-level key is an enum name; the value is the enum's member object.
                additionalProperties:
                  type: object
                  additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-access-policies:
        - Moderator
  /admin/room:
    post:
      tags:
        - Rooms
      operationId: adminCreateRoom
      summary: Create an admin-controlled room (broken stub)
      description: |
        **Broken stub** — the handler is empty (`adminCreateRoom` in
        `cloud/src/CloudAdminApi.ts` never calls `res.json` or
        `res.send`), so requests **hang until the client times out**.
        Kept registered for parity with the rest of the room CRUD;
        expect this to land later.
      deprecated: true
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '504':
          description: |
            The handler never responds — gateway timeout (or the
            client's own timeout) is the most likely outcome. No
            body shape is documented because no body is sent.
      x-access-policies:
        - Moderator
  /admin/room/{roomId}:
    parameters:
      - in: path
        name: roomId
        required: true
        schema:
          type: string
        description: External room id (e.g. `room:abc1234567`).
    get:
      tags:
        - Rooms
      operationId: adminGetRoom
      summary: Fetch a room (combined live + stored view)
      description: |
        Returns the combined live + stored view of a room. For lobby / TV rooms
        (which aren't database-backed), the live snapshot's stored projection is
        used. Includes the owner's `AdminAccount` and recent history when the
        room is database-backed.
      responses:
        '200':
          description: Combined room view.
          content:
            application/json:
              schema:
                type: object
                properties:
                  liveRoom:
                    $ref: '#/components/schemas/LiveRoom'
                  roomInstances:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                  storedRoom:
                    $ref: '#/components/schemas/StoredRoom'
                  ownerBigscreenAccount:
                    $ref: '#/components/schemas/AdminAccount'
                  history:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No room with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
    put:
      tags:
        - Rooms
      operationId: adminUpdateRoom
      summary: Update a room (stored + live)
      description: |
        Moderator update — applies to both the stored row and the live
        Redis snapshot. Most fields on `StoredRoom` are accepted; the
        handler reconciles the live state to match. Returns
        `StoredRoom.getPublicData()` (a subset of the full
        `StoredRoom` schema).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
                visibility:
                  type: string
                category:
                  type: string
                publicTags:
                  type: array
                  items:
                    type: string
                backgroundImageUrl:
                  type: string
                  format: uri
                images:
                  type: array
                  items:
                    type: string
                    format: uri
                environmentId:
                  type: string
                maxUsersPerInstance:
                  type: integer
                instancePolicy:
                  type: string
                microphonePolicy:
                  type: string
                movementSoundsPolicy:
                  type: string
                drawingVisibility:
                  type: string
                avatarNameVisibility:
                  type: string
                poseVisibility:
                  type: string
                toySpawnability:
                  type: string
                preferredUserType:
                  type: string
              additionalProperties: true
      responses:
        '200':
          description: Updated room — `StoredRoom.getPublicData()` projection.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StoredRoom'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No room with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
    delete:
      tags:
        - Rooms
      operationId: adminDeleteRoom
      summary: Delete a room (or hide it from listings)
      description: |
        Removes the room from the public listings and tears down the
        live snapshot. **Returns plain text `OK`** — not JSON.
      responses:
        '200':
          description: Room deleted. Body is the literal string `OK`.
          content:
            text/plain:
              schema:
                type: string
                example: OK
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No room with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
  /admin/room/{roomId}/live:
    parameters:
      - in: path
        name: roomId
        required: true
        schema:
          type: string
    get:
      tags:
        - Rooms
      operationId: adminGetLiveRoom
      summary: Live (Redis-backed) room snapshot
      description: |
        Returns just the live Redis snapshot via `getPublicData()` — sanitized
        for non-admin display (no per-session detail). Use the parent
        `GET /admin/room/{roomId}` for the moderator-grade view with sessions.
      responses:
        '200':
          description: Live room (public projection).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LiveRoom'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No live room with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
  /admin/room/{roomId}/history:
    parameters:
      - in: path
        name: roomId
        required: true
        schema:
          type: string
    get:
      tags:
        - Rooms
      operationId: getRoomHistory
      summary: Room event history (not implemented)
      description: |
        **Not implemented.** The handler currently throws — the route is
        registered to reserve the path while the underlying audit store is
        being built. Calls return a 500 today.
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          description: Always — handler is a stub.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
  /admin/room/{roomId}/users/{userSessionId}/kick:
    parameters:
      - in: path
        name: roomId
        required: true
        schema:
          type: string
      - in: path
        name: userSessionId
        required: true
        schema:
          type: string
    get:
      tags:
        - Rooms
      operationId: adminKickUser
      summary: Kick a user from a room
      description: |
        Forcibly removes the user from whatever room they're in. The
        user is blocked from re-entering until they restart their
        session. Compare to `/eject`, which is recoverable. **Returns
        plain text `OK`** — not JSON.
      responses:
        '200':
          description: Kick acknowledged. Body is the literal string `OK`.
          content:
            text/plain:
              schema:
                type: string
                example: OK
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No matching session in this room.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
  /admin/room/{roomId}/users/{userSessionId}/eject:
    parameters:
      - in: path
        name: roomId
        required: true
        schema:
          type: string
      - in: path
        name: userSessionId
        required: true
        schema:
          type: string
    get:
      tags:
        - Rooms
      operationId: adminEjectUser
      summary: Eject a user from a room (recoverable)
      description: |
        Removes the user from a room but does not block re-entry. The
        user can rejoin at will. Use for misbehaving users where a kick
        is too heavy-handed. **Returns plain text `OK`** — not JSON.
      responses:
        '200':
          description: Eject acknowledged. Body is the literal string `OK`.
          content:
            text/plain:
              schema:
                type: string
                example: OK
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No matching session in this room.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
  /admin/rooms/live:
    get:
      tags:
        - Rooms
      operationId: adminGetLiveRooms
      summary: List live rooms (filtered)
      description: |
        Lists currently-live rooms. With no filters, returns every public live
        room. With `roomType=TV` or `roomType=Lobby`, returns the special
        Bigscreen-managed rooms. With `roomType=Cinema`, returns live cinema
        rooms via the stored-room → live-room hydration path.
      parameters:
        - in: query
          name: roomType
          schema:
            type: string
            enum:
              - TV
              - Lobby
              - Cinema
          description: Filter by room type. Other types return 404 by design.
        - in: query
          name: ownerBigscreenAccountId
          schema:
            type: string
      responses:
        '200':
          description: Array of live rooms (public projection).
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/LiveRoom'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Returned for `roomType` outside the supported set.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Invalid query params.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
  /admin/rooms/environments:
    get:
      tags:
        - Rooms
      operationId: adminGetEnvironments
      summary: Environment catalog
      description: |
        Returns the catalog of room environments (scenes the room
        loads on first join). The handler returns
        `CloudDatabase.getEnvironments({})` directly — the response
        shape matches whatever that function returns; treat it as
        opaque on the client.
      responses:
        '200':
          description: Environments catalog (raw output of `getEnvironments`).
          content:
            application/json:
              schema:
                description: Shape per `CloudDatabase.getEnvironments` — opaque to clients.
                additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-access-policies:
        - Moderator
  /admin/rooms/all:
    get:
      tags:
        - Rooms
      operationId: adminGetRooms
      summary: List stored rooms (filtered)
      description: |
        Lists rows from the persistent stored-room table. Public rooms only —
        does not surface private rooms. Filters by `roomType` and / or
        `ownerBigscreenAccountId`.
      parameters:
        - in: query
          name: roomType
          schema:
            type: string
        - in: query
          name: ownerBigscreenAccountId
          schema:
            type: string
      responses:
        '200':
          description: |
            Stored rooms — raw output of `CloudDatabase.getStoredRooms`.
            The wrapper shape (whether `items` / `count` /
            `totalCount` are present) is determined by that
            function; treat as opaque on the client.
          content:
            application/json:
              schema:
                description: Shape per `CloudDatabase.getStoredRooms` — opaque to clients.
                additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          description: Invalid query params.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
  /admin/account/{bigscreenAccountId}/message:
    parameters:
      - in: path
        name: bigscreenAccountId
        required: true
        schema:
          type: string
    post:
      tags:
        - Accounts
      operationId: adminMessageUser
      summary: Send an admin message to a user
      description: |
        Sends a message that surfaces in the user's VR client (or
        website connection) — used for warnings, system notifications,
        or moderator-to-user contact. **Returns plain text `OK`** —
        not JSON.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - message
              properties:
                message:
                  type: string
                severity:
                  type: string
                  enum:
                    - info
                    - warning
                    - urgent
                persist:
                  type: boolean
                  description: When true, the message is also queued for delivery if the user is offline.
      responses:
        '200':
          description: Message dispatched. Body is the literal string `OK`.
          content:
            text/plain:
              schema:
                type: string
                example: OK
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Account has no live connections (and `persist` is false).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
  /admin/account/{bigscreenAccountId}/connections:
    parameters:
      - in: path
        name: bigscreenAccountId
        required: true
        schema:
          type: string
    get:
      tags:
        - Accounts
      operationId: getAccountConnections
      summary: Live connections for an account
      description: |
        Lists active VR / website / cloud connections currently attributed to the
        account. For each connection, includes the room id the user is in
        (rooms with private visibility are surfaced as the literal `"hidden"`).
      responses:
        '200':
          description: Active connections.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  required:
                    - userSessionId
                    - data
                  properties:
                    userSessionId:
                      type: string
                    currentRoomId:
                      type:
                        - string
                        - 'null'
                      description: The room id the user is currently in, or `"hidden"` for private rooms, or null.
                    data:
                      type: object
                      description: Public projection of the user session (`BigscreenUser.toPublicObject()`).
                      additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-access-policies:
        - Moderator
  /admin/account/{bigscreenAccountId}/rooms:
    parameters:
      - in: path
        name: bigscreenAccountId
        required: true
        schema:
          type: string
    get:
      tags:
        - Accounts
      operationId: getRoomsForAccount
      summary: Rooms an account is in or owns (stub — always empty)
      description: |
        **Stub** — `getRoomsForAccount` in
        `cloud/src/CloudAdminApi.ts` returns `[]` unconditionally.
        Endpoint is registered for parity with the rest of the room
        CRUD; expect this to come back online later.
      deprecated: true
      responses:
        '200':
          description: Always returns an empty array.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/LiveRoom'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-access-policies:
        - Moderator
  /admin/account/{bigscreenAccountId}/disconnect:
    parameters:
      - in: path
        name: bigscreenAccountId
        required: true
        schema:
          type: string
    get:
      tags:
        - Accounts
      operationId: disconnectAccount
      summary: Force-disconnect all of an account's sessions
      description: |
        Severs every active VR / website connection on the account. Used as
        part of banning flows. Returns the literal text `"OK"` on success.
      responses:
        '200':
          description: '"OK".'
          content:
            text/plain:
              schema:
                type: string
                example: OK
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: User has no live connections.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
  /admin/network/media-servers:
    get:
      tags:
        - Network
      operationId: getMediaServers
      summary: List media servers
      description: Returns every registered media-server instance, with its current activation status, load, and room list.
      responses:
        '200':
          description: Array of media-server records.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/MediaServer'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-access-policies:
        - Moderator
  /admin/network/media-server/{mediaServerId}:
    parameters:
      - in: path
        name: mediaServerId
        required: true
        schema:
          type: string
    get:
      tags:
        - Network
      operationId: getMediaServer
      summary: Fetch a media server
      description: Returns a specific media-server record by id.
      responses:
        '200':
          description: Media server record.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MediaServer'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No media server with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Admin
    put:
      tags:
        - Network
      operationId: updateMediaServer
      summary: Update activation status
      description: |
        Mutates the media server's `activationStatus`. The handler only allows
        this single field — anything else in the body is ignored. Used to take
        servers out of rotation for maintenance or roll them in after registration.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - activationStatus
              properties:
                activationStatus:
                  type: string
                  description: Must match a `MediaServerActivationStatus` member (`Active`, `Disabled`, etc.).
      responses:
        '200':
          description: Updated media server.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MediaServer'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No media server with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: '`activationStatus` is not in the enum.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Admin
  /admin/network/redis:
    post:
      tags:
        - Network
      operationId: searchRedis
      summary: Inspect Redis (prefix-allowlisted)
      description: |
        SuperUser-only Redis introspection. Either supply a `pattern` (calls
        `SCAN MATCH`) or a `key` (reads the value, dispatching on its type
        — hash → HGETALL, set → SMEMBERS, string → GET).

        Both `pattern` and `key` must start with one of an allowlisted set of
        prefixes — namespaces that hold operational state are exposed,
        namespaces that can carry PII or secrets are deliberately omitted.

        Every call is audit-logged to the Admin Discord channel.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                pattern:
                  type: string
                  description: SCAN-style pattern. Mutually exclusive with `key`.
                key:
                  type: string
                  description: Specific key to read. Mutually exclusive with `pattern`.
      responses:
        '200':
          description: |
            Pattern result is an array of matched keys. Key result is the
            Redis value cast appropriately — object for hashes, array for
            sets, string for strings.
          content:
            application/json:
              schema:
                oneOf:
                  - type: array
                    items:
                      type: string
                    description: Keys matched by `SCAN MATCH`.
                  - type: object
                    additionalProperties:
                      type: string
                    description: Hash value (`HGETALL`).
                  - type: array
                    items:
                      type: string
                    description: Set members (`SMEMBERS`).
                  - type: string
                    description: String value (`GET`).
        '400':
          description: '`pattern` / `key` is outside the allowlist.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  allowedPrefixes:
                    type: array
                    items:
                      type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          description: Neither `pattern` nor `key` was supplied.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - SuperUser
  /admin/network/redis-info:
    get:
      tags:
        - Network
      operationId: getRedisInfo
      summary: Redis cluster INFO
      description: |
        Returns the raw INFO payload from the Redis cluster — replication
        topology, keyspace cardinality, memory stats, etc. Restricted to
        SuperUser because the payload leaks operational detail. Audit-logged
        to the Admin Discord channel on every call.
      responses:
        '200':
          description: INFO payload.
          content:
            application/json:
              schema:
                type: object
                required:
                  - info
                properties:
                  info:
                    type: string
                    description: Raw multi-section INFO output (newline-delimited `key:value` pairs grouped by `# Section` headers).
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-access-policies:
        - SuperUser
  /admin/screen/{screenId}:
    parameters:
      - in: path
        name: screenId
        required: true
        schema:
          type: string
    get:
      tags:
        - Screens
      operationId: getScreen
      summary: Fetch a screen (not implemented)
      description: |
        **Not implemented.** Handler currently throws — calls return a 500.
        Endpoint is registered to reserve the path; expect this to land later.
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          description: Always — handler is a stub.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
    put:
      tags:
        - Screens
      operationId: updateScreen
      summary: Update a screen (incomplete)
      description: |
        Mutates a screen's settings. **Currently incomplete** — the handler
        throws after fetching the screen because the target-user wiring isn't
        done. Calls return a 500.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                settings:
                  type: object
                  description: '`ScreenSettings` payload.'
                  additionalProperties: true
              additionalProperties: true
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No screen with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Handler currently throws.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
    delete:
      tags:
        - Screens
      operationId: deleteScreen
      summary: Delete a screen (broken stub)
      description: |
        **Broken stub** — `deleteScreen` in
        `cloud/src/CloudAdminApi.ts` calls `Cloud.deleteScreen` but
        never sends a response on success, so requests **hang until
        the client times out** unless the underlying delete throws.
      deprecated: true
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No screen with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '504':
          description: Handler never responds on success — gateway / client timeout.
      x-access-policies:
        - Moderator
  /admin/screen/{screenId}/session:
    parameters:
      - in: path
        name: screenId
        required: true
        schema:
          type: string
    put:
      tags:
        - Screens
      operationId: updateScreenMediaSession
      summary: Update the screen's media session
      description: |
        Mutates the active media session on a screen — pause / resume,
        seek, change product. Used by the Arda screen-control panel.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                playbackState:
                  type: string
                  enum:
                    - playing
                    - paused
                    - ended
                currentTimeMs:
                  type: integer
                mediaProductId:
                  type: string
                  description: Switch the screen to a different product.
                bigMediaId:
                  type: string
              additionalProperties: true
      responses:
        '200':
          description: Updated screen.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Screen'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No screen with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
  /admin/activity:
    post:
      tags:
        - Activities
      operationId: createActivity
      summary: Create a shared activity (not implemented)
      description: |
        **Not implemented.** Handler currently throws — calls return
        a 500. Endpoint is registered to reserve the path; expect
        this to land later.
      deprecated: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - type
                - ownerBigscreenAccountId
              properties:
                type:
                  type: string
                  description: Activity type identifier.
                ownerBigscreenAccountId:
                  type: string
                roomId:
                  type: string
                  description: Bind the activity to a specific room (optional).
                metaData:
                  type: object
                  additionalProperties: true
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          description: Always — handler is a stub.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
  /admin/activity/{activityId}:
    parameters:
      - in: path
        name: activityId
        required: true
        schema:
          type: string
    put:
      tags:
        - Activities
      operationId: updateActivity
      summary: Update an activity (not implemented)
      description: |
        **Not implemented.** Handler currently throws — calls return
        a 500.
      deprecated: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          description: Always — handler is a stub.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
    delete:
      tags:
        - Activities
      operationId: deleteActivity
      summary: Delete an activity (not implemented)
      description: |
        **Not implemented.** Handler currently throws — calls return
        a 500.
      deprecated: true
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          description: Always — handler is a stub.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
  /admin/account/{bigscreenAccountId}/activity/{activityId}:
    parameters:
      - in: path
        name: bigscreenAccountId
        required: true
        schema:
          type: string
      - in: path
        name: activityId
        required: true
        schema:
          type: string
    get:
      tags:
        - Activities
      operationId: getActivityForAccount
      summary: Fetch one activity owned by an account (not implemented)
      description: |
        **Not implemented.** Same `getActivities` handler as
        `/admin/account/{id}/activities` — currently throws and
        returns a 500.
      deprecated: true
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          description: Always — handler is a stub.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
  /admin/account/{bigscreenAccountId}/activities:
    parameters:
      - in: path
        name: bigscreenAccountId
        required: true
        schema:
          type: string
    get:
      tags:
        - Activities
      operationId: getActivitiesForAccount
      summary: List activities for an account (not implemented)
      description: |
        **Not implemented.** Handler currently throws — calls return
        a 500.
      deprecated: true
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          description: Always — handler is a stub.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
  /admin/account/{bigscreenAccountId}/social/profile:
    parameters:
      - in: path
        name: bigscreenAccountId
        required: true
        schema:
          type: string
    get:
      tags:
        - Social
      operationId: getSocialProfileForAccount
      summary: Primary social profile for an account
      description: |
        Returns the account's primary `SocialProfile`. Admin shape — includes
        `bigscreenAccountId` and the full `SocialStats`. Backed by
        `SocialProfileLookup` in the cloud_api social subsystem.
      responses:
        '200':
          description: Social profile.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SocialProfile'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Account has no social profile yet.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Moderator
  /admin/account/{bigscreenAccountId}/social_profiles:
    parameters:
      - in: path
        name: bigscreenAccountId
        required: true
        schema:
          type: string
    get:
      tags:
        - Social
      operationId: getSocialProfilesForAccount
      summary: All social profiles for an account (debug)
      description: |
        Returns every `SocialProfile` row linked to the account — used for
        debugging migrations or unexpected duplicates. `SocialProfileLookup`
        normally enforces one profile per account, so this should usually
        return a single-element array.
      responses:
        '200':
          description: Social profiles.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SocialProfile'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-access-policies:
        - Moderator
  /admin/account/{bigscreenAccountId}/social/history/{historyItemType}:
    parameters:
      - in: path
        name: bigscreenAccountId
        required: true
        schema:
          type: string
      - in: path
        name: historyItemType
        required: true
        schema:
          $ref: '#/components/schemas/HistoryItemType'
        description: |
          `HistoryItemType` filter. Optional in the underlying route
          (`/social/history{/:historyItemType}`) — when omitted, all
          classes are returned.
    get:
      tags:
        - Social
      operationId: getSocialHistoryForAccount
      summary: Recently encountered users / rooms
      description: |
        Returns the account's social history — rooms joined, users met,
        activities engaged in. Items are the public projection
        (`HistoryItem.getPublicData()`).
      responses:
        '200':
          description: History entries.
          content:
            application/json:
              schema:
                type: object
                required:
                  - bigscreenAccountId
                  - items
                  - count
                properties:
                  bigscreenAccountId:
                    type: string
                  historyItemType:
                    $ref: '#/components/schemas/HistoryItemType'
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/HistoryItem'
                  count:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-access-policies:
        - Moderator
  /admin/account/{bigscreenAccountId}/social/graph/{socialGraphType}:
    parameters:
      - in: path
        name: bigscreenAccountId
        required: true
        schema:
          type: string
      - in: path
        name: socialGraphType
        required: true
        schema:
          type: string
          enum:
            - Unknown
            - Blocked
            - Friends
            - Following
            - Muted
            - '0'
            - '1'
            - '2'
            - '3'
            - '4'
        description: |
          `SocialGraphType` filter — see [`SocialGraphType`](../components/schemas/SocialGraphType.yaml).
          Accepts the member name or its numeric value. Optional in the
          underlying route — when omitted, all edges are returned.
    get:
      tags:
        - Social
      operationId: getSocialGraphForAccount
      summary: Social graph edges for an account
      description: Returns the social-graph edges (friends / blocked / following / muted) involving this account.
      responses:
        '200':
          description: Graph edges page.
          content:
            application/json:
              schema:
                type: object
                required:
                  - bigscreenAccountId
                  - items
                  - count
                properties:
                  bigscreenAccountId:
                    type: string
                  socialGraphType:
                    $ref: '#/components/schemas/SocialGraphType'
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/SocialGraphItem'
                  count:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-access-policies:
        - Moderator
  /admin/social/graph/{graphItemId}:
    parameters:
      - in: path
        name: graphItemId
        required: true
        schema:
          type: string
    delete:
      tags:
        - Social
      operationId: deleteSocialGraphItem
      summary: Delete a social-graph edge
      description: |
        Hard-deletes a single graph edge — used to undo erroneous
        friend / block entries surfaced by support. Restricted to
        `Admin`. **Returns plain text `OK`** — not JSON.
      responses:
        '200':
          description: Edge deleted. Body is the literal string `OK`.
          content:
            text/plain:
              schema:
                type: string
                example: OK
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No edge with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Admin
  /admin/account/{bigscreenAccountId}/social/stats:
    parameters:
      - in: path
        name: bigscreenAccountId
        required: true
        schema:
          type: string
    get:
      tags:
        - Social
      operationId: getSocialStats
      summary: Social counters for an account
      description: |
        Returns the account's `SocialStats` — friend / follower / blocked /
        notification / history counters plus `lastOnlineDate`.
      responses:
        '200':
          description: Social stats.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SocialStats'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-access-policies:
        - Moderator
  /admin/account/{bigscreenAccountId}/social/notifications:
    parameters:
      - in: path
        name: bigscreenAccountId
        required: true
        schema:
          type: string
    get:
      tags:
        - Social
      operationId: getSocialNotifications
      summary: List notifications for an account
      description: |
        Returns every `Notification` row stored for the account regardless
        of `notificationStatus` — admin shape, includes
        `senderBigscreenAccountId`, `recipientBigscreenAccountId`, and the
        DynamoDB `ttl`.
      responses:
        '200':
          description: Notifications.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SocialNotification'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-access-policies:
        - Admin
  /admin/account/{bigscreenAccountId}/social/notification:
    parameters:
      - in: path
        name: bigscreenAccountId
        required: true
        schema:
          type: string
    post:
      tags:
        - Social
      operationId: createAdminNotification
      summary: Send an admin notification
      description: |
        Creates a notification destined for the supplied account. Used for
        system messages (`AdminMessage`) and ad-hoc admin nudges. The
        recipient is the path account; the sender is the calling staff
        account.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - notificationType
                - title
              properties:
                notificationType:
                  $ref: '#/components/schemas/NotificationType'
                title:
                  type: string
                body:
                  type: string
                metaData:
                  type: object
                  description: |
                    `NotificationMetaData` payload. Common keys:
                    `roomId` (for `RoomInvite` / `KnockKnock`),
                    `version`.
                  properties:
                    roomId:
                      type: string
                    version:
                      type: string
                  additionalProperties: true
                expiryDate:
                  type: integer
                  description: |
                    Optional override for the default 30-day
                    application-level expiry (Unix seconds).
      responses:
        '200':
          description: Created notification.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SocialNotification'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-access-policies:
        - Admin
  /admin/social/notification/{notificationId}:
    parameters:
      - in: path
        name: notificationId
        required: true
        schema:
          type: string
    delete:
      tags:
        - Social
      operationId: deleteSocialNotification
      summary: Delete a notification
      description: Hard-deletes a notification.
      responses:
        '200':
          description: Notification deleted.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No notification with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Admin
  /admin/social/notification/{notificationId}/accept:
    parameters:
      - in: path
        name: notificationId
        required: true
        schema:
          type: string
    get:
      tags:
        - Social
      operationId: acceptSocialNotification
      summary: Force-accept a notification (stub)
      description: |
        **Stub** — `acceptNotification` in `cloud/src/CloudAdminApi.ts`
        currently just returns plain text `OK` without firing any
        side effects. The intended behaviour: accept the notification
        on behalf of the recipient (driving e.g. `FriendRequest` →
        create the `Friends` edge, set `notificationStatus=Accepted`).
      deprecated: true
      responses:
        '200':
          description: Body is the literal string `OK`. No actual side effects today.
          content:
            text/plain:
              schema:
                type: string
                example: OK
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No notification with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-access-policies:
        - Admin
components:
  securitySchemes:
    cloudAdminApiKey:
      type: http
      scheme: bearer
      bearerFormat: CloudAdminApiKey-or-OAuthJWT
      description: |
        Bearer key registered for the `cloud-admin-api` application. May
        be a 64-character app key or, when OAuth is enabled, an OAuth 2.0
        JWT — admission shape determines which path runs.
    cloudAdminAccessToken:
      type: apiKey
      in: header
      name: x-access-token
      description: |
        JWT for the staff account the request is acting as. Must carry an
        `AccessPolicy` admitted by the per-endpoint `x-access-policies`.
  responses:
    Unauthorized:
      description: Missing or invalid `Authorization` / `x-access-token`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: Authenticated but lacks the required access policy.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: string
        details:
          description: Optional structured details.
      example:
        error: Forbidden
        details:
          requiredPolicy: Moderator
    BigscreenAccountStatus:
      type: string
      description: |
        Account moderation state. Mirrors `BigscreenAccountStatus` in
        [`auth/Schemas.ts`](../../../../auth/Schemas.ts).
        - `OK` — normal.
        - `ReadOnly` — can sign in and view, but mutations are blocked.
        - `Banned` — sign-in blocked.
      enum:
        - OK
        - ReadOnly
        - Banned
      default: OK
      example: OK
    SocialStats:
      type: object
      description: |
        Aggregate counters carried inside a `SocialProfile` and returned from
        `/admin/account/{id}/social/stats`. Mirrors `SocialStats` in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).
        The public-facing projection (`PublicSocialStats`) is a subset of these
        fields — see [`PublicSocialStats`](./PublicSocialStats.yaml).
      required:
        - friendsCount
        - followersCount
        - blockedCount
        - notificationsCount
        - unreadNotificationsCount
        - historyItemCount
        - lastOnlineDate
      properties:
        socialId:
          type: string
          description: Profile this stats record belongs to.
        notificationsCount:
          type: integer
          description: Total notifications stored for the profile.
        unreadNotificationsCount:
          type: integer
          description: Notifications still in `Unread` status.
        historyItemCount:
          type: integer
          description: Active rows in the social-history table for the profile.
        friendsCount:
          type: integer
          description: Active `Friends` edges.
        followersCount:
          type: integer
          description: Active `Following` edges where the profile is the target.
        blockedCount:
          type: integer
          description: Active `Blocked` edges owned by the profile.
        lastOnlineDate:
          type: integer
          description: Last time the profile was online, as a Unix timestamp in **seconds**.
      example:
        socialId: soc_01HZR12345ABC678DEF90GHJKM
        notificationsCount: 12
        unreadNotificationsCount: 3
        historyItemCount: 87
        friendsCount: 42
        followersCount: 18
        blockedCount: 1
        lastOnlineDate: 1762462440
    SocialProfile:
      type: object
      description: |
        Stored social profile for a Bigscreen account — the **admin** shape
        returned from `/admin/account/{bigscreenAccountId}/social/profile` and
        `/admin/account/{bigscreenAccountId}/social_profiles`. Mirrors
        `SocialProfile` (extends `DocumentDatabase.BaseItem`) in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).

        - `id` is the `socialId` (the class also exposes a `socialId` getter that
          returns `id` — both names refer to the same value).
        - `bigscreenAccountId` is admin-only; the public projection
          ([`PublicSocialProfile`](./PublicSocialProfile.yaml)) strips it.
        - There is at most one `SocialProfile` per Bigscreen account
          (`SocialProfileLookup` enforces uniqueness); the
          `/admin/account/{id}/social_profiles` endpoint returning an array is a
          debug surface for migrations / multi-handle situations.
      required:
        - id
        - bigscreenAccountId
        - createdAt
      properties:
        id:
          type: string
          description: |
            `socialId` — same value as the `socialId` getter on the class.
        createdAt:
          type: integer
          description: Profile creation, Unix timestamp in **milliseconds** (from `BaseItem`).
        updatedAt:
          type: integer
          description: Last write to the profile row, Unix timestamp in **milliseconds**.
        bigscreenAccountId:
          type: string
          description: |
            Underlying Bigscreen account this profile wraps. Admin-only —
            stripped by `getPublicData()`.
        accountCreatedAt:
          type: integer
          description: |
            Underlying Bigscreen account creation, Unix timestamp in
            **seconds**. Copied in via `updatePublicAccountData()`.
        bigAccountStatus:
          $ref: '#/components/schemas/BigscreenAccountStatus'
        isVerified:
          type: boolean
        username:
          type: string
        oculusId:
          type: string
        oculusUserId:
          type: string
        steamId:
          type: string
        smallImageUrl:
          type: string
          format: uri
        largeImageUrl:
          type: string
          format: uri
        badges:
          type: array
          items:
            type: string
        stats:
          $ref: '#/components/schemas/SocialStats'
      example:
        id: soc_01HZR12345ABC678DEF90GHJKM
        createdAt: 1761127240000
        updatedAt: 1762462440000
        bigscreenAccountId: acct_01HXJ4Q12345
        accountCreatedAt: 1761127200
        bigAccountStatus: OK
        isVerified: true
        username: alice
        oculusId: ''
        oculusUserId: ''
        steamId: '76561198012345678'
        smallImageUrl: https://cdn.bigscreenvr.com/avatars/alice_sm.png
        largeImageUrl: https://cdn.bigscreenvr.com/avatars/alice_lg.png
        badges:
          - beyond-early-supporter
        stats:
          socialId: soc_01HZR12345ABC678DEF90GHJKM
          notificationsCount: 12
          unreadNotificationsCount: 3
          historyItemCount: 87
          friendsCount: 42
          followersCount: 18
          blockedCount: 1
          lastOnlineDate: 1762462440
    StoredRoom:
      type: object
      description: |
        A persistent room record — the database row that survives restarts. Mirrors
        `cloud/src/Schemas.StoredRoom`. Distinct from `LiveRoom`, which is the
        Redis-backed real-time view (occupants, server assignment, current screens,
        etc.). Lobbies and TV rooms don't have stored rows; for those, the live
        snapshot's `storedRoomSnapshot` substitutes.
      required:
        - roomId
        - ownerBigscreenAccountId
        - name
      properties:
        roomId:
          type: string
          description: External room id (e.g. `room:abc123`). Admins can join via this id; other users need an invite.
        creatorBigscreenAccountId:
          type: string
        ownerBigscreenAccountId:
          type: string
        serverId:
          type: string
          description: Server that owns this room.
        roomStatus:
          type: string
          description: '`RoomStatus` value (`OK`, etc.).'
        inviteCode:
          type: string
          description: Shareable invite code; can be rotated at any time.
        name:
          type: string
        description:
          type: string
        category:
          type: string
          description: '`RoomCategory` — used for VR-side sorting; NSFW rooms are gated separately.'
        publicTags:
          type: array
          items:
            type: string
        backgroundImageUrl:
          type: string
          format: uri
        images:
          type: array
          items:
            type: string
            format: uri
        environmentId:
          type: string
          description: Scene loaded on first join.
        maxUsersPerInstance:
          type: integer
          description: Max users per room instance; multiple instances may exist when the room is scaleable.
        roomType:
          type: string
          description: Deprecated `RoomType` value.
          deprecated: true
        visibility:
          type: string
          description: '`RoomVisibility` — `Public`, `Private`, `FriendsOnly`.'
        instancePolicy:
          type: string
          description: '`RoomInstancePolicy` — controls when new room instances spin up.'
        screenSettings:
          type: object
          additionalProperties: true
          description: Default screen settings for the room.
        microphonePolicy:
          type: string
        movementSoundsPolicy:
          type: string
        drawingVisibility:
          type: string
        avatarNameVisibility:
          type: string
        poseVisibility:
          type: string
        toySpawnability:
          type: string
        preferredUserType:
          type: string
        stagedSocialIds:
          type: array
          items:
            type: string
          description: Staged users — granted draw / toy / mic / movement settings equivalent to admins.
        modSocialIds:
          type: array
          items:
            type: string
          description: Mods — can kick other users.
        adminBigscreenAccountIds:
          type: array
          items:
            type: string
        version:
          type: string
          example: V1
        createdAt:
          type: string
          format: date-time
      example:
        roomId: room:abc1234567
        creatorBigscreenAccountId: acct_01HXJ4Q12345
        ownerBigscreenAccountId: acct_01HXJ4Q12345
        serverId: ms-us-west-2-04
        roomStatus: OK
        inviteCode: BLUE-LION-9421
        name: Friday Movie Night
        description: Watching Big Buck Bunny.
        category: Movie
        publicTags:
          - movie
          - casual
        images: []
        environmentId: env-cinema-01
        maxUsersPerInstance: 12
        visibility: FriendsOnly
        instancePolicy: Default
        microphonePolicy: All
        movementSoundsPolicy: 'On'
        drawingVisibility: All
        avatarNameVisibility: Default
        poseVisibility: All
        toySpawnability: All
        preferredUserType: VerifiedUsersOnly
        stagedSocialIds: []
        modSocialIds: []
        adminBigscreenAccountIds:
          - acct_01HXJ4Q12345
        version: V1
        createdAt: '2026-04-22T10:14:00.000Z'
    LiveRoom:
      type: object
      description: |
        Real-time view of a room — `LiveRoom` from
        [`cloud/src/Schemas.ts`](../../../../cloud/src/Schemas.ts). The
        Redis-backed snapshot tracking participants, server assignment,
        screens, and instance state.

        Note that **most cloud-api routes that look like they return a
        `LiveRoom` actually return a [`PublicRoom`](./PublicRoom.yaml)** —
        the sanitised projection built for VR clients. `LiveRoom` is the
        internal storage shape used by admin endpoints
        (`/admin/rooms/live`, `/admin/room/{id}/live`).
      required:
        - roomId
      properties:
        roomId:
          type: string
        createdAt:
          type: integer
          description: |
            `createdAt` of the underlying `StoredRoom` — Unix milliseconds.
        instanceCreatedAt:
          type: integer
          description: When this `LiveRoom` instance was bootstrapped — Unix milliseconds.
        visibility:
          type: string
          description: '`RoomVisibility` — `Public`, `Private`, `FriendsOnly`. Deprecated; check `legacyVisibility` too.'
        roomType:
          type: string
          description: '`RoomType` — `Room`, `Channel`, `Lobby`, etc.'
        inviteCode:
          type: string
          description: Deprecated — prefer `storedRoomSnapshot.inviteCode`.
        ownerSocialProfile:
          description: |
            `SocialProfile` of the room owner — unset for unowned rooms
            (lobbies, TV channels).
          oneOf:
            - $ref: '#/components/schemas/SocialProfile'
            - type: 'null'
        storedRoomSnapshot:
          $ref: '#/components/schemas/StoredRoom'
          description: Snapshot of the underlying `StoredRoom` (or generated for lobby / TV rooms).
        environment:
          type: object
          description: '`Environment` instance — the loaded room theme / scene.'
          additionalProperties: true
        status:
          type: string
          description: '`LiveRoomStatus` — used by the client to detect migrations or room errors.'
        adminUserIds:
          type: array
          items:
            type: string
        mediaServer:
          type: object
          description: '`MediaServer` instance currently hosting the room — admin shape (full data).'
          additionalProperties: true
        participantsIndex:
          type: object
          description: |
            Map from `userSessionId` to `roomInstanceId` — fast lookup of
            the instance a given participant is in.
          additionalProperties:
            type: string
        roomInstanceParticipantsIndex:
          type: object
          description: |
            Map from `roomInstanceId` to `RoomInstanceParticipantsInfo` —
            the inverse index, used to enumerate who is in a given
            instance.
          additionalProperties:
            type: object
            additionalProperties: true
        screens:
          type: array
          description: Screens currently in the room (admin shape — raw `Screen`).
          items:
            type: object
            additionalProperties: true
        primaryScreenIndex:
          type: integer
        legacyVisibility:
          type: string
          description: Old-style `RoomVisibility`. Some legacy clients read this instead of `visibility`.
        contextAppName:
          type: string
          description: App-name of the bearer token that originally created the room.
        contextAppVersion:
          type: string
          description: Minimum app version a joiner must have (the app version of the bearer that created the room).
      example:
        roomId: room:abc1234567
        createdAt: 1761127240000
        instanceCreatedAt: 1762462000000
        visibility: FriendsOnly
        roomType: Room
        inviteCode: BLUE-LION-9421
        adminUserIds:
          - sess_01HZP12345
        participantsIndex:
          sess_01HZP12345: inst_01HZP67890
        roomInstanceParticipantsIndex: {}
        screens: []
        primaryScreenIndex: 0
        legacyVisibility: Public
        contextAppName: bigscreen-unity
        contextAppVersion: 1.42.0
    AdminAccount:
      type: object
      description: |
        Sanitized admin view of a Bigscreen account — `SanitizedAdminAccount`
        from [`auth/Schemas.ts`](../../../../auth/Schemas.ts) — the shape
        returned to admin callers from `/admin/account/*` and
        `/admin/account/query/*`. Built from the raw `BigscreenAccount` minus
        secret fields like password hashes; the Steam / Oculus / EmailPassword
        subobjects are flattened into `email`, `steamProfile`, and
        `oculusProfile`.
      required:
        - id
        - username
      properties:
        id:
          type: string
          description: Bigscreen account UUID.
        createdAt:
          type: string
          format: date-time
        status:
          $ref: '#/components/schemas/BigscreenAccountStatus'
        isVerified:
          type: boolean
          description: Whether the account's primary email is verified.
        isStaff:
          type: boolean
          description: Whether the account belongs to Bigscreen staff.
        username:
          type: string
        lowerCaseUsername:
          type: string
          description: |
            `username` lowercased — surfaced for case-insensitive search.
        email:
          type: string
          format: email
          description: Surfaced from `EmailPasswordAccount.email` when an email identity is attached.
        steamProfile:
          type: object
          description: Steam identity (when attached). Loose shape — Steam fields surfaced as-is.
          additionalProperties: true
        oculusProfile:
          type: object
          description: '`SanitizedOculusProfile` (when an Oculus identity is attached).'
          properties:
            oculusId:
              type: string
            oculusImageURL:
              type: string
              format: uri
            oculusSmallImageURL:
              type: string
              format: uri
        accessPolicies:
          type: array
          items:
            type: string
          description: Access policies the account holds (e.g. `Admin`, `Moderator`, `BigscreenDefault`).
        banHistory:
          type: array
          items:
            type: object
            additionalProperties: true
        adminNotes:
          type: array
          description: '`AdminNote[]` — staff annotations attached to the account.'
          items:
            type: object
            additionalProperties: true
      example:
        id: acct_01HXJ4Q12345
        createdAt: '2026-04-22T10:14:00.000Z'
        status: OK
        isVerified: true
        isStaff: false
        username: alice
        lowerCaseUsername: alice
        email: alice@example.com
        accessPolicies:
          - BigscreenDefault
        banHistory: []
        adminNotes: []
    MediaServer:
      type: object
      description: |
        A media-server registration — one row per `media_server_next` instance that
        has registered with `cloud_api`. Mirrors `cloud/src/Schemas.MediaServer`.
        Identifies where the server lives, current load, last-heartbeat freshness,
        and which rooms it's hosting.

        `activationStatus` is the only field admins typically mutate via
        `PUT /admin/network/media-server/{id}` — everything else is reported by the
        server itself.
      required:
        - id
        - ipAddress
        - activationStatus
      properties:
        id:
          type: string
          description: Server-reported identifier.
        version:
          type: string
          description: Media-server build version (independent of cloud_api's app version).
        createdAt:
          type: string
          format: date-time
          description: When the server first registered.
        lastHeartbeat:
          type: string
          format: date-time
          description: Most recent heartbeat from the server. Stale heartbeats imply `Unresponsive` status.
        activationStatus:
          type: string
          description: '`MediaServerActivationStatus` value — `Active`, `Disabled`, etc. Admin-mutable.'
        ipAddress:
          type: string
          description: Public IP address.
        minPort:
          type: integer
          description: Lower bound of the UDP port range this server will use.
        maxPort:
          type: integer
          description: Upper bound of the UDP port range.
        isAccelerated:
          type: boolean
          description: Whether the server uses AWS Global Accelerator.
        region:
          type:
            - string
            - 'null'
          description: AWS region (when applicable).
        instanceId:
          type:
            - string
            - 'null'
          description: AWS EC2 instance id.
        instanceType:
          type:
            - string
            - 'null'
          description: AWS instance type (e.g. `t2.xlarge`).
        load:
          type: number
          description: Estimated load 0–100, server-reported.
        roomIds:
          type: array
          items:
            type: string
          description: Rooms currently hosted on this server.
      example:
        id: ms-us-west-2-04
        version: 1.42.3
        createdAt: '2026-04-22T10:14:00.000Z'
        lastHeartbeat: '2026-05-06T19:14:00.000Z'
        activationStatus: Active
        ipAddress: 203.0.113.40
        minPort: 32000
        maxPort: 33000
        isAccelerated: true
        region: us-west-2
        instanceId: i-0a1b2c3d4e5f6789
        instanceType: c6i.xlarge
        load: 28
        roomIds:
          - room:abc1234567
          - room:def8901234
    Screen:
      type: object
      description: |
        Public-facing screen — `PublicScreen` from
        [`cloud/src/Schemas.ts`](../../../../cloud/src/Schemas.ts). Returned
        by every cloud-api screen endpoint that exposes screen state to a VR
        client (`/screen`, `/screen/{id}`, `/screens`,
        `/room/{roomId}/screens`). `localScreenId` is only populated for the
        screen's owner.
      required:
        - id
        - name
      properties:
        id:
          type: string
        name:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        screenType:
          type: string
          description: '`ScreenType` — the screen''s role in its room (`Movie`, `Monitor`, etc.).'
        visibility:
          type: string
          description: '`ScreenVisibility` enum.'
        mediaServer:
          type: object
          description: '`PublicMediaServer` for the host serving this screen.'
          additionalProperties: true
        mediaPeer:
          type: object
          description: '`MediaPeer` payload — peer-connection metadata for the active publisher / subscriber.'
          additionalProperties: true
        sharedMediaSession:
          type: object
          description: '`SharedMediaSession` — currently-playing media on the screen.'
          additionalProperties: true
        legacyContentString:
          type: string
          description: |
            Pre-`SharedMediaSession` content descriptor. Old clients
            still parse this; new clients should prefer
            `sharedMediaSession`.
        ownerUserSessionId:
          type: string
        isMonitor:
          type: boolean
        isPresenting:
          type: boolean
        isSimulated:
          type: boolean
        localScreenId:
          type: string
          description: |
            The owner's local screen id — only included when the
            requesting user owns the screen.
        format:
          type: string
          description: '`ScreenFormat` (e.g. `2D`, `3D_SBS`, `3D_TB`).'
        aspectRatio:
          type: number
          format: float
        brightness:
          type: number
          format: float
        channel:
          description: Legacy channel descriptor when the screen is showing channel content.
          additionalProperties: true
        mediaProduct:
          description: Legacy media-product descriptor when the screen is showing a `MediaProduct`.
          additionalProperties: true
      example:
        id: scr_01HZP01234
        name: Main Screen
        createdAt: '2026-04-22T10:14:00.000Z'
        updatedAt: '2026-05-06T19:14:00.000Z'
        screenType: Movie
        visibility: Public
        mediaServer:
          id: ms-us-west-2-04
          url: wss://ms-us-west-2-04.bigscreencloud.com
        sharedMediaSession: {}
        legacyContentString: ''
        ownerUserSessionId: sess_01HZP12345
        isMonitor: false
        isPresenting: true
        isSimulated: false
        format: 2D
        aspectRatio: 1.778
        brightness: 1
    HistoryItemType:
      type: string
      description: |
        Class of social-history entry. Mirrors `HistoryItemType` in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).
        - `Room` — caller visited / joined a room (the snapshot is the room).
        - `User` — caller encountered another user (the snapshot is that user's
          public profile, plus the room they were met in).
        - `Activity` — caller engaged with a shared activity (game, co-watch).
      enum:
        - Room
        - User
        - Activity
      example: User
    HistoryItemVisibility:
      type: string
      description: |
        Soft-delete state on a history entry. Mirrors `HistoryItemVisibility` in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).
        `MarkedForDeletion` rows are filtered out of normal history responses but
        kept on disk until TTL expiry.
      enum:
        - Default
        - MarkedForDeletion
      default: Default
      example: Default
    PublicSocialStats:
      type: object
      description: |
        Public-safe projection of `SocialStats` — strips notification / history
        counts so it can be returned to other users. Mirrors `PublicSocialStats`
        in [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).
        A `PublicSocialProfile` carries either this or the full `SocialStats`,
        depending on whether the requester is viewing their own profile.
      required:
        - friendsCount
        - followersCount
        - lastOnlineDate
      properties:
        friendsCount:
          type: integer
        followersCount:
          type: integer
        lastOnlineDate:
          type: integer
          description: Last time the profile was online, as a Unix timestamp in **seconds**.
      example:
        friendsCount: 42
        followersCount: 18
        lastOnlineDate: 1762462440
    PublicSocialProfile:
      type: object
      description: |
        Public-safe projection of a `SocialProfile` — the shape returned to other
        users from `/social/profile/{socialId}` and friends-list / search routes.
        Mirrors `PublicSocialProfile` in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).
        Strips moderation-only fields (`bigscreenAccountId`, the full `SocialStats`)
        in favour of `socialId` and `PublicSocialStats`.

        `stats` carries the **full** `SocialStats` when the caller is viewing
        their own profile, and `PublicSocialStats` otherwise.
      required:
        - socialId
        - username
      properties:
        socialId:
          type: string
        createdAt:
          type: integer
          description: Profile creation, Unix timestamp in **milliseconds**.
        updatedAt:
          type: integer
          description: Last write to the profile row, Unix timestamp in **milliseconds**.
        accountCreatedAt:
          type: integer
          description: Underlying Bigscreen account creation, Unix timestamp in **seconds**.
        bigAccountStatus:
          $ref: '#/components/schemas/BigscreenAccountStatus'
        isVerified:
          type: boolean
        username:
          type: string
        oculusId:
          type: string
        oculusUserId:
          type: string
        steamId:
          type: string
        smallImageUrl:
          type: string
          format: uri
        largeImageUrl:
          type: string
          format: uri
        badges:
          type: array
          items:
            type: string
        stats:
          oneOf:
            - $ref: '#/components/schemas/SocialStats'
            - $ref: '#/components/schemas/PublicSocialStats'
          description: Full `SocialStats` when viewing own profile; `PublicSocialStats` otherwise.
      example:
        socialId: soc_01HZR12345ABC678DEF90GHJKM
        createdAt: 1761127240000
        updatedAt: 1762462440000
        accountCreatedAt: 1761127200
        bigAccountStatus: OK
        isVerified: true
        username: alice
        oculusId: ''
        oculusUserId: ''
        steamId: '76561198012345678'
        smallImageUrl: https://cdn.bigscreenvr.com/avatars/alice_sm.png
        largeImageUrl: https://cdn.bigscreenvr.com/avatars/alice_lg.png
        badges:
          - beyond-early-supporter
        stats:
          friendsCount: 42
          followersCount: 18
          lastOnlineDate: 1762462440
    HistoryItem:
      type: object
      description: |
        Entry in a user's social history — a room they joined, a person they met,
        or an activity they engaged in. The shape here is the **public projection**
        returned from `getPublicData()` on `HistoryItem` in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts) —
        the stored row also carries a `bigscreenAccountId` (owner) and a `ttl`
        that are not surfaced.

        Two snapshot fields are present depending on `historyItemType`:
        - `historyItemType=Room` — `room` is set (a `SimpleRoomSnapshot`),
          `socialProfileSnapshot` is unset.
        - `historyItemType=User` — both `room` and `socialProfileSnapshot` are set.
        - `historyItemType=Activity` — neither snapshot is guaranteed.
      required:
        - id
        - createdAt
        - historyItemType
        - historyVisibility
      properties:
        id:
          type: string
          description: |
            Stable hash of `(owner, target, room)` — used as the upsert key so
            re-encountering the same user in the same room does not duplicate
            history rows.
        createdAt:
          type: integer
          description: First-encounter time, Unix timestamp in **milliseconds**.
        historyItemType:
          $ref: '#/components/schemas/HistoryItemType'
        historyVisibility:
          $ref: '#/components/schemas/HistoryItemVisibility'
        roomId:
          type: string
          description: Room the encounter happened in (when applicable).
        room:
          type: object
          description: |
            `SimpleRoomSnapshot` of the room at the time of the encounter — a
            denormalized copy so the history entry survives room deletion.
          additionalProperties: true
        socialProfileSnapshot:
          description: |
            Snapshot of the encountered user's `PublicSocialProfile` at
            encounter time. Only present when `historyItemType=User`.
          oneOf:
            - $ref: '#/components/schemas/PublicSocialProfile'
            - type: 'null'
        metaData:
          type: object
          description: Free-form payload attached at write time.
          additionalProperties: true
      example:
        id: 0a1b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789
        createdAt: 1762462440000
        historyItemType: User
        historyVisibility: Default
        roomId: room:abc1234567
        room:
          roomId: room:abc1234567
          title: Movie Night
          visibility: FriendsOnly
        socialProfileSnapshot:
          socialId: soc_01HZR67890ABC678DEF90GHJKM
          username: bob
          smallImageUrl: https://cdn.bigscreenvr.com/avatars/bob_sm.png
        metaData: {}
    SocialGraphType:
      type: integer
      description: |
        Edge classification on the social graph. Mirrors `SocialGraphType` in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts) —
        the enum is **numeric** in storage (DynamoDB / JSON responses), but
        admin / cloud routes accept the member name as a path segment too
        (the handler coerces both). Member meanings:
        - `0` (`Unknown`) — placeholder; should not appear in stored edges.
        - `1` (`Blocked`) — owner has blocked target. Uni-directional; only the
          blocker can delete it.
        - `2` (`Friends`) — mutual friendship. Either side can terminate.
        - `3` (`Following`) — uni-directional follow. Only the follower can delete.
        - `4` (`Muted`) — uni-directional mute. Only the muter can delete.
      enum:
        - 0
        - 1
        - 2
        - 3
        - 4
      x-enum-varnames:
        - Unknown
        - Blocked
        - Friends
        - Following
        - Muted
      example: 2
    SocialGraphItem:
      type: object
      description: |
        An edge in the social graph — friend, follow, block, or mute. Mirrors
        `SocialGraphItem` (extends `DocumentDatabase.BaseItem`) in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).

        Edges have an *owner* and a *target*. Edge directionality varies by type:
        - `Friends` (mutual) — two rows are written, one per direction.
        - `Blocked` (uni-directional) — only the blocker can delete; target should
          not be able to detect the edge by query.
        - `Following` / `Muted` (uni-directional) — only the owner can delete.

        The id is a deterministic `sha256(owner, target, type)` (with the two
        accounts sorted lexicographically for `Blocked`), so re-asserting an
        edge upserts the same row.
      required:
        - id
        - ownerBigscreenAccountId
        - targetBigscreenAccountId
        - socialGraphType
      properties:
        id:
          type: string
          description: SHA-256 hex digest — see description for the hash inputs.
        createdAt:
          type: integer
          description: Edge creation, Unix timestamp in **milliseconds** (from `BaseItem`).
        ownerBigscreenAccountId:
          type: string
          description: Account that initiated the edge (the blocker, the friender, etc.).
        targetBigscreenAccountId:
          type: string
          description: Account on the receiving side of the edge.
        ownerSocialId:
          type: string
          description: Social profile id for the owner — convenience copy.
        targetSocialId:
          type: string
          description: Social profile id for the target — convenience copy.
        socialGraphType:
          $ref: '#/components/schemas/SocialGraphType'
        targetSocialProfileSnapshot:
          description: |
            Denormalized `PublicSocialProfile` for the target at edge-creation
            time, so the edge can render a name + avatar without a join even
            after profile mutation. Shape is loose — older rows may be
            partial.
          oneOf:
            - $ref: '#/components/schemas/PublicSocialProfile'
            - type: 'null'
        metaData:
          type: object
          description: Free-form payload attached at write time.
          additionalProperties: true
      example:
        id: 0a1b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789
        createdAt: 1761127240000
        ownerBigscreenAccountId: acct_01HXJ4Q12345
        targetBigscreenAccountId: acct_01HXJ4Q67890
        ownerSocialId: soc_01HZR12345ABC678DEF90GHJKM
        targetSocialId: soc_01HZR67890ABC678DEF90GHJKM
        socialGraphType: 2
        targetSocialProfileSnapshot:
          socialId: soc_01HZR67890ABC678DEF90GHJKM
          username: bob
          smallImageUrl: https://cdn.bigscreenvr.com/avatars/bob_sm.png
        metaData: {}
    NotificationType:
      type: string
      description: |
        Class of social notification. Mirrors `NotificationType` in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).
        Drives the side effect on accept (e.g. accepting a `FriendRequest`
        creates the friend edge) and the icon / wording in the client.
        - `Unknown` — placeholder; should not appear in stored notifications.
        - `FriendRequest` — pending friend invite. Accept creates a `Friends` edge.
        - `RoomInvite` — invite to join a specific room.
        - `KnockKnock` — request to enter a private room.
        - `Message` — user-to-user message.
        - `AdminMessage` — system / staff-issued message (no reply).
        - `MarketingMessage` — Bigscreen-issued promotional message.
        - `AcceptedFriendRequest` — confirmation surfaced to the original sender
          after the recipient accepted their `FriendRequest`.
      enum:
        - Unknown
        - FriendRequest
        - RoomInvite
        - KnockKnock
        - Message
        - AdminMessage
        - MarketingMessage
        - AcceptedFriendRequest
      example: FriendRequest
    NotificationStatus:
      type: string
      description: |
        Lifecycle of a notification. Mirrors `NotificationStatus` in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).
        Stored under the field `notificationStatus` (the `status` name is reserved
        in DynamoDB query filter expressions).
      enum:
        - Unread
        - Read
        - Deleted
        - Accepted
        - Declined
      default: Unread
      example: Unread
    SocialNotification:
      type: object
      description: |
        Stored notification — the **admin** shape returned from
        `/admin/account/{id}/social/notifications` and the admin notification
        routes. Mirrors `Notification` (extends `DocumentDatabase.BaseItem`) in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).

        - `id` is a deterministic SHA-256 of
          `(sender, recipient, type [, metaData])` — re-asserting upserts.
        - `notificationStatus` is the field name (the natural `status` is reserved
          in DynamoDB query filter expressions).
        - The public-facing shape returned to the recipient client is
          [`PublicNotification`](./PublicNotification.yaml) — it resolves
          `senderBigscreenAccountId` into a `senderSocialProfile` and renames
          `id` to `notificationId`.
      required:
        - id
        - recipientBigscreenAccountId
        - notificationType
        - notificationStatus
      properties:
        id:
          type: string
          description: SHA-256 hex digest — see description for the hash inputs.
        createdAt:
          type: integer
          description: Creation time, Unix timestamp in **milliseconds** (from `BaseItem`).
        senderBigscreenAccountId:
          type: string
          description: Account that triggered the notification. Empty for system / marketing messages.
        recipientBigscreenAccountId:
          type: string
          description: Account the notification is delivered to.
        notificationType:
          $ref: '#/components/schemas/NotificationType'
        notificationStatus:
          $ref: '#/components/schemas/NotificationStatus'
        ttl:
          type: integer
          description: |
            DynamoDB row-TTL, Unix timestamp in **seconds**. `-1` when unset.
        expiryDate:
          type: integer
          description: |
            Application-level expiry (default `now + 30 days`), Unix timestamp
            in **seconds**. The client hides expired notifications even if the
            row is still present.
        title:
          type: string
        body:
          type: string
        metaData:
          type: object
          description: |
            `NotificationMetaData` — free-form per-`notificationType` payload.
            Common keys: `roomId` (for `RoomInvite` / `KnockKnock`), `version`.
          properties:
            roomId:
              type: string
            version:
              type: string
          additionalProperties: true
      example:
        id: 0a1b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789
        createdAt: 1762462440000
        senderBigscreenAccountId: acct_01HXJ4Q12345
        recipientBigscreenAccountId: acct_01HXJ4Q67890
        notificationType: FriendRequest
        notificationStatus: Unread
        ttl: -1
        expiryDate: 1765054440
        title: alice wants to be your friend
        body: ''
        metaData: {}
