# Part 12 — operating it

## 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:

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. **Treat a `log.alarm` as an incident.** See Part 13. `unverified` means
   "could not check", which is not the same as "fine".
9. **Never log ciphertext with its AAD context**, a graph key, or a wrap.
10. **Serve over TLS from an origin you control**, and keep record identifiers
    out of URLs the relay sees.

## 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

| 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

1. Swap `MemoryRateLimiter` for `SharedRateLimiter` over a shared counter.
2. Set `sweepIntervalMs: 0` on every process and run the sweeper from exactly
   one job:
   ```ts
   import { sweepBlobs } from '@argala/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.

## 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.

---
