bluesky-social/proposals
Bluesky proposal discussions. Contribute to bluesky-social/proposals development by creating an account on GitHub.
178 stars · 18 forks
README
0016 Permissioned Data This is a proposal, not the final specification. Details, terminology, and behaviors are all likely to change. For discussion, head to the community forum. For a friendlier introduction to the problem space and deeper analysis of design decisions made along the way, see the permissioned data diary blog posts. Current drafts of lexicons can be viewed on the implementation branch in the atproto repo. Note that the implementation itself is still very much a work in progress. Introduction AT Protocol (atproto) is a foundation for building social applications on shared, user-owned data. While commonly referred to as a "protocol", atproto is actually a framework composed of several distinct protocols as well as a schema language. Atproto currently includes a data repository and sync protocol for public broadcast data. In the public broadcast protocol, users publish records into a repository on their PDS, and applications crawl those repositories to build views. Records are signed, redistributable, and universally addressable, with authority resting in the DID that publishes a record. This document specifies an additional data protocol for permissioned data, or data with an access perimeter. The permissioned data protocol serves modalities such as:
Personal data: bookmarks, mutes, drafts Gated content: paid newsletters, subscriber-only posts Socially shared: private posts, stories Groups: private forums, communities, group chats
The permissioned data protocol shares the abstract shape of public broadcast. It retains DID-based authority, per-user repositories, lexicon-typed records, and the general flow of applications crawling PDSes to build views. However it has its own repository format, sync mechanism, addressing scheme, and resolution path. Public broadcast is built for open distribution (signed, archival, rebroadcastable) while the permissioned data protocol is built for party-to-party transmission within an access boundary. The permissioned data protocol provides access control, not confidentiality. It is not end-to-end encrypted. Services (both PDSes and authorized applications) can read the data they handle, which is required for server-side features such as search, indexing, notifications, aggregation, and moderation. E2EE is a separate concern that may be layered on top by an application and is out of scope in this proposal. Relationship to public broadcast
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 Traditional at:// URI at:// URI with space segment
Access Public Gated by space credential
Terminology
Space: an authorization and sync boundary for a set of permissioned records, identified by an (authority, type, skey) triple. Permissioned repo: one user's records within one space, with a cryptographic commit, hosted on that user's PDS. Repo host: a service that stores and serves users' permissioned repos. Space host: a service that answers for a space as a whole, issuing credentials, enumerating writers, and routing notifications. Space authority: the DID at the root of a space, which resolves to the space host and the key material for issuing credentials. Space credential: a token issued by the space authority that grants read access to a space. Delegation token: a token issued by a user's PDS that an application exchanges with a space authority for a space credential. Client attestation: a token signed by an application's own client authentication key, proving the application's identity to a space authority. Required only when a space gates on client app identity. Syncer: an application that keeps its own copy of a space in sync by pulling from repo hosts.
A PDS fulfills both the roles of a repo host and a space host. However, these roles are discussed separately because they do not necessarily need to be filled by a PDS. A permissioned repo or a space may be hosted by any service that implements the required APIs. Spaces A space is an authorization and sync boundary representing a shared social context. A space may include many different types of records from many users. The space does not colocate records on a single network host. Instead, each user stores their own records for a given space in a permissioned repo on their own repo host. A space is the aggregation of these per-user repos across the network: an application presenting a space pulls each member's repo from its host, assembles the view, and applies access control to requesting users. Each space is identified by three values:
space authority: a DID, the root of authority for the space space type: an NSID describing the modality of the space space key (skey): a string distinguishing spaces of the same space type under the same space authority
Reading or syncing a space requires a space credential signed by the declared signing key of the space authority. The space authority decides whether to issue one based on the requesting user and client application. The protocol does not define how that decision is made and carries no member list (see Access Control). Spaces scale from a single user's personal data (e.g. bookmarks) to communities of millions of users. Addressing A permissioned record is addressed by an at:// URI with a fixed space segment: at://{spaceDid}/space/{spaceType}/{skey}/{authorDid}/{collection}/{rkey}
Component Type Description
spaceDid DID Space authority DID
space literal Fixed marker identifying this as a permissioned space URI
spaceType NSID Space type
skey string Space key
authorDid DID DID of the record's author
collection NSID Record collection
rkey string Record key
Permissioned data reuses the at:// scheme rather than defining its own. The literal space segment sits where a collection NSID appears in a public atproto URI, so a permissioned URI is distinguished from a public one by that marker in the first path segment under the authority DID. The two are never ambiguous, as a public collection is an NSID, which always contains at least two .s, whereas the space marker contains none. All segments through rkey are necessary to identify a permissioned record. The leading segments through skey may be used to reference a space: Space: at://{spaceDid}/space/{spaceType}/{skey} Record: at://{spaceDid}/space/{spaceType}/{skey}/{authorDid}/{collection}/{rkey}
Space authority A space's authority is the DID at the root of the space and the issuer of its credentials. It may be a user's own DID as for personal data such as bookmarks or mutes. Or it may be a dedicated DID which lets a shared space transfer between users independently of any individual account. A space authority is resolved through two entries in its DID document:
a verification method with id #atproto_space: the public key used to verify the space's credentials a service entry with id #atproto_space_host: the endpoint of the space host
Both entries are optional. When #atproto_space is absent, the space signing key falls back to the account's #atproto signing key. Similarly, when #atproto_space_host is absent, the space host falls back to the account's #atproto_pds service endpoint. An authority MAY instead publish the dedicated entries to point at distinct key material or a distinct host, and MAY set them to the same values as #atproto and #atproto_pds explicitly. Space type A space's type is an NSID that names its modality and resolves to a space type declaration. It identifies the kind of data a space holds before any network resolution, much as a collection NSID does in public atproto. Because a type names a concrete modality, every space is some specific kind of space rather than a generic container. The type is also the OAuth consent boundary. Access is granted to a user by type, e.g. "access to your AtmoBoards forums" (see OAuth scopes). Space type declarations A space type NSID resolves to a space type declaration: a Lexicon definition with "type": "space". { "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 Type Required Description
type "space" yes Marks this as a space type declaration. Must be the main definition.
description string no Description of the space type for developers. Not shown to users.
key string yes Specifies the recommended space key type (similar to record key types)
name string (1–64) yes Human-readable name for the space type, shown to users on consent screens.
name:lang map<lang, string> no Localized name values by language code.
collections array of NSID yes Collections clients should expect in a space of this type.
The collections field is the default collection set for a space: scope of this type. However, ultimately any collection may be written to any space and is not constrained at the protocol level by the collections in a space type declaration. Space key (skey) The space key (skey) is a string distinguishing spaces of the same type under the same authority, analogous to a record key (rkey): a slug, a TID, a cryptographic identifier, or a reserved string such as self. Maximum length 512 bytes with the same syntax requirements as an rkey. Access control Reading a space is gated by a space credential issued by the space authority. The authority issues one based on two axes:
which user is being acted for: established by a delegation token minted by the user's PDS which application is acting: established by a client attestation signed by the application itself
The delegation token is always required. The client attestation is required only when a space gates on client app identity. An application obtains a credential by getting a delegation token from a user's PDS, then presenting that token (together with its client attestation, if needed) to the space authority in exchange for a credential. The authority decides whether to issue the credential. The protocol does not define the decision procedure (the policies of the PDS's space-management implementation are described under simplespace). Some spaces do not require a client attestation. This can be detected by querying the space configuration, or by simply making a request for a credential without an attestation and seeing whether an error is returned. Delegation token A delegation token proves that an application is acting on a user's behalf when it asks an authority for a space credential. The user's PDS mints it, requested through com.atproto.space.getDelegationToken. It asserts only the delegation from user to application. Whether the user is a member of the space is the authority's determination, and the delegation token says nothing about it. The token is single-use, short-lived (default 60 seconds), and addressed (aud) to the space authority. A client app session can only request a delegation token for a space it has been authorized to access. The application must hold a covering space: OAuth scope (specifically a read grant, which confers access to getDelegationToken). A delegation token is structurally similar to an atproto service auth token, but it differs in a few ways that make it its own credential class rather than an interchangeable one:
The typ field in the header is set to atproto-space-delegation+jwt. It does not include an lxm claim. It is bound to a target space through the sub claim.
Example JWT header and payload (before base64url encoding and signing): { "typ": "atproto-space-delegation+jwt", "alg": "ES256K", // or ES256 "kid": "#atproto" // Key Identifier - MUST be "#atproto" } { "iss": "did:example:user_did", // User DID "sub": "at://did:example:space_did/space/com.example.space_type/space_key", // Space being requested "aud": "did:example:space_did#atproto_space_host", // Space host (service fragment of the authority DID) "iat": 1738368000, // Issued-at (unix seconds) "exp": 1738368060, // iat + 60 (60 seconds) "jti": "f47ac10b58cc4372a5670e02b2c3d479" // random nonce } The delegation token asserts only the user-to-app delegation; it says nothing about which application is acting. App identity is established independently by the client attestation, which the authority verifies itself. The two are presented together but signed by different parties and evaluated independently. The signature for the delegation token is computed using the regular JWT process, using the account's signing key. For more details, see the Inter-Service Authentication section of the AT Protocol spec. Client attestation A client attestation is a short-lived, single-use JWT that the application presents to the space authority alongside the delegation token to identify itself, when the space requires it. It is structurally a private_key_jwt client assertion, the same shape an atproto confidential client already presents to its authorization server, but addressed to the space authority rather than to the PDS. { "typ": "atproto-client-attestation+jwt", "alg": "ES256", "kid": "key-1" // Key id from the client's published JWKS } { "iss": "https://app.example.com/client-metadata.json", // The client_id "sub": "https://app.example.com/client-metadata.json", // The client_id (== iss) "aud": "did:example:space_did#atproto_space_host", // Space host being asked for a credential "iat": 1738368000, // Issued-at (unix seconds) "exp": 1738368060, // short-lived "jti": "b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8" // random nonce, replay protection } The authority verifies the attestation by resolving iss (the client_id) to the client's client-metadata.json, fetching its published JWKS (jwks or jwks_uri), and verifying the signature against the key in that JWKS identified by the attestation's kid. Space credential A space credential is the token an application presents to a repo host to read a permissioned repo within a space. The authority mints it in exchange for a delegation token, requested through com.atproto.space.getSpaceCredential. It is short-lived (default 2 hours) and signed by the space authority's signing key, so any repo host can verify it against the authority's key without contacting the authority. A space credential is multi-use. A single credential is intended to be reused across every repo host serving a repo in the space as well as against a given host for repeated requests, until it expires. A space credential resembles a space delegation token, differing in these ways:
The typ field in the header is set to atproto-space-credential+jwt. It is signed by the space authority rather than the user. It has no aud: it is presented to any repo host serving a repo in the space, not to a single recipient.
Example JWT header and payload (before base64url encoding and signing): { "typ": "atproto-space-credential+jwt", "alg": "ES256K", // or ES256 "kid": "#atproto_space" // Key Identifier - "#atproto_space" or "#atproto" } { "iss": "did:example:space_did", // Space authority DID "sub": "at://did:example:space_did/space/com.example.space_type/space_key", // Space the credential reads "iat": 1738368000, // Issued-at (unix seconds) "exp": 1738375200, // iat + 7200 (2 hours) "jti": "9f8e7d6c5b4a3210fedcba9876543210" // random nonce } The signature for the space credential is computed using the regular JWT process, using the space authority's signing key. Credential flow ┌──────┐ ┌────────────┐ ┌─────────────┐ ┌─────────────────┐ │ User │ │ User's PDS │ │ Application │ │ Space Authority │ └───┬──┘ └──────┬─────┘ └──────┬──────┘ └────────┬────────┘ │ │ │ │ ├── OAuth consent ───► │ │ │ ├───── OAuth token ─────► │ │ │ │ │ │ ◄─ getDelegationToken ──┤ │ │ ├── delegation token ───► │ │ │ │ getSpaceCredential │ │ │ ├─(token [+ attestation])─►│ │ │ ◄──── space credential ────┤
The user authorizes the application via OAuth. The application calls com.atproto.space.getDelegationToken on the user's PDS, receiving a delegation token. The application presents the delegation token to the space authority via com.atproto.space.getSpaceCredential, adding its own client attestation if the space gates on client app identity. The authority verifies what it received and, on authorization, returns a space credential. The application reads the repo from each member's repo host with the credential.
An application serving several users of a space does not necessarily need to maintain a space credential for each user. It may obtain its credential using any one user's session. When it loses all OAuth sessions for a space, it can no longer renew the credential and loses access. Permissioned repos A permissioned repo is one account's set of records within one space, generally stored on that user's own PDS. A user has one permissioned repo per space it participates in. Abstractly, a permissioned repo offers a similar interface to a public atproto repository: a key/value mapping where keys are path names (a collection NSID and an rkey) and values are CBOR-encoded records. PDSes expose a CRUD interface for interacting with a user's permissioned repos. Each permissioned repo is summarized by a commit. A commit is a short, signed digest that allows a syncer to check whether its copy of a repo matches the source without resyncing it in its entirety. Commit digest The digest in a commit is computed from a set hash over the records a repo currently contains. It is independent of write/delete order, so two repos with the same records always produce the same digest. Adding or removing a record is a single cheap operation, so a repo host maintains the set hash incrementally on each write. The construction is LtHash, a homomorphic hash built on a lattice problem (and thus quantum-secure). A record is added to or removed from the digest with a single addition or subtraction rather than a recomputation over the whole repo. The state is a fixed 2048-byte buffer, interpreted as 1024 little-endian unsigned 16-bit lanes. Each record in the repo maps to one element, the UTF-8 bytes of {collection}/{rkey}/{record_cid}. As with the public repository, the components are currently limited to ASCII, so the encoding (though specified as UTF-8) is a no-op. To add an element:
Expand the element to 2048 bytes with BLAKE3 in XOF (extendable-output) mode. Read those bytes as 1024 little-endian uint16 lanes. Add each lane into the corresponding state lane, modulo 65536 (2^16, i.e. with wraparound)
To remove an element, perform the same process, but subtract its lanes instead (modulo 65536). Both operations are commutative, so the state depends only on the current set of records, not the order of writes. The empty repo's state is all zeroes. The commit's hash is sha256(state), a 32-byte digest of the 2048-byte buffer. The repo host maintains the full state. Only the hash is carried in a commit. Commit signature A user does not sign the digest directly since a signature over the content digest would be a rebroadcastable proof of what the user wrote