Skip to content

feat: shared spaces for shared cars - #77

Merged
leviFrosty merged 1 commit into
mainfrom
shared-spaces
Oct 31, 2026
Merged

leviFrosty merged 1 commit into
mainfrom
shared-spaces

Conversation

@leviFrosty

@leviFrosty leviFrosty commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds Shared Spaces (/spaces/v1): a blind, end-to-end encrypted record store shared by up to 10 members. The app's Shared Cars feature uses it, so a household can keep one car's mileage together. The decision is ADR 0037 in the app repo, and the wire contract is the app's docs/shared-cars-protocol.md.

What the server holds. Each space is one SharedSpace Durable Object holding sealed records under opaque ids, with a per-space seq. The server keeps the version with the newest stamp; a deletion wins a tie, then the lower blob hash.

  • Members: a per-space memberId that commits to the member's X25519 key, and a hash of their token. Accounts are never recorded against a space; the bearer session only gates suspension and rate limits.
  • What it can't see: what a space is, who is on it, or any record's contents.

Ops: create, sync, put, invite/create, invite/delete, join, leave, member/remove, rekey, delete, plus a live WebSocket that sends hello and changed.

Rules that keep a blind server honest about membership:

  • Join: the server checks that memberId matches dhPub on create and join, and a fresh join must carry the current epoch.
  • Reserved ids: m.<memberId> is writable only by that member, and k.<x> (the key chain) only by the owner.
  • rekey: stores its key chain link in the same transaction, replacing any planted copy.
  • Removal: member/remove closes every open invite, so a removed member can't rejoin through someone else's invitation before the rekey.
  • Invite ids: a closed, used, or burned invite id never opens again.

Limits:

  • 10 members, counting open invites (KV spaces:maxMembers); 50,000 records and 256 MiB per space; 16 KB per record; 100 records per put.
  • A persisted rolling 60 puts a minute per member, and 10,000 records a day per space.
  • 4 live sockets per allowed member, with fair eviction.
  • Retention: at least 400 days without a sync or put.

Kill switch: spaces:enabled / SPACES_ENABLED. While it's off, leave, member/remove, and delete still work, so people can always get off a car.

Buddies:

  • car.invite becomes an immediate push kind.
  • sync paging moved to a shared chunked reader (src/durableObjectStorage.ts). It never leaves a SQLite cursor open; an open cursor made DROP TABLE fail with SQLITE_LOCKED in Spaces.
  • deleteAllThen and a shared rolling-window helper replace duplicated code. Buddies' behavior is unchanged.

Config: the SHARED_SPACE binding and migration v10 are added for both prod and env.dev. Sentry drops x-ww-space-auth, x-ww-space-token, and the internal x-ww-space-member-token.

Verification

  • Checks: pnpm typecheck passes, and pnpm test passes 102 files, 1,676 tests.
  • e2e: Spaces and Buddies e2e against a local worker pass 23 of 23. pnpm fuzz:buddies --runs 300 passes.
  • Black-box verification: two rounds against a running worker.
    • The first round ran 356 checks and found the SQLITE_LOCKED bug, now fixed with a regression e2e.
    • The second ran 315 checks on this head and passed everything:
      • join epochs and retries;
      • reserved ids;
      • closed invite ids;
      • removal closing every invite;
      • rekey with its link;
      • delete and leave after a page cut by the 4 MiB budget;
      • live headers and socket fairness;
      • last-writer-wins, the kill switch, and suspension.
    • No secret (member tokens, join secrets, wraps, blobs) appeared in wrangler.log.
  • Interop: the app's sharedCars.interop.test.ts passes 8 of 8 against this branch. It covers a car's whole life with three people, two rekeys, and another device opening the car with an empty keyring.
  • Devices: an iOS simulator, an Android emulator, and a headless third person ran against this branch:
    • share, invite, and join across platforms;
    • conflicting edits, where the newest wins;
    • removal and rekey, after which the remaining member keeps writing;
    • leaving with a copy, and the last one left.
  • Review: three independent review rounds; every finding is fixed in this commit.

The app's half, witness-work#1227, needs this on main first.

Blind encrypted record spaces at /spaces/v1, one SharedSpace Durable
Object per space, behind SPACES_ENABLED and KV spaces:enabled. Alert
car.invite at once in Buddies.
@leviFrosty
leviFrosty merged commit 2966034 into main Oct 31, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant