Multi-tenant
What this layer does
Phase 14 carves the substrate into tiers: public canonical (everyone pulls), per-tenant private state (only the owning tenant), client work, and subscriber-tier content. The Worker derives tenant_id from SNAPPY_MASTER_KEY on every request and enforces the tier boundary server-side. Cross-tenant PID still works because the aggregate is anonymized and gated by quorum.
Files involved
state/bin/sync/tenant-id.sh— derive 12-char tenant ID from
SNAPPY_MASTER_KEY.
state/bin/invite-mint.ts— mint per-tenant invite codes,
single-use, 7-day TTL.
~/projects/snappy-skills/src/auth.ts— WorkercanAccessFile()
derives tenant from key, checks tier grants.
s3://robert-storage/snappy-os-meta/tenants/<tenant_id>.json—
per-tenant tier grants (public always; personal/client/subscriber per row).
~/projects/snappy-skills/src/quorum.ts— cross-tenant PID
promotion.
Tier model
| Tier | Prefix | Access |
|---|---|---|
| Public | s3://robert-storage/snappy-os/ | All tenants pull, only canonical author writes |
| Public kernel | s3://robert-storage/snappy-kernel/ | Same |
| Personal | s3://robert-storage/snappy-os-tenants/<tenant_id>/ | Only owning tenant reads + writes |
| Client | s3://robert-storage/snappy-os-clients/<client_slug>/ | Only granted tenants per tenants/<id>.json |
| Subscriber | s3://robert-storage/snappy-os-subscriber/ | Only tenants with subscriber: true grant |
| Staging | s3://robert-storage/snappy-os-staging/<skill>/<rev_id>/ | Public read, write per quorum logic |
tenant_id derivation
tenant_id = sha256(SNAPPY_MASTER_KEY).first(12_hex)
Worker computes on every request from the auth header. Client-supplied tenant_id is never trusted. Loss of SNAPPY_MASTER_KEY means loss of tenant identity; recovery is via Robert-minted single-use invite code that re-binds the new key to the prior tenant_id (manual approval).
Cross-tenant PID
Per Phase 7: anonymized aggregate (evals.aggregate.ndjson) ships to the public-tier prefix. Quorum logic reads aggregates across tenants and promotes a staged rev when ≥3 tenants score ≥0.85 across ≥5 runs. No tenant sees another tenant's raw evals — only the rolled-up tenant ID hash.
Operational gotchas
- Tenant prefix is server-derived. A client that hand-crafts a
tenant_id field in the request body is ignored; Worker recomputes from the auth header on every write.
- Public-tier reads work without a key. The catalog, detail pages,
docs, status, and changelog are all reachable anonymously.
- Tier upgrades require Robert. There is no self-serve "upgrade me to
subscriber" flow in v1 — the tenants/<id>.json file is updated by Robert on confirmed payment / acceptance.
- Invite codes are single-use, 7-day TTL, stored in Worker KV
INVITES. After redemption they're deleted; expired codes return 410 Gone.
- The catalog generator MUST honor tier grants. A subscriber-only
skill should not appear in the public catalog row even though its bytes live in the same bucket.
How to verify it's working
- A request with no auth header reads public-tier files but is
rejected on _push.
- A request with auth Key A cannot read Key B's personal-tier files
(HTTP 403, no body).
state/bin/sync/tenant-id.shreturns a stable 12-hex string for a
given key.
- An invite code minted by
invite-mint.tsworks exactly once;
second use returns 410.
- Quorum promotion across 3 distinct tenant aggregates triggers a
public-tier write within 60s.