# 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](https://docs.thinkingmachinelabs.dev/argala/project/changelog.md).

## 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. `oldest` on the wire says where the history now starts, and both
  cases are handled honestly — a device that had already folded past the prune
  point reports `unverified`, 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

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

**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.checkpoint` is 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

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

- **A React binding.** Deliberately absent: `useArgala` is 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 `BroadcastChannel` for the followers. Either
  is a day's work and neither is done.

## 6. Operational

- **A rate limiter that survives a restart.** `SharedRateLimiter` now 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

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.** `Directory` is 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.
