eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1# πŸ€– Federated blob storage β€” providers contribute buckets, not trust
2
3**Status: designed 2026-07-15, Phase A ready to build.** The plan of record
4for taking attachment storage from one bucket (#550 Phase 1, the s3BlobStore
5merged as #987) to many providers β€” the storage twin of the co-op compute
6model (`docs/planet-scale.md` Part 10). Companion decisions this leans on:
7the durability ladder (`tier-economics.md Β§ Durability per tier`) and the
8E2EE rule (`README.md` / `CLAUDE.md` β€” the whole design only works because
9of it).
10
11## Why storage is the easy half of planet-scale
12
13Hibernation needed leases, heartbeats, and a mesh because a SQLite database
14is **mutable with exactly one legitimate writer** β€” every hard problem in
15that design flows from that fact. Attachment blobs are the opposite:
16**write-once ciphertext under server-minted ids**. Nothing is ever rewritten,
17so there are no conflicts, no single-writer lock, no consensus, and no
18ordering. Mirroring immutable objects is order-free and idempotent β€” a copy
19sweep can crash anywhere and rerun from the top. The distributed-systems hard
20part evaporates; what remains is sweeps and bookkeeping, which is exactly the
21shape this codebase already trusts (the meter sweep, the quota push, the
22reaper).
23
24## The trust inversion E2EE buys
25
26A storage provider holds ciphertext under opaque ids. They can read
27*nothing* β€” confidentiality is not a trust axis at all. What a provider is
28actually trusted with:
29
30- **Availability** β€” they might vanish. Answer: a second copy on a second
31 provider, plus a sweep that notices and re-mirrors. A vanished provider
32 costs a re-copy, never data.
33- **Integrity** β€” they might corrupt or withhold. Answer: every blob's
34 ciphertext hash lives in the instance's `blobs` table (recorded at
35 upload); reads can verify, and an **audit sweep** samples ranged reads per
36 provider and checks them. A cheating provider is *caught*, published, and
37 evicted β€” trust-but-verify with teeth.
38
39Two leaks remain, and the design closes both:
40
41- **Access patterns + member IPs**: a provider serving members directly
42 (presigned URLs) would learn who fetches what, when, from where. So
43 co-op-provider blobs are served **proxied through the box, never
44 presigned** β€” providers see the fleet's IP and nothing about members.
45 (First-party stores may use presigned URLs later; that's a separate,
46 deliberate choice per store.)
47- **Sizes and timing**: unavoidable for any object store, identical to what
48 the server itself already sees. Nothing new is exposed.
49
50## The model, kept boring
51
52**One wire dialect: S3.** It is the lingua franca β€” R2, Hetzner Object
53Storage, and AWS speak it natively, and MinIO/Garage turn any box with disks
54into it. A storage contribution is therefore four values: endpoint, bucket,
55key pair, region. No custom protocol, no agent, no inbound port on the
56provider's side. The s3BlobStore (#987) already consumes exactly these.
57
58**A store registry**, beside the route registry: one row per store β€” name,
59endpoint, bucket, region, provider, declared capacity, standing
60(`primary-eligible` / `mirror-only` / `draining`). Tens of rows, not
61thousands; a table a human reads whole.
62
63**Placement per space**: each space's row records which store holds its
64primary and which its mirror β€” the same per-space shape as `Route.Provider`
65(the co-op seam that has been waiting in the schema since #550 Part 10).
66Placement policy stays legible: region affinity first, capacity second,
67never more clever than a sentence can state.
68
69**Two sweeps**:
70
71- **Mirror sweep**: the `blobs` table enumerates every id a space owns; copy
72 whatever the mirror store lacks. Idempotent, restartable, journal-free β€”
73 the table *is* the journal.
74- **Audit sweep**: per store, sample a few blobs, ranged-GET a window,
75 verify against the stored hash. Probabilistic coverage over time at
76 trivial request cost β€” the same philosophy as tamper-evidence inclusion
77 sampling (#573). Results feed the status pages: provider standing is
78 public, like everything else about how this fleet runs.
79
80**Reads**: primary first, then mirror, then the local-disk fallback the
81s3BlobStore already has. A store outage degrades to slower reads, never
82errors, and the audit sweep turns persistent failures into an eviction.
83
84**Migration = copy + flip.** Because stores are dumb ciphertext buckets
85keyed by id, moving a space between providers is: mirror to the new store,
86flip the placement row, drain the old. The same no-lock-in shape as instance
87migration β€” a provider leaving the co-op is an afternoon's sweep, not an
88event.
89
90## One ladder, two knobs
91
92The durability ladder (`stream`/`batched`/`daily`) governs how fast a
93space's *messages* reach the replica. **Copy count is the same product shape
94for media**: how many providers hold a space's blobs. One member-facing
95story β€” *your plan's durability covers your messages and your media* β€” and
96plausible rungs (final numbers belong to a pricing session, not this doc):
97
98| Tier | Message replication | Blob copies |
99|---|---|---|
100| Free | daily | 1 |
101| Family / Community | daily / batched | 2 |
102| Business | stream | 2, region-diverse |
103
104## Contribution accounting
105
106A provider's contribution is meterable with nothing but the registry and the
107audit sweep: **bytes-months held Γ— audited availability**. Both numbers are
108publishable (the books, `tier-economics.md`, status pages) β€” the storage twin
109of a co-op member contributing a compute box, credited in whatever form the
110Part 10 economics settle on. No usage-secret accounting: the fleet publishes
111what it measures, providers can dispute with their own logs.
112
113**Onboarding ratchet**: a new provider's store joins as `mirror-only` and
114carries second copies through an audit burn-in (weeks, not hours). Only
115stores that survive it become primary-eligible β€” a hobbyist NAS strengthens
116the pool without ever being load-bearing alone. Standing is a ratchet the
117audit sweep can lower but only time can raise.
118
119## Phases
120
121- **Phase A β€” two stores, one owner (ready to build).** Multi-store under
122 fleet control: a `mirror` store config beside the primary, the ciphertext
123 hash column + recording at upload, the mirror sweep, read fallback
124 primary→mirror→disk, and the audit sweep's first form (verify-on-read
125 counters). Ships dark behind env like #987. This alone closes the
126 vendor-concentration exposure (Hetzner primary + the existing AWS bucket
127 as mirror) and exercises every rail federation needs.
128- **Phase B β€” the registry.** Store rows + per-space placement + placement
129 at provision (cloud push, like the replication class), migration sweep
130 (copy + flip + drain), audit results to the status pages.
131- **Phase C β€” contribution.** Provider onboarding (four values + burn-in),
132 standing lifecycle, contribution metering into the books, proxy-only
133 serving enforced for co-op stores.
134
135## Non-goals, deliberately
136
137- **DB replicas stay first-party.** Small, critical, mutable,
138 lease-guarded β€” federating them buys pennies and costs the crown jewels'
139 simplicity. Media is where the volume, cost, and federation win live.
140- **No erasure coding.** Two full copies are legible, cheap at object-store
141 prices, and degrade gracefully. Erasure across heterogeneous providers is
142 clever; the second rule (`CLAUDE.md`) says legibility wins.
143- **No DHT, no consensus, no gossip protocol for placement.** Placement is
144 rows in a registry a human reads. The fleet is tens of boxes and tens of
145 stores for the foreseeable future; designs for millions can wait for
146 millions.
147- **No custom provider daemon.** If a provider must run something, it's
148 MinIO/Garage β€” maintained by people whose whole job that is. Contribution
149 stays four values.
150
151## Decisions needed before Phase A flips live
152
1531. **Enable Hetzner Object Storage** on the fleet account (primary store;
154 ~€4.9/TB-mo, egress €1/TB, same DC as the box) β€” one console action +
155 key pair into the vault.
1562. **Confirm the mirror target**: the existing AWS account (a dedicated
157 bucket; storage-only cost, writes incur no egress) is the default
158 assumption.
1593. Pack pricing and pay-flips-paid (already on Paul's desk) β€” independent,
160 but the same pricing session should set the blob copy-count rungs above.
161