# Simplespace independent read/write access The policy split follows the September 9, 2026 lexicons ([upstream change](https://github.com/bluesky-social/atproto/commit/1ce408be0edc71778a80eb0aa744812cfcb67390)). ZDS v0.4.0 also adopts the October 1 credential, notification, and `spaceType` changes; see the [current Spaces contract](permissioned-data.md). These experimental updates intentionally remove the previous request shapes. ## Client changes - Replace `addMember({space, did})` with `putMember({space, did, read, write})`. Both flags are required booleans. This is an upsert: subsequent calls replace both permissions, including `false` values. - Replace `policy` in `createSpace` with `readPolicy` and `writePolicy`. Both are required and accept the same lexicon policy unions as before. - `updateSpace` accepts either policy independently; omitted fields stay unchanged. A supplied old `policy` field returns `InvalidRequest`, including when mixed with the new fields. - Read both policies from `getSpace`; the old `policy` output is removed. - Read `read` and `write` from each `listMembers` entry. The page default is 100, maximum 1000. The owner needs a covering read grant to enumerate members. - Managing apps receive `checkUserAccess` with `access=read` or `access=write`. Write checks omit `clientId`. Each policy can name a different managing app. For example, a public-readable space with selected writers: ```json { "spaceType": "example.shared.notes", "readPolicy": {"$type": "com.atproto.simplespace.defs#publicPolicy"}, "writePolicy": {"$type": "com.atproto.simplespace.defs#memberListPolicy"}, "appAccess": {"$type": "com.atproto.simplespace.defs#open"} } ``` Read policy governs credential issuance. Write policy governs which writers the space authority tracks and forwards notifications for. A resident can store records in their own repo even when the space denies write access; those writes do not register the writer or get forwarded by the authority. This also applies when both roles are hosted by ZDS. ## Stored data Startup migrates the previous SQLite schema in one transaction: - The previous policy and managing app become the read policy and managing app. - The write policy and managing app start with the same values. - Every existing member starts with read and write enabled. - Records, memberships, app-access configuration, and deleted-space tombstones are retained. Future restarts do not reapply the access defaults. This preserves previous access while removing compatibility code from the API. Old clients must update; no legacy route or single-policy translation remains. The policy-schema migration alone does not change credential expiry. The v0.4.0 credential cutover does: clients must reacquire credentials and replace Spaces DPoP with HTTP message signatures. OAuth DPoP is unchanged. Before deploying, take a consistent database backup and volume snapshot, rehearse startup on a disposable copy, and compare the account, policy, membership, and record state. A binary rollback cannot safely represent independently changed permissions using the old schema. ## Validation `just test` covers migration preservation and repeat execution, member upserts, and independent policy decisions. `just smoke-permissioned` exercises read-only, write-only, neither, and both permissions through credential issuance and actual record writes, plus old payload rejection and partial policy updates. Use `just bench space 1000` and `just bench space 10000` for the storage baseline. These are single-caller storage measurements; they do not measure remote managing-app latency. Use `just bench http` separately for the public HTTP paths.