33 KiB
ATProto Permissioned Data (Research Notes)
STATUS: EXTERNAL RESEARCH. Last reviewed 2026-08-03.
This describes work happening in the upstream
bluesky-social/atprotoproject, not in ATCR. Nothing here is implemented in this repository, and nothing here is a commitment that ATCR will adopt it. The upstream spec is itself explicitly labeled "a proposal, not the final specification" and its reference implementation is an open, WIP pull request. Details, terminology, and wire formats are all expected to change.This document exists so we can track the design as it stabilizes and reason about what, if anything, it means for ATCR.
Sources
| Source | What it is | Date |
|---|---|---|
| Proposal 0016 Permissioned Data | The spec of record. Supersedes the diary posts wherever they disagree. | current |
atproto#5187 permissioned-data |
Reference implementation. Marked "⚠️ WIP ⚠️". 167 files, +12,178/−732. | updated 2026-07-31 |
| Discourse discussion thread | Community forum discussion of the proposal. | ongoing |
Design log by Daniel Holmgren, oldest first. Useful for why, unreliable for what: see What changed since the diaries.
| Post | Date |
|---|---|
| Diary 1: To Encrypt or Not to Encrypt | 2026-02-11 |
| Diary 2: Buckets | 2026-02-26 |
| Diary 3: Your Bucket, My Data | 2026-03-12 |
| Interlude: Spaces | 2026-03-17 |
| Diary 4: The Big Picture | 2026-03-20 |
| Diary 5: What's in a Name? | 2026-05-08 |
| Modeling communities on permissioned data | 2026-06-02 |
| Diary 6: Boring Auth | 2026-06-05 |
| Diary 7: Off the Record | 2026-07-17 |
Referenced external specs: LtHash, CAR, DRISL, TLS 1.3 §3.4, atproto permission spec.
Summary
Atproto today is a protocol for public broadcast: signed, redistributable, universally addressable records that anyone can crawl. Permissioned data adds a second data protocol for records with an access perimeter, targeting personal data (bookmarks, mutes, drafts), gated content (paid newsletters), socially shared content (private posts, stories), and groups (private forums, group chats).
It deliberately keeps the same abstract shape as public broadcast: DID-based authority, per-user repositories, lexicon-typed records, applications crawling PDSes to build views. What changes is the repository format, the sync mechanism, the addressing scheme, and the resolution path.
The single most important framing: this provides access control, not confidentiality. It is explicitly not end-to-end encrypted. Servers (both PDSes and authorized applications) read plaintext, which is required for search, indexing, notifications, aggregation, and moderation. E2EE is left as an app-layer concern.
| Public broadcast | Permissioned data | |
|---|---|---|
| Unit of data | Record in a repo | Record in a permissioned repo |
| Repo scope | One repo per user | One permissioned repo per (user, space) |
| Record authority | User DID | User DID |
| URI authority | User DID | Space authority DID |
| Commit | Merkle Search Tree root | LtHash set-hash digest |
| Signature | Rebroadcastable, archival | Deniable on rebroadcast |
| Addressing | at:// URI |
at:// URI with space segment |
| Access | Public | Gated by space credential |
| Distribution | Relay firehose | Direct pull from each repo host |
Core model
Spaces
A space is an authorization and sync boundary representing a shared social context. It is not a storage location. Each participant stores their own records for that space in a permissioned repo on their own PDS. A space is the aggregation of those per-user repos across the network: an application pulls each member's repo from its host, assembles the view, and applies access control to its own users.
A space is identified by a triple:
- space authority — a DID, the root of authority
- space type — an NSID naming the modality
- space key (
skey) — a string distinguishing spaces of the same type under the same authority (max 512 bytes,rkeysyntax)
The authority may be a user's own DID (personal data: bookmarks, mutes) or a dedicated DID, which lets a shared space transfer between owners without breaking backlinks.
Spaces scale from one user's bookmarks to communities of millions.
Terminology
| Term | Meaning |
|---|---|
| Space | Authorization + sync boundary, identified by (authority, type, skey) |
| Permissioned repo | One user's records within one space, on their repo host |
| Repo host | Service storing and serving users' permissioned repos |
| Space host | Service answering for a space as a whole (credentials, writer set, notification routing) |
| Space authority | The DID at the root of a space |
| Space credential | Token from the authority granting read access to a space |
| Delegation token | Token from a user's PDS, exchanged for a space credential |
| Client attestation | Token signed by an app's own key, proving app identity |
| Syncer | Application keeping its own copy of a space in sync |
A PDS fills both the repo-host and space-host roles, but they are specified separately because neither has to be a PDS.
Addressing
Permissioned data reuses at:// with a fixed literal space marker where a
collection NSID would sit:
Space: at://{spaceDid}/space/{spaceType}/{skey}
Record: at://{spaceDid}/space/{spaceType}/{skey}/{authorDid}/{collection}/{rkey}
Never ambiguous with a public URI: a collection NSID always contains at least
two dots, space contains none. The ordering is SPACE/author/collection/rkey
rather than author-first because user authority derives from space membership,
and because a space reference is then a clean prefix of a record URI.
The POC modifies packages/syntax/src/aturi.ts and
packages/syntax/src/aturi_validation.ts accordingly.
Space type declarations
A space type NSID resolves to a new kind of Lexicon definition,
"type": "space", which must be the main def:
{
"lexicon": 1,
"id": "com.atmoboards.forum",
"defs": {
"main": {
"type": "space",
"description": "A discussion forum",
"key": "any",
"name": "AtmoBoards Forum",
"name:lang": { "es": "Foro AtmoBoards", "ja": "AtmoBoards 掲示板" },
"collections": ["com.atmoboards.thread", "com.atmoboards.reply"]
}
}
}
| Field | Required | Purpose |
|---|---|---|
type: "space" |
yes | Marks the declaration |
key |
yes | Recommended skey type |
name (1–64) |
yes | User-facing, shown on OAuth consent screens |
name:lang |
no | Localized names |
collections |
yes | Default collection set for a space: scope of this type |
description |
no | Developer-facing only |
collections is a default, not a constraint: any collection may be written to
any space at the protocol level.
DID document entries
A space authority is resolved through two optional DID doc entries:
- verification method
#atproto_space— key verifying the space's credentials - service
#atproto_space_host— the space host endpoint
When absent they fall back to #atproto and #atproto_pds respectively, so an
authority hosted on an ordinary PDS needs no extra DID doc entries at all.
Access control
Reading a space requires a space credential issued by the space authority. The authority decides based on two independent axes, and the protocol does not define the decision procedure — that is the concern of a space-management implementation layered above it (see simplespace).
Three token classes
All three share a wire shape and are implemented as one parameterized signer in
packages/space/src/credential.ts.
| Token | typ |
Signed by | Lifetime | aud |
Reuse |
|---|---|---|---|---|---|
| Delegation token | atproto-space-delegation+jwt |
user (kid: #atproto) |
60s | space host | single-use |
| Client attestation | atproto-client-attestation+jwt |
app's client-auth key | 60s | space host | single-use |
| Space credential | atproto-space-credential+jwt |
space authority | 2h | none | multi-use |
Delegation token. Minted by the user's PDS via
com.atproto.space.getDelegationToken. Asserts only the user-to-app
delegation, and says nothing about whether the user is a member of the space.
Structurally close to a service auth token but a distinct credential class: no
lxm claim, bound to a target space through sub.
Client attestation. Structurally a private_key_jwt client assertion, the
same shape a confidential client already presents to its authorization server,
but addressed to the space authority. iss and sub are both the client_id.
The authority verifies it by resolving client_id → client-metadata.json →
JWKS → the key named by kid. Required only when a space gates on app
identity, so public clients still work against #open spaces.
Space credential. Deliberately has no aud: one credential is
presented to every repo host serving a repo in the space, and any host verifies
it against the authority's published key without contacting the authority.
Credential flow
┌──────┐ ┌────────────┐ ┌─────────────┐ ┌─────────────────┐
│ User │ │ User's PDS │ │ Application │ │ Space Authority │
└───┬──┘ └──────┬─────┘ └──────┬──────┘ └────────┬────────┘
│ │ │ │
├── OAuth consent ───► │ │
│ ├───── OAuth token ─────► │
│ │ │ │
│ ◄─ getDelegationToken ──┤ │
│ ├── delegation token ───► │
│ │ │ getSpaceCredential │
│ │ ├─(token [+ attestation])─►│
│ │ ◄──── space credential ────┤
The two tokens are presented together but signed by different parties and evaluated independently. An application serving many users of a space needs only one credential, obtainable via any single user's session; when it loses every OAuth session for that space it can no longer renew and loses access.
Repo format
Commit digest: LtHash
No Merkle Search Tree. Permissioned repos sync as complete units bounded by space membership, so the MST's partial-proof machinery buys nothing.
The digest is LtHash, a homomorphic set hash built on a lattice problem (hence quantum-secure):
- State is a fixed 2048-byte buffer read as 1024 little-endian u16 lanes
- Each record maps to the element
{collection}/{rkey}/{record_cid} - Add: expand the element to 2048 bytes with BLAKE3 in XOF mode, read as 1024 LE u16 lanes, add lane-wise mod 2^16
- Remove: identical, but subtract
- Empty repo state is all zeroes
- Commit carries only
sha256(state); the host keeps the full 2048-byte state
Both operations commute, so the state depends only on the current record set, never on write order. Adding or removing a record is one cheap operation rather than a recomputation, so hosts maintain the state incrementally on each write. Two repos holding the same records always produce the same digest.
Implementation: packages/space/src/lthash.ts (~80 lines, @noble/hashes,
two views over one ArrayBuffer so Uint16Array arithmetic wraps mod 2^16 for
free).
Deniable commit signatures
A signature over the content digest would be a rebroadcastable proof of what a user wrote in a private space. So the signature covers only random per-reader bytes, and the digest is bound to those bytes by a symmetric MAC.
Both are domain-separated by a shared context string using the TLS 1.3 §3.4 variable-length vector encoding:
ctx = "atproto-space-v1" // fixed protocol tag
|| uint16be(len(space)) || space // space URI
|| uint16be(len(author)) || author // author DID of the repo
|| uint16be(len(rev)) || rev // commit revision (TID)
|| uint16be(len(ikm)) || ikm // per-signature nonce
Producing a commit:
- Generate
ikm, 32 fresh random bytes. A newikmper reader served. sig = sign(ctx)with the user's signing key. The signed message contains space, author, rev, and ikm, but not the repository hash.mac = HMAC-SHA256(HKDF-SHA256(ikm, ctx), hash)
A reader verifies sig (authenticity), then recomputes mac (integrity), and
from there trusts hash. But because the MAC key derives from the public
ikm, anyone holding a leaked commit can compute a valid MAC for any hash at
all. A rebroadcast commit therefore proves only that the user signed a
(space, author, rev, ikm) context, never what they wrote. A leak becomes a
social and reputational matter rather than a cryptographically provable one.
com.atproto.space.defs#signedCommit:
| Field | Type | Description |
|---|---|---|
ver |
integer | Commit format version, currently 1 |
hash |
bytes | sha256 of the LtHash state (32 bytes) |
ikm |
bytes | Per-signature nonce (32 random bytes) |
sig |
bytes | sign(ctx) by the user's signing key |
mac |
bytes | HMAC-SHA256(HKDF-SHA256(ikm, ctx), hash) |
rev |
string | Commit revision (TID), also bound into ctx |
Byte-order gotcha the spec calls out explicitly: ctx length prefixes are
big-endian (TLS convention), LtHash lanes are little-endian (LtHash
reference construction). Each keeps its native order.
Implementation: packages/space/src/repo-commit.ts; hmacSha256 and
hkdfSha256 were added to @atproto/crypto.
CAR serialization
A permissioned repo serializes to a CAR file with two roots, in order:
- the signed commit block
- the index — a DRISL (DAG-CBOR) map from
"{collection}/{rkey}"to the record CID, keys in lexicographic order
Record blocks follow, in the same order as their index entries. Blobs are
excluded and fetched separately via getBlob.
This layout lets a consumer stream-verify in three stages with no buffering:
- Verify the commit's signature and MAC.
hashis now trusted. - Fold each index entry's
{collection}/{rkey}/{cid}into a running set hash as it is read, compare againsthash. This authenticates the entire index without reading a single record. - Verify each record block against its own CID as it streams past.
Implementation: packages/space/src/sync/provider.ts (serialize),
packages/space/src/sync/consumer.ts (streaming verify).
Sync
There is no relay. Permissioned repos are non-rebroadcastable by nature, so there is no collated firehose. Applications pull directly from each repo host and are responsible for keeping their own copy in sync.
Incremental sync
A syncer keeps its own copy of a repo plus its own running set hash over that copy. Agreement with the host's digest means exactly up to date; disagreement means behind or corrupted.
com.atproto.space.listRepoOps(since) returns operations after a revision.
Each entry is { rev, collection, rkey, cid, prev }:
cidnull → deleteprevnull → create- entries sharing a
rev→ one atomic multi-record write
Record values are inlined by default, so a syncer advances in a single call
with no per-write getRecord round-trip. Only the current value for a path is
inlined; superseded intermediates are omitted. excludeValues gives
metadata-only entries.
If a response includes the last available operation, it must also carry the member's current signed commit. The syncer compares it against its running set hash: match means fully in sync and authenticated; mismatch means diverged, fall back to recovery.
Sync is self-healing because correctness rests on the set-hash comparison rather than on receiving every operation. A missed op is detected on a later sync. Total history disjunction is likewise detectable.
The oplog is a transport optimization, not a committed data structure. Its
contents and history are not guaranteed; a host may compact or drop it, keeping
only a backfill window, and it resets on account migration since a new host
starts fresh. A syncer that cannot find its since falls back to full-state
recovery, which does not depend on the oplog.
Full-state recovery
Fetch the whole repo as a serialized CAR from com.atproto.space.getRepo,
verify as described above, rebuild locally. A syncer replacing an existing copy
diffs and keeps only what it is missing.
For healing a slightly diverged copy there is a lighter path: getLatestCommit
for the commit, listRecords with excludeValues for the paths → CIDs
structure, diff locally, then getRecord only the differences. Trades one
round-trip for a smaller total transfer.
Write notifications
Best-effort, carrying no record data, stating only that a repo reached a new revision.
A syncer registers via com.atproto.space.registerNotify — against the space
host for the whole space (the normal case), or against a specific repo host for
individual repos. Authenticated with a space credential; the registration
expiry may outlive the credential's.
When a member writes, their PDS sends com.atproto.space.notifyWrite to each
registered endpoint. Since a PDS generally does not know who is syncing the
space, the space authority registers itself as a subscriber on each repo
host and forwards notifications to syncers registered with it. Repo hosts
auto-register the authority's #atproto_space_host endpoint on the first
write into a shared space, so no out-of-band setup is needed. Personal-data
spaces, where the authority is the account's own DID, need none of this.
Dropped notifications are recovered by a later write's notification or by a
periodic sweep: listRepos returns each repo's current rev, so a syncer
re-syncs only repos that have advanced.
The writer set
To sync a space in full, or to start syncing one, an app needs the set of
accounts holding data in it. That writer set comes from
com.atproto.space.listRepos on the space authority, with each repo's current
rev and hash.
Important scoping:
- It enumerates accounts that have written at least one record, not accounts merely allowed to write (which the authority may not track).
- Readers are never enumerated at the protocol level. Apps that want a reader list must build one from records published in the space.
- It is what the authority claims, current only as far as notifications have kept it. The repo host is source of truth; a syncer confirms each repo by syncing it directly.
OAuth scopes
space:<spaceType>[?authority=<did>][&skey=<skey>][&collection=<nsid>...][&action=<action>...][&manage=<op>...]
| Parameter | Multiple | Default | Values |
|---|---|---|---|
spaceType (positional, required) |
no | — | space-type NSID, or * |
authority |
no | self |
authority DID, self, or * |
skey |
no | * |
space key (1–512 chars), or * |
collection |
yes | the type declaration's collections |
collection NSID, or * |
action |
yes | read, create, update, delete |
read_self, read, create, update, delete |
manage |
yes | (none) | create, update, delete |
authority/spaceType/skey select which spaces; action (+ collection)
govern operations on records; manage governs operations on the space
itself and is empty by default, so an ordinary access grant confers no admin
capability.
Points worth internalizing:
authoritydefaults toself. A barespace:com.example.bookmarkscovers only the user's own spaces. Reaching a forum anchored elsewhere requires naming the authority orauthority=*.readis all-or-nothing at the space boundary. There is no partial, per-record, or per-author whole-space read grant. It confers both the read and sync methods on the holder's own PDS and access togetDelegationToken.read_selfconfers the same methods but only for the holder's own repo, and notgetDelegationToken. This is the grant for a backup or export tool that must not see other members' posts.readimpliesread_self.- A space credential grants whole-space read/sync directly, so read and sync methods accept either a covering OAuth scope or a credential. Write methods accept only OAuth, since a write is attributed to the authoring user.
collectiondefaults dynamically. It resolves from the space type declaration as it stands when the grant is evaluated, not frozen at consent time, following permission-set semantics. If the declaration later adds a collection, existing bare grants widen. Apps that don't want that must enumerate collections explicitly.- When
spaceTypeis*there is no declaration to draw from, so the default collection set is empty and the grant confers no write targets. - In a permission set,
spaceTypemay not be a wildcard. A cross-type grant is expressible only as a standalone requested scope.
Consent screens show the declaration's name in place of the raw NSID, and a
specified authority as its bidirectionally-linked handle (or the raw DID if
none validates). Wildcards on both authority and spaceType must be presented
with a prominent warning.
Examples:
space:com.example.bookmarks
the user's own bookmarks space, read + write to declared collections
space:com.atmoboards.forum?authority=*
every forum the user is in, any authority; the typical forum-client grant
space:com.atmoboards.forum?authority=*&action=read_self&collection=*
the user's own posts only, across all forums; backup/export tools
space:com.atmoboards.forum?authority=*&action=read_self&manage=update&manage=delete
administer the user's forums without record-write access
space:*?authority=did:plc:abc123
every space under one authority, any type
simplespace
The protocol does not specify how spaces are created or how an authority
decides who may read them. com.atproto.simplespace is the space-management
implementation every PDS MUST support — not privileged, just guaranteed
present, so applications have a baseline available on every account. Spaces are
anchored on a user's own DID.
Other space types may define their own management implementations and are full protocol participants, but they run on bespoke space services rather than on the PDS.
Methods (all called with an OAuth credential carrying the relevant manage
scope): createSpace, updateSpace, deleteSpace, addMember,
removeMember, listMembers.
Configuration
| Field | Values | Decides |
|---|---|---|
policy |
member-list (default) | public | managing-app |
whether to authorize a user |
appAccess |
#open (default) | #allowList |
whether to authorize an app |
managingApp |
service identifier (DID + fragment) | request routing and the managing-app check target |
Both must pass for a credential to be minted, and a valid delegation token is required regardless.
#open requires no client attestation, so public clients work. #allowList is
evaluated against the attested client_id (the iss of a verified client
attestation), making it enforceable rather than advisory.
The managing app
Under managing-app policy, at mint time the authority calls
com.atproto.simplespace.checkUserAccess on the space's managingApp — served
by the app, not the PDS — with itself as iss and the app's service identifier
as aud, passing the space, the requesting user, and the attested client_id.
This is the escape hatch for dynamic policy: follower-gating, paid-subscription
status, join approvals, anything the app tracks, without the app maintaining a
literal member list. Any account with space access can read the managingApp
from getSpace, so it also serves as a routing target for app-level requests
(join requests and similar) a generic PDS cannot handle.
XRPC surface
All under com.atproto.space. Roles: repo methods concern one account's
repo (repo host), host methods concern a space as a whole (space host),
pds methods are the required PDS baseline, syncer methods are
notification delivery. One service is usually several roles at once.
| Method | Role | Type | Auth |
|---|---|---|---|
getSpace |
host | query | space credential |
getSpaceCredential |
host | procedure | delegation token (+ attestation) |
listRepos |
host | query | space credential |
getRecord |
repo | query | OAuth / space credential |
listRecords |
repo | query | OAuth / space credential |
getBlob |
repo | query | OAuth / space credential |
getLatestCommit |
repo | query | OAuth / space credential |
getRepo |
repo | query | OAuth / space credential |
listRepoOps |
repo | query | OAuth / space credential |
getDelegationToken |
pds | query | OAuth |
createRecord |
pds | procedure | OAuth |
putRecord |
pds | procedure | OAuth |
deleteRecord |
pds | procedure | OAuth |
applyWrites |
pds | procedure | OAuth |
listSpaces |
pds | query | OAuth |
registerNotify |
repo/host | procedure | space credential |
notifyWrite |
syncer/host | procedure | service auth |
notifySpaceDeleted |
syncer/repo | procedure | service auth |
Space deletion
An authority stops issuing credentials, stops answering for the space, deletes
its own repo in it, then notifies registered syncers and known repo hosts via
notifySpaceDeleted over the same best-effort path as write notifications.
The two sides behave differently, deliberately:
- A syncer should delete every copy it holds, pulled repos and derived state alike, since it is no longer authorized to retain them.
- A repo host should flag the member's repo as belonging to a deleted space rather than erase it. The data is the user's own, so the host decides how to surface it (export, grace period) before garbage-collecting.
Cross-cutting concerns
Moderation. The model is unchanged except that a moderation service cannot
observe a space it has not been admitted to; it is just another reader and needs
a space credential like anyone else. Labelers should not publish public
labels for permissioned records, since that leaks metadata about private data.
com.atproto.label.subscribeLabels is a poor fit; labels should instead be
records in a permissioned repo inside the same access boundary. The authority
gains one lever with no public analogue: it can decline to issue a credential
and thereby cut off read access entirely.
Scaling. Similar to public broadcast (partitioned per user, no node holds a whole space), with the absence of relays as the one real difference, placing load directly on PDSes. Three mitigations: the sync protocol is lighter than public sync with no MST structural nodes; load scales with the number of applications syncing a space, not end users, since an app pulls each repo once and fans out from its own copy; and the sync APIs are credentialed rather than public, so the syncer set is closed, correlatable, and rate-limitable.
Account lifecycle. Tied to the same DID and signing key as the public
identity, so migration, key rotation, deactivation, and deletion all work the
same way. The wrinkle is that a user has many permissioned repos rather than
one, so migration flows must enumerate and track all of them plus their blobs.
Notably, an application syncing only permissioned repos still needs a
firehose subscription to receive #account and #identity events. A
subscription carrying only those two event types is listed as future work.
Implementation status (atproto#5187)
Present in the branch:
| Area | Files |
|---|---|
New @atproto/space package |
lthash.ts, repo-commit.ts, credential.ts, sync/provider.ts, sync/consumer.ts, types.ts, util.ts, error.ts + tests for each |
| Lexicons | 17 under com/atproto/space/, 8 under com/atproto/simplespace/ |
| PDS API | all com.atproto.space.* and com.atproto.simplespace.* handlers |
| PDS storage | actor-store/space/{reader,transactor}.ts, migration 002-space.ts |
| PDS auth | client-attestation-verifier.ts, auth-verifier.ts, auth-output.ts |
| OAuth | scopes/space-permission.ts, scope-set + transition changes, provider lexicon manager |
| Consent UI | oauth-provider-ui consent form/view, scope-description.tsx, 6 locale files |
| Lexicon tooling | "type": "space" support across lex-document, lex-schema, lex-builder, lex-installer |
| Syntax | at:// space-URI parsing and validation |
| Crypto | hmacSha256, hkdfSha256 |
| Common | CAR read/write stream helpers |
| Dev env | bin-multi-pds.ts, network-no-appview.ts, introspect.ts |
| Tests | spaces.test.ts, space-scope.test.ts, client-attestation.test.ts |
The PDS actor-store schema is a useful summary of what a repo host tracks:
space uri PK, isOwner, policy, managingApp,
appAccessType, appAllowed, createdAt, deletedAt
space_member (space, did) PK -- host-internal list
space_record (space, collection, rkey) PK, cid, value, repoRev
space_repo space PK, setHash (raw 2048B blob), rev
space_record_oplog (space, rev, idx) PK, action, collection, rkey,
cid, prev -- append-only
space_writer (space, did) PK, rev, hash -- the writer set
space_credential_recipient (space, serviceDid) PK, serviceEndpoint,
lastIssuedAt -- registerNotify
Not present:
- No syncer or appview implementation. The consumer-side verification
library exists but nothing in-tree consumes it. Testing is currently PDS↔PDS
via
network-no-appview, which is what the new dev-env harnesses are for. - No relay work, and no
#account/#identity-only subscription. - No
@atproto/apiclient surface and no Bluesky product surface. Nothing here is wired to a shipping app yet.
What changed since the diaries
Read the diary posts for reasoning, not for wire details. These were reversed:
| Diary said | Proposal says |
|---|---|
A separate ats:// scheme (Diary 5, argued at length) |
at:// with a literal space segment |
| ECMH, elliptic-curve multiset hash (Diary 4) | LtHash, lattice-based |
A protocol-level member list of (DID, read|write) tuples as the "narrow waist" (Diary 4, 6) |
The protocol carries no member list; membership is a simplespace implementation detail and the mint decision is undefined at protocol level |
| "Buckets" | "Spaces" (renamed in the Mar 17 interlude; "bucket" wrongly implied colocation). The Diary 2 concept is otherwise intact |
| Asymmetric signatures over repo state | Deniable HMAC-bound commits (Diary 7) |
| Space credentials "~2-4 hour expiration" | 2 hours by default |
Stable across the whole series:
- No E2EE at the protocol layer (Diary 1, never revisited)
- Partitioned rather than colocated storage — settled in Diary 3 on storage burden, moderation liability, availability coupling, and trust grounds
- Pull-based sync, no relay
- Write permissions are not protocol-enforced. Anyone can write to their own repo; readers validate against space rules, the same way apps validate replies on public posts
- Per-modality spaces rather than one universal community container. The June 2 post rejects universal spaces on two grounds: consent screens become illegible ("your Open Communities" vs "your AtmoBoards forums"), and access boundaries land in the wrong place (an events app would get chat, photos, and forum posts too). A community is instead several typed spaces under one DID, with governance provided by services layered on top rather than by the protocol
Open questions
Things the proposal leaves undefined or flags as future work:
- The
#account/#identity-only subscription that permissioned-only apps need - How space discovery works for a user across authorities beyond
listSpaceson their own PDS - Migration flows enumerating many permissioned repos and their blobs
- Whether anything outside
simplespacematerializes, and what a bespoke space service looks like in practice - Oplog retention windows and compaction policy (host-defined today)
- Non-normative status of the whole "Considerations" section