Skip to content

Part 12 — operating it

From SECURITY.md, restated because they are the difference between an app that is end-to-end encrypted and one that says it is:

  1. Bind the session token to the device. A token naming only the member would let any of that member’s devices fetch another’s wraps.
  2. Declare your kinds. Without a registry, a member with the key can write anything into every other member’s store.
  3. Leave requireSignatures on. Turning it off makes by a claim again.
  4. Show the device roster. A ghost device is only detectable if somebody can see it.
  5. Rotate on removal, immediately, and check both failed and owed.
  6. Watch for keys.behind. It means a device is holding writes.
  7. Watch status.rejected and record.rejected. A refused record exists on one device and nowhere else.
  8. Never log ciphertext with its AAD context, a graph key, or a wrap.
  9. Serve over TLS from an origin you control, and keep record identifiers out of URLs the relay sees.

The relay’s database is ciphertext and wrapped keys. Back it up like any database — a stolen backup is exactly as useful to an attacker as a stolen live disk, which is the point.

What a backup cannot do is recover a member who lost every device and has no recovery bundle. Nothing can. Say so at key creation.

signal means
quota.exceeded events a graph hit a ceiling and is refusing writes
ratelimit.hit rising a client is looping, or your limits are too tight
auth.rejected rising tokens are wrong, or somebody is probing
graph.push with rejected > 0 clients are writing things the relay will not take
device status.rejected > 0 data exists on one device and nowhere else
  1. Back RateLimiter with something shared.
  2. Set sweepIntervalMs: 0 on every process and run the sweeper from exactly one job:
    import { sweepBlobs } from '@skm/relay';
    setInterval(() => void sweepBlobs({ db, directory, blobStore, onEvent }), 60_000);
  3. Point openDb at shared storage, or implement the store interfaces against your own database.

Migrations are append-only and run on openDb. Roll the relay first: a new relay serves old clients (new response fields are optional on the wire), while an old relay may not understand a new client.