Roadmap
This roadmap lists what is missing, in priority order, and what each item would actually require. Nothing here is scheduled; it is the honest backlog a platform should publish instead of a feature grid.
Things that used to be on this list and are now built — partial sync, signed records, per-device keys, object-store blobs, reactive queries, native stores — are described in CHANGELOG.md.
1. Rollback and omission — the rest of it
Section titled “1. Rollback and omission — the rest of it”Built (see CHANGELOG.md): the relay keeps an append-only log of every write it accepted; devices check it against what they were actually served, fold it into a chain, and publish signed checkpoints that catch a relay serving two histories.
What remains:
- The all-worlds relay. A relay that shows every member a completely separate world, including their peers’ checkpoints, is still invisible in-band. Detection requires out-of-band comparison — either a head two people can read to each other or a third party that pins checkpoints — and neither exists.
- Partial sync cannot verify. A filtered subscriber sees a subset by design and cannot distinguish that subset from one chosen by the relay. Today, the check simply turns off. A chain over the filtered projection could be checked against other devices using the same filter, but that is a substantial design in its own right.
- Nobody audits an idle graph. Detection needs a device that syncs, folds and compares. A graph nobody has opened in a month is unverified, and says so, but no process proactively audits it.
- Log retention has no policy. It grows by about sixty bytes per write
forever.
oldeston the wire says where the history now starts, and both cases are handled honestly — a device that had already folded past the prune point reportsunverified, and a device linked afterwards anchors its chain at the new start, keeps checking from there, and stops corroborating anybody (status.graphs[g].log.from). What is missing is the prune API itself, guidance on when to use one, and the wire field that would let two devices with the SAME anchor corroborate each other again instead of both going silent.
2. Key transparency
Section titled “2. Key transparency”Today: a device is gated rather than trusted. Every wrap of a graph key
passes an approval gate that fingerprints the published key triple; a device
is approved without a prompt only when it has proven possession (it wrote a
record this device opened and verified, and its selfSig binds its wrapping
key to the same signing key), and otherwise a human compares a twelve-character
pairing code. A blocked device is named in excluding so it cannot deadlock a
rotation.
What is still missing: the gate ends at a human, and people may not compare the code carefully. The roster itself has no cryptographic consistency guarantee: a malicious relay can still show one roster to one member and a different roster to another. The only detection mechanism is someone noticing an unfamiliar device.
The shape: an append-only log of device registrations that clients can audit, plus safety numbers a pair of members can compare out of band. The weaker, much cheaper first step — worth doing regardless — is a signed roster digest each client checks for consistency across sessions and across members, so a relay that shows different rosters becomes detectable without a human in the loop.
Also missing, and smaller: linking: 'approve' is opt-in for one release
because an older relay drops excluding and would refuse the rotation as
incomplete. Flipping the default is a breaking change gated on relay uptake,
not on more design.
3. Cryptographic write control
Section titled “3. Cryptographic write control”Today: the relay enforces two capabilities from the host app’s directory —
write and rotate — which gives a real read-only seat and admin-only
removal, because a push and an epoch bump are operations the relay mediates
and can therefore refuse. See Part 15 of the guide.
What is still missing: finer-grained control. A member with write can
overwrite or delete another member’s records and cannot be restricted by kind
or shard, because deciding either would require the relay to read a record,
which it cannot and must never do. Those decisions can only be made on a device
that holds the key, which makes them non-recognition after the fact rather than
prevention.
The shape: a signed policy, delivered alongside the epoch key rather than as a record, naming which kinds each member may write and which records are theirs; devices apply it when reading. Earlier designs exposed four constraints:
- Never reject on the sync path. A reject advances the cursor past an op the relay will never serve again, so the record exists on one device and nowhere else — permanent divergence, silently. Enforcement has to be non-recognition at READ time, leaving the stored bytes identical everywhere.
- Never deliver policy as a record. The relay ANDs the kind and shard filters, so a shard-filtered subscriber can never receive one, at any cursor, ever. It has to ride the keys route, which is unfiltered.
- Exempt reserved kinds. A role that could withhold
skm.checkpointis a role that can switch off the relay tamper detection. - Genesis cannot be adoptable by arrival order, or the first caller seizes a permanent founder seat on an existing graph.
The hard part is not the check itself, but what happens when the policy is contested. That is fundamentally a governance problem, not a cryptographic one.
4. Search that scales
Section titled “4. Search that scales”Today: LIKE %term% over the record JSON. Correct, escaped, and linear.
The shape: FTS5 in the SQL stores, requiring a small virtual table and trigger, and something else entirely for IndexedDB — probably an inverted index maintained on write. Both are device-local and derived, which is the good news: derived state cannot be corrupted by a concurrent write, and can be rebuilt from the records at any time.
5. Ergonomics not yet earned
Section titled “5. Ergonomics not yet earned”- A React binding. Deliberately absent:
useArgalais easy to write badly, and the version that re-renders on every change is worse than nothing.watch()is the primitive it should be built on. - A published test relay. The 200-line in-process relay in this repository’s test helpers should become a package so apps can test against a real relay without a process.
- Batch key ceremonies for ROTATION. Grants are batched by epoch now, so linking a device into a workspace that has rotated costs one request per epoch rather than one per (device, epoch). Rotation still wraps every device in a single request by necessity — the relay refuses an incomplete one — so a graph with more than a few hundred devices will need the completeness check to accept a rotation across several requests, which it currently cannot.
- Storage pressure beyond noticing. The client now asks for persistence
and reports
status.storage, including the one combination that loses data (unsynced writes on an evictable store). What it does not do is act: no backpressure when a graph approaches the quota, and no guidance on what an app should drop first. - One engine per browser, not per tab. OPFS takes exclusive handles, so a
second tab silently falls back to IndexedDB and the two tabs stop sharing a
local store. The fix is a SharedWorker holding the engine with tabs talking
to it over a port — the same shape as the OPFS worker, one level up — or a
Web Locks leader election with
BroadcastChannelfor the followers. Either is a day’s work and neither is done.
6. Operational
Section titled “6. Operational”- A rate limiter that survives a restart.
SharedRateLimiternow holds one allowance across processes over any counter you can increment and read. What it cannot survive is the counter itself disappearing: a Redis restart gives every caller a fresh allowance. That is the safer failure direction, but it is still worth stating explicitly. - Relay-side backup and restore guidance. The relay holds ciphertext, so a backup is uninteresting to steal and vital to have — but nothing here says how to take one consistently from a WAL-mode SQLite file under load.
- A migration path for
WRAP_INFO. Apps adopting argala with existing wrapped keys pass their old HKDF label; there is no tooling to re-wrap onto the current one, which is what they would eventually want.
Non-goals
Section titled “Non-goals”Stated so nobody plans around them:
- A CRDT engine. LWW at a granularity the app chooses covers the cases this is built for. Real collaborative text editing wants Yjs or Automerge, and those compose with argala at the blob or record level rather than replacing it.
- Server-side query, search, or AI over member data. Structurally impossible here, and that is the point of the product.
- Owning your users.
Directoryis the seam. argala will never grow a user table you are expected to migrate onto. - Hiding metadata the relay must have to route. Sealed sender and mixnets are real answers to a real problem, and they are a different project.