API reference

Credentials & pairing

The endpoints that exchange a pairing code for a device token and accept encrypted FCM uploads.

Device pairing

POST /pairing/device/code       (session auth)
  body: { label?: string }
  → { code: 'RUST-7A3B', expiresAt }

POST /pairing/device/activate   (no auth)
  body: { code }
  → { token: <48 bytes hex>, userId }

DELETE /pairing/device/:id      (session auth)
  → { ok: true }

Codes expire 10 minutes after issue. Max 3 active codes per user; oldest is invalidated on the 4th.

FCM credential upload

PUT /credentials/fcm            (bearer auth)
  body: <opaque FCM bundle JSON>
  → { ok: true, steamId, uploadedAt }

What the API does on receipt:

  1. Decode the bundle just enough to extract its Steam ID.
  2. Reject (400 mismatched_steam_id) if it doesn't match the bearer token's user's Steam ID.
  3. Encrypt the entire bundle with AES-256-GCM using CREDS_ENCRYPTION_KEY.
  4. Upsert into FcmCredential (encrypted blob + key version + timestamp).
  5. Return { ok: true }.

Reading credentials (internal)

The relay and FCM worker never call HTTP — they read from Postgres directly with the same CREDS_ENCRYPTION_KEY env. The decrypted blob lives in memory only during one Rust+ connection attempt or one FCM session.

Key rotation

keyVersion on FcmCredential lets us migrate to a new encryption key without re-uploading every user's credentials:

  1. Set both CREDS_ENCRYPTION_KEY (new) and CREDS_ENCRYPTION_KEY_PREV (old) in env.
  2. On decrypt, try new key first, fall back to old.
  3. On every successful decrypt, re-encrypt with the new key and bump keyVersion.
  4. Once keyVersion is uniform, drop CREDS_ENCRYPTION_KEY_PREV.

This is documented in src/lib/crypto.ts.