spec: account ids are sequential — 40-bit random gave 9-char sqids
Paul's call after seeing kass.nyfbskdye.oncarlos.com. Durability comes from minted-once-into-the-bucket, not from randomness, so sequential is exactly as safe as random here — the original bug was the REGISTRY assigning by listing order. Also corrects the anti-enumeration rationale: Certificate Transparency publishes every minted hostname, so sqids were never secret; the only real disclosure is the account count.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+20
−3
docs/superpowers/specs/2026-08-02-multi-account-design.md+20 −3
diff --git a/docs/superpowers/specs/2026-08-02-multi-account-design.md b/docs/superpowers/specs/2026-08-02-multi-account-design.md| index 35d935064..171053d21 100644 |
| --- a/docs/superpowers/specs/2026-08-02-multi-account-design.md |
| +++ b/docs/superpowers/specs/2026-08-02-multi-account-design.md |
| @@ -13,7 +13,12 @@ in §8 — which is what currently blocks enabling `PLATFORM_APPS_DOMAIN`. |
| An **account** is `{id, sqid, name, created_at}`: |
| -- **`id`** — a random 40-bit integer minted at creation. Not sequential. |
| +- **`id`** — a **sequential integer** minted at creation (next free id; |
| + `1` is reserved for ops). Corrected 2026-08-02 after seeing the result: |
| + 40-bit random ids encode to NINE-character sqids |
| + (`kass.nyfbskdye.oncarlos.com`), which is not a URL anyone can read or |
| + type. Sequential ids keep them short and grow the length linearly with |
| + account count, with no cap. |
| - **`sqid`** — the frozen encoder (`internal/domains`) over that id; the |
| account's public identity, appearing in every platform domain |
| (`kass.<sqid>.oncarlos.com`) and every bucket key. |
| @@ -53,8 +58,20 @@ it into the bucket**, at `accounts/<sqid>/account.json`: |
| The registry only ever *materializes* what the bucket says — it never |
| assigns. The bucket is the control plane; identity belongs there like |
| -everything else. Randomness also means sqids are **not enumerable**: you |
| -cannot walk `bab`, `bac`, `bad` to discover who else is on a deployment. |
| +everything else. **Durability comes from minted-once-and-recorded, not |
| +from randomness** — which is why sequential ids are safe here and the |
| +original AUTOINCREMENT approach was not: that one let the *registry* |
| +assign in bucket-listing order, so a rebuild reassigned them. |
| + |
| +Minting must use a **create-only write**, retrying with the next id on |
| +conflict, so two concurrent minters can never claim the same integer. |
| + |
| +Sequential ids are enumerable, and that is accepted: every platform |
| +domain gets a Let's Encrypt certificate, and Certificate Transparency |
| +publishes those hostnames publicly regardless of how the sqid is |
| +generated — so sqids were never secret. The only genuine disclosure is |
| +the rough account count, which for a platform that publishes a fleet |
| +status page is not a secret worth an unreadable URL. |
| `ops` keeps the fixed id `1` (sqid `bab`), created by migration on every |
| deployment — so a console-less self-hoster is unaffected and every app |