Part 12 — operating it
The nine rules
Section titled “The nine rules”From SECURITY.md, restated because they are the difference between an app that is end-to-end encrypted and one that says it is:
- 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.
- Declare your kinds. Without a registry, a member with the key can write anything into every other member’s store.
- Leave
requireSignatureson. Turning it off makesbya claim again. - Show the device roster. A ghost device is only detectable if somebody can see it.
- Rotate on removal, immediately, and check both
failedandowed. - Watch for
keys.behind. It means a device is holding writes. - Watch
status.rejectedandrecord.rejected. A refused record exists on one device and nowhere else. - Never log ciphertext with its AAD context, a graph key, or a wrap.
- Serve over TLS from an origin you control, and keep record identifiers out of URLs the relay sees.
Backups
Section titled “Backups”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.
Monitoring
Section titled “Monitoring”| 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 |
Scaling out
Section titled “Scaling out”- Back
RateLimiterwith something shared. - Set
sweepIntervalMs: 0on every process and run the sweeper from exactly one job:import { sweepBlobs } from '@skm/relay';setInterval(() => void sweepBlobs({ db, directory, blobStore, onEvent }), 60_000); - Point
openDbat shared storage, or implement the store interfaces against your own database.
Upgrading
Section titled “Upgrading”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.