Portable Web Spaces v0.5

Unofficial Draft

More details about this document
Latest published version:
https://w3c-ccg.github.io/wallet-attached-storage-spec/
Latest editor's draft:
https://w3c-ccg.github.io/portable-web-spaces-spec/
History:
Commit history
Editor:
Dmitri Zagidulin
Feedback:
GitHub w3c-ccg/portable-web-spaces-spec (pull requests, new issue, open issues)

Abstract

Portable Web Spaces is a general purpose permissioned storage API.

Status of This Document

This document is a draft of a potential specification. It has no official standing of any kind and does not represent the support or consensus of any standards organization.

This is an experimental specification and is undergoing regular revisions.

1. Conformance

As well as sections marked as non-normative, all authoring guidelines, diagrams, examples, and notes in this specification are non-normative. Everything else in this specification is normative.

The key words MAY, MUST, MUST NOT, NOT RECOMMENDED, OPTIONAL, RECOMMENDED, REQUIRED, and SHOULD in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.

2. Introduction

The Portable Web Spaces (PWS) specification brings together the lessons learned from many attempts to standardize permissioned cloud storage over the years.

This specification aims to provide:

Note
This document is deliberately comprehensive: it specifies both a small required core and a set of optional extensions. A conformant minimal server implements only Resource CRUD (12. Resources and Blobs) and the baseline authorization profile (5. Authorization, [PWS-AUTHZ]); every other endpoint group is OPTIONAL. See 2.4 Scope and Conformance Profiles for the full conformance-tier map, and 2.5 Quickstart: Your First Request to watch a first request succeed.

2.1 Version History and Roadmap

Note
This subsection is non-normative.
  • v0.1 (January 2025) -- Initial version, created for the MIT Digital Credentials Consortium in collaboration with Benjamin Goering, as the "Wallet Attached Storage" specification.
  • v0.2 (April 2026) -- Added examples and editorial fixes. Snapshot at https://wallet.storage/spec.
  • v0.3 (through June 2026) -- Initial incubation at MIT DCC, based on implementation experience.
  • v0.4 (mid-July 2026 - September 2026) -- Migrated to the W3C Credentials Community Group (CCG)'s Capability Based Storage Task Force for further incubation. Goal: updating spec to match deployments and learnings.
  • v0.5 (September 2026 - now) -- Breaking changes, refactoring, community input. Renamed from Wallet Attached Storage to Portable Web Spaces. Breaking changes so far:
    • The description of a container moved to its meta sub-resource at every level, and a Collection's description merged with its Metadata object. GET/PUT at /space/{space_id}/meta read and write the 10.1 Space Metadata Data Model; GET/PUT at /space/{space_id}/{collection_id}/meta read and write the 11.1 Collection Metadata Data Model, which now carries the members the Collection Description used to hold. The term "Collection Description" is retired.
    • A container URL is canonically written with a trailing slash, and a trailing slash no longer distinguishes two operations (see 2.2 Reading This Document).
    • The Space is an ordinary container: GET /space/{space_id}/ lists its Collections and POST /space/{space_id}/ creates one. The /space/{space_id}/collections/ endpoint is retired; its segment stays reserved.
    • A method a server does not implement at a reserved endpoint answers 405 Method Not Allowed (see E.4 Methods at Reserved Endpoints). This replaces the earlier choice between 405 and a reserved-id (409) error at the Collection meta path. A PUT at a container URL is now a MUST 405 rather than a SHOULD.
    • An update of a Collection's Metadata object that omits backend keeps the stored backend selection, rather than resetting it to the default.
    • A Collection's generator is now an object with members id, origin, url, and name, replacing the flat generator and generatorOrigin members. An update that omits it keeps the stored value.
    • Create Resource can no longer respond with reserved-id, or with id-conflict for an existing id. The server generates the Resource id there, and a client that chooses one uses Update (or Create by Id) Resource. That operation can no longer respond with reserved-id either, since a reserved segment in the Resource position is a reserved endpoint (see E.4 Methods at Reserved Endpoints).
    • A server publishes a service description listing every specification version it speaks, found through a Link: <...>; rel="service" header on every response (see 8. Service Description). Serving it is REQUIRED, and a response without the link identifies a v0.4 server.
    • Delete Space, Update (or Create by Id) Space, Delete Collection, and Update (or Create by Id) Collection now require a root capability invoked directly by the Space's controller, with exactly two named exceptions (see #the-container-rule). Delegated capabilities previously authorized a Delete Collection carrying the DELETE action, and a Delete Space or Update Space carrying any zcap scoped to the Space; both are now refused outside the exceptions.
    • A Backend description no longer carries a features array; conditional writes and key-epoch stamping are now baseline server requirements rather than backend-advertised affordances. changes-query is now advertised server-wide in the service description's features array rather than per backend. PWS-EC's optional affordances (blinded-index-query and governed-history-logs) are now tokens of its own version entry rather than backend features, and serving the chunk endpoints is now a plain requirement of PWS-EC conformance rather than a token at all.
    • The zCap authorization profile and the PublicCanRead policy type moved to a companion specification, [PWS-AUTHZ], with wire behavior unchanged. This document keeps the contract every authorization profile meets (see 5. Authorization) and gains a C. Policy Type Registry. The signatureAlgorithms and zcapCryptosuites members moved from this specification's version entry to the profile's own entry under https://w3id.org/pws/authz-profile, which a server now lists.
    • The Chunked Resources section and the EDV-over-PWS Profile appendix moved to the companion specification [PWS-EC], with wire behavior unchanged. Core keeps the chunks reserved path segment and the edv Encryption Scheme Registry entry's server-visible structural validation.
    • The changes query profile's checkpoint is an opaque string scoped to the server and Collection that issued it, replacing the { id, updatedAt } object. The feed is ordered by a per-Collection feed position the server assigns, no longer by (updatedAt, id), and each feed entry carries its own checkpoint. A server rejects a retired object checkpoint, and the replica restarts its pull from the beginning (see G.1 changes profile).

No stored data moves across the v0.4-to-v0.5 path changes. The durable artifacts to audit are capabilities. A delegated capability whose invocationTarget was a v0.4 description URL with a PUT action has no meaning under the v0.5 layout and needs re-delegation against the meta URL, while a capability on a container URL keeps its prefix coverage unchanged. A server MAY serve the two route tables together during a transition, listing both versions in its service description.

2.2 Reading This Document

Note
This subsection is non-normative. It collects conventions that the rest of the document relies on, so that a section read in isolation is still intelligible.

Authorization: ... is a placeholder. Every request example that carries an Authorization header abbreviates a signed zCap (capability) invocation (as opposed to a bearer token). Reads are authorized in PWS just as writes are. The expanded form -- with the Digest, Capability-Invocation, and Signature headers -- appears once, in the worked example of [PWS-AUTHZ].

Every path has one canonical form, and a trailing slash marks a container. A container -- a Space or a Collection -- is canonically written with a trailing slash, and everything else without one. So /space/{space_id}/{collection_id}/ is the Collection, and /space/{space_id}/{collection_id}/{resource_id} is a Resource in it. The two forms of a path name the same thing. A request to the non-canonical form is not a different operation, and a server SHOULD answer it with a 308 Permanent Redirect to the canonical form. No two paths defined by this specification differ only by a trailing slash. Because a signed capability invocation covers the full request URL (see Capability Invocation in [PWS-AUTHZ]), a client that follows such a redirect MUST sign again for the new target rather than replay its Authorization header; sending the canonical form to begin with avoids the round trip.

A container's description lives at its meta sub-resource. GET on a container lists its members and POST adds one, at every level: a Space's members are its Collections, a Collection's are its Resources. What the container is -- its name, its configuration -- is a separate document one segment down, at meta: 10.1 Space Metadata Data Model at /space/{space_id}/meta and 11.1 Collection Metadata Data Model at /space/{space_id}/{collection_id}/meta. The same split holds for a Resource, whose content is at its own URL and whose 12.8 Resource Metadata Data Model is at meta beneath it. Reading a description is therefore always a GET of meta. The one exception is the server itself, which has no container URL: its description is the service description, found by following the service link every response carries (see 8. Service Description).

Paths are relative to a server root, which may be a subpath. Every path in this document, /space/{space_id}/... and /spaces/, is written from the server's root. That root is not necessarily an origin: a server MAY be mounted under a path such as https://example.com/pws/. A client cannot derive the root from a URL it holds, which is why the service description is linked rather than placed at a fixed path, and why every URL the service description carries is absolute.

All examples share one Space. Every example in this document uses the Space id 81246131-69a4-45ab-9bff-9c946b59cf2e on the host example.com. Path segments in braces -- {space_id}, {collection_id}, {resource_id} -- are placeholders for those identifiers.

Normative lists live in the appendices. Error type URIs are catalogued in H. Error Type Registry, path segments this specification reserves in E. Reserved Path Segment Registry, access control policy types in C. Policy Type Registry, and client-side encryption schemes in F. Encryption Scheme Registry. Those registries are normative: they, not the surrounding prose, are where an implementation looks such values up.

2.3 Use Cases

Initial use cases that are motivating this work:

2.4 Scope and Conformance Profiles

This specification represents a layered and modular approach to storage, combining core features and optional extension points.

The layers below describe conformance tiers. Each tier adds optional capability on top of the one before it, so an implementer can stop at any tier and still be conformant. The normative body, by contrast, is organized container-first (outermost to innermost: Spaces Repositories, Spaces, Collections, then Resources), which is convenient as a reference but is the reverse of the tiers. If you're new to the spec, scan the profile table below for the conformance tiers, then walk through 2.5 Quickstart: Your First Request to watch a request succeed. The core tier is just 12. Resources and Blobs plus the baseline authorization profile (5. Authorization, [PWS-AUTHZ]).

Profile Adds Defining sections Advertised as (see 8.2 Service Description Data Model)
Minimal Resource CRUD (KV + blob read/write) + authorization + the service description 12. Resources and Blobs, 5. Authorization with [PWS-AUTHZ], 8. Service Description the version entry itself, plus the [PWS-AUTHZ] entry
+ Listing list resources / collections / spaces 11.5 List Collection operation, 10.5 List All Collections operation, 9.2 List Spaces Operation listing
+ Collection mgmt create / manage collections in a Space 11. Collections collection-management
+ Space mgmt manage an individual Space 10.2 Read Space operation (Space endpoints) space-management
+ Multi-tenant create / manage many Spaces on a server 9. Spaces Repositories the spaces URL member
+ Extensions linksets, policy, metadata, export, backends, query, quotas, encryption, changes-query, created-by 13. Linksets one features token per extension, named as listed

Each tier stacks on the one above it. The Minimal profile is a permissioned key/value CRUD API built from simple HTTP verbs and delegatable capability-based authorization -- enough, on its own, to read and write any resource (text, structured document, or binary blob) without collection or space management -- plus the service description through which a client learns which versions and tiers the server speaks. Listing, Collection and Space management will feel familiar to anyone who has used a GUI front end for a database or file system. Multi-tenant support lets a provider host many Spaces on one server. The Extensions tier layers on optional features -- an access-policy resource, user-writable metadata (for example, "tags" on binary files), a Space export endpoint, pluggable 14. Backends, query, quotas, and the server-side validation that the encrypted-collection profile [PWS-EC] builds on -- advertised server-wide in the service description's features array and, per Space, through the linkset feature-detection mechanism (from [RFC9264]; see 13. Linksets).

Normative status. Section back-placement does not imply informative status. Everything from 2. Introduction through 15. Quotas is normative, as are the appendices A. Pagination, B. Space Controller DID Method Registry, C. Policy Type Registry, E. Reserved Path Segment Registry, F. Encryption Scheme Registry, and H. Error Type Registry (each of which also carries an inline "This appendix is normative." banner). The remaining appendices -- I. Goals and Requirements and J. IANA Considerations -- are informative. Optionality is orthogonal to normativity: many normative sections describe OPTIONAL endpoint groups, but a server that implements them MUST follow the stated requirements.

2.5 Quickstart: Your First Request

Note
This walkthrough is a non-normative tutorial, not a conformance requirement. It assumes a server that already hosts a single Space as well as a messages collection, and that you are that Space's controller. The goal is to store one JSON Resource and read it back.

Step 1: Write. PUT a JSON document to a resource path under a collection. A 204 No Content confirms the write:

PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/hello-world HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...

{"message":"hi"}
HTTP/1.1 204 No Content

Step 2: Read. GET the same path to retrieve what you just stored:

GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/hello-world HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...
HTTP/1.1 200 OK
Content-type: application/json

{"message":"hi"}

(The Authorization: ... placeholder stands for a signed zCap invocation). PWS reads are authorized too, so the header is required on the GET just as on the PUT. How that invocation is constructed is defined by the baseline authorization profile, [PWS-AUTHZ], and its fully expanded form -- with the Digest, Capability-Invocation, and Signature headers spelled out -- is shown once in the worked example there. Producing that signature requires a conformant PWS client: it is an Ed25519 did:key capability invocation, not something you can hand-write with curl.

To go beyond a single pre-existing Space -- to create your own Space, or delegate access to others -- see the conformance-profile table in 2.4 Scope and Conformance Profiles and the full endpoint map in 2.6 API Summary.

2.6 API Summary

API summary at a glance.

Unless otherwise specified by the controller, all operations require authorization. This can be overridden by the controller via the (optional) /policy endpoints. Hosting "public-read" resources, such as HTML files for websites, or media files you can link to via <img src="">, is a common use case.

The endpoints below divide into a Core profile that every conformant server implements, and Optional extensions grouped by feature. Each row links to the section that defines its operation. Rows marked Reserved name a path this specification anchors (see E. Reserved Path Segment Registry) but does not yet define an operation for; a server MUST NOT repurpose a reserved path. A method the server does not implement at a reserved endpoint is answered with 405 Method Not Allowed; see E.4 Methods at Reserved Endpoints.

2.6.1 Core

Service Description:

Resource CRUD (Create, Read, Update, Delete):

2.6.2 Optional extensions

List resources in a Collection:

If not implemented on a server, implies that only individual Key/Value operations are supported.

Manage Collections in a Space:

If not implemented on a server, implies that collections are pre-configured or implicit, controlled by the server.

Spaces Repository Endpoints -- Manage Spaces on a Server:

If not implemented on a server, implies that any existing Spaces are pre-configured and controlled by the server.

Space Endpoints -- Manage an individual Space:

  • GET /space/{space_id}/meta -- 10.2 Read Space operation
  • PUT /space/{space_id}/meta -- 10.3 Update (or Create by Id) Space operation
  • DELETE /space/{space_id}/ -- 10.4 Delete Space operation
  • POST /space/{space_id}/export -- Reserved / not yet specified. Export (download) a Space's contents (all collections and resources). When specified, an exported archive carries the exporting server's Service Description (see 8. Service Description) verbatim, as a top-level service.json beside the archive's manifest, so that an importer can read the specification version and the feature set the contents were written under before it writes anything.
  • POST /space/{space_id}/import -- Reserved / not yet specified. Import (upload) a previously exported Space archive (the operation the registered invalid-import error kind anticipates). When specified, import MUST restore an archived Space Metadata object's user-writable members (see 10.1 Space Metadata Data Model) only under an invocation of the Space's root capability, MUST skip them under a delegated chain, and MUST report which of the two it did. It MUST NOT restore server-derived members.
Note
The per-Space archive manifest written by the reference implementation names section anchors of this document as the specification of its entries, so those anchors are stable identifiers: 10. Spaces, 11.1 Collection Metadata Data Model, 12.2 Resource Data Model, 12.8 Resource Metadata Data Model, and 5.4 Access Control Policies.

Advanced Resource Endpoints:

Chunked Resource Endpoints: the reserved chunks sub-path under a Resource is where [PWS-EC] defines its chunk endpoints (store, read, head, delete a single chunk, and list a Resource's chunks). A server that does not implement [PWS-EC] MAY omit them entirely; see the E. Reserved Path Segment Registry.

Policy Related Endpoints:

Policy overrides are hierarchical and inherited. A policy set for the entire Space applies to all its Collections and Resources (unless overridden by a more specific policy, either at the Collection or Resource level).

  • GET|PUT|DELETE /space/{space_id}/policy -- Reserved / not yet specified. CRUD on the policy object for the Space.
  • GET|PUT|DELETE /space/{space_id}/{collection_id}/policy -- Reserved / not yet specified. CRUD on the policy object for the Collection.
  • GET|PUT|DELETE /space/{space_id}/{collection_id}/{resource_id}/policy -- Reserved / not yet specified. CRUD on the policy object for the Resource.

Linkset / Discovery Endpoints:

Required if Space endpoints or Collection endpoints are supported.

Query Endpoints:

  • POST /space/{space_id}/query -- Cross-collection queries (backend-specific).
  • POST /space/{space_id}/{collection_id}/query -- Queries within a Collection, discriminated by a request-body profile; catalogued in G. Query Profile Registry.

Backend Management Endpoints (see 14. Backends):

Quota Endpoints (see 15. Quotas):

  • GET /space/{space_id}/quotas -- 15. Quotas: the Quota report object, grouped by available Backend (add ?include=collections for a per-collection usage breakdown)
  • GET /space/{space_id}/{collection_id}/quota -- 15. Quotas: the Quota report object for the specific Collection (not all Backends will support per-collection quotas however)

3. Terminology

backend
A storage engine that a collection's resources are physically stored on, registered at the Space level either by server configuration or by client-side "Bring Your Own Storage" registration. Backends are an OPTIONAL concern, orthogonal to the Space > Collection > Resource hierarchy: a Collection names one via its backend property, and is assigned the default backend when it does not. See section 14. Backends.
collection
A namespace and configuration container for resources. Conceptually maps to folders (for file system like storage), buckets (for object storage), or database tables (for RDBMSs). See section 11. Collections.
controller
An entity that can make changes to a given object.
decentralized identifier (DID)
See [DID-CORE].
instance, server
A deployed instance of an application or service that implements this specification's API.
policy
A JSON document with a required type property that declares what access a target grants to callers in general, independent of any zCap a caller might present. A policy is stored at the /policy auxiliary resource of a Space, Collection, or Resource. Policies are inherited most-specific-wins (Resource over Collection over Space), can only broaden access (never deny a caller holding a valid capability), and grant nothing when absent or of an unrecognized type. See section 5.4 Access Control Policies.
quota
A storage limit enforced per backend, together with the usage measurement of the storage consumed against it. Quota reporting and enforcement are OPTIONAL and backend-dependent: a Space's per-backend report is read from its /quotas auxiliary resource, and a per-Collection breakdown from /quota. A write that would exceed a quota is rejected with quota-exceeded (507); a single upload larger than the backend's maxUploadBytes constraint is rejected with payload-too-large (413). See section 15. Quotas.
service description
The JSON document through which a server advertises, without authorization, every specification and version it implements and the server-wide URLs and affordances a client needs before it holds a Space. It has no fixed path: every response carries a Link header with the service relation pointing to it. See section 8. Service Description.
zCap (Authorization Capability)
The delegatable object capability of the baseline authorization profile, [PWS-AUTHZ]. See the [zCap Developer Guide](https://interop-alliance.github.io/zcap-developer-guide/) for more details.

4. Identifiers

4.1 Identifier Required Properties

Space, Collection, and Resource identifiers used in this specification are required to have the following properties.

  1. URL-safety - All characters in a given identifier MUST be URL-safe.
  2. Uniqueness - All identifiers MUST be unique within a given container. That is: Space ids (denoted by {space_id} in URL templates) MUST be unique within a given server, Collection ids (denoted by {collection_id} in URL templates) MUST be unique within a given Space, and Resource ids (denoted by {resource_id} in URL templates) MUST be unique within a given Collection.

4.2 Identifier Length and Format

Identifier length limits are currently left to the implementer. However, implementations SHOULD limit identifier length to their appropriate use case.

Identifier format constraints are currently left to the implementer. Common identifier formats include:

5. Authorization

The ability to do cross-domain, operator-independent, standardized cloud storage operations requires an authorization system that is:

Authorization is pluggable. This specification defines the contract every authorization profile meets and the sockets a profile plugs into: the Space controller as the root of trust and the /policy auxiliary resource. The mechanism itself, how a caller proves it may act on a target, and the vocabulary in which a credential names the operation and the resource it covers, are defined by an authorization profile: a separate specification with a persistent identifier of its own, listed in 5.1 Authorization Profiles.

5.1 Authorization Profiles

A server MUST implement at least one authorization profile listed in the table below. It MUST list every profile it implements in the service description's specs member, under the profile's identifier (see 8.2 Service Description Data Model). The members of a profile's version entry are defined by the profile.

Profile Identifier Status
PWS Authorization Profile [PWS-AUTHZ]: zCaps over HTTP Signatures, with the PublicCanRead policy https://w3id.org/pws/authz-profile Baseline

The PWS Authorization Profile [PWS-AUTHZ] is the baseline. A server conformant to the Minimal profile of 2.4 Scope and Conformance Profiles MUST implement it, so that the Minimal profile is implementable from this document and [PWS-AUTHZ] alone. A server MAY implement further profiles. Registering a profile adds a row to this table.

Every authorization profile meets the following contract:

5.2 Space controller and the Root of Trust

Conceptually, the space's controller serves as the root of trust and authorization for any operations on the space or its collections or resources. That is, any operation requiring an authorization MUST provide a chain of proof all the way to the space controller. The forms that proof takes (a credential the controller signs directly, one the controller delegated to another agent, or a matching access control policy) are defined by the authorization profile; for the baseline, see the Root of Trust section of [PWS-AUTHZ].

Space controllers MUST be in the form of a DID.

For minimal compatibility, all PWS implementations MUST support the did:key DID Method, using the Multikey encoding of Ed25519 elliptic curve keys, as specified in the Multikey section of the CID spec as the space controller.

A Space controller MAY use a DID method other than did:key, provided the method is listed in the B. Space Controller DID Method Registry. Support for every method other than did:key is OPTIONAL. A server refuses a controller whose DID method it does not support with invalid-request-body (400), whether at creation or on an update (see 9.1.2 Create Space Errors and 10.3 Update (or Create by Id) Space operation).

5.2.1 Setting a controller to an optional DID method

A server that supports an optional DID method decides at which point it admits a controller of that method. It MAY accept one at Space creation. It MAY instead require that every Space be created with a did:key controller and admit the optional method only through a later update of the existing Space (see 10.3 Update (or Create by Id) Space operation); such an update promotes the did:key-controlled Space to the new controller. In both cases the proposed controller MUST resolve and verify before it is stored; a proposed controller that does not is refused with invalid-request-body (400). After the controller is stored, later updates to the Space are authorized by the DID document being resolved, so storing an unresolvable DID (a typo, a history not yet published) would leave the Space with no party able to act on it.

When a space is created via an HTTP POST or PUT operation (see 9.1.1 (HTTP API) POST /spaces/ and 10.3 Update (or Create by Id) Space operation), the controller for that space is set explicitly. That is, a client specifies the controller as part of the payload of the PUT or POST create space request, and the server MUST verify that the creating request is authorized by that controller, as the authorization profile defines (for the baseline, see Authorizing Space creation in [PWS-AUTHZ]). A request that is not is refused with controller-mismatch (400). A matching policy cannot authorize creation: no Space, and therefore no policy, exists yet.

See 9.1.1 (HTTP API) POST /spaces/ below for examples of controller determination and verification.

5.4 Access Control Policies

Capabilities answer the question "does the caller hold a credential that grants this action?" Access control policies answer the complementary question "does this target grant this action to callers in general (or to a named set of principals)?" Policies are how a controller makes a target public-readable (or, in future profiles, shares it with a list of people or groups) without having to issue a capability to each caller.

A policy is a JSON document with a required type property, stored at the /policy auxiliary resource of a Space, Collection, or Resource and discoverable via the policy relation in the linkset (see 13.1 Space Linkset and 13.2 Collection Linkset).

Evaluation contract:

5.4.1 Policy Types

Policy type values are defined by policy profiles, in the same way that authorization is defined by an authorization profile. This specification does not fix the set. The registered types, each with the specification that defines it, are listed in C. Policy Type Registry. An implementation recognizes a type by looking it up there, and a type not listed grants nothing. The baseline profile [PWS-AUTHZ] defines PublicCanRead, the first registered type.

6. Error Handling

This specification uses [RFC9457] Problem Details for HTTP APIs for error responses.

The type property is a URI identifying the kind of problem (not the operation), and the same type is reused across operations. See Appendix H. Error Type Registry for the catalog of type URIs this specification defines, along with their typical status codes and a canonical example response for each kind.

When returning errors, keep in mind the principle of maximum privacy: always "not found" instead of "not authorized". The existence of a resource, collection, or space, is by itself an item of sensitive information. If a client makes an API call, and they have insufficient authorization to perform that action, a "not found" error (such as HTTP 404) MUST be returned, just as if that resource (or space or collection) did not exist. To put it another way, an unauthorized client (meaning, either not carrying any authorization in the request itself, or possessing insufficient permissions) MUST NOT be able to discover the existence of a resource based on the error response.

This privacy rule governs failures that would otherwise reveal whether a target exists. It does not require masking failures that describe the request itself:

"List Spaces" operations are an exception to 404 masking: rather than returning an error, they return 200 OK with only the subset of items the caller is authorized to see (an empty items array if none) -- see 9.2 List Spaces Operation.

7. Common Behaviors

This section defines HTTP mechanics that are shared across the operations in this specification, rather than belonging to any single endpoint: caching, conditional requests (optimistic concurrency control), and pagination of list responses.

7.1 Caching

PWS relies on ordinary [RFC9111] HTTP caching and defines no caching layer of its own. On the read side, a Resource's strong ETag validator (see 7.2 Conditional Requests) drives standard validation: a GET carrying If-None-Match: "<etag>" yields 304 Not Modified when the Resource is unchanged. Servers SHOULD emit ETag (and MAY emit Last-Modified) on GET/HEAD responses, and SHOULD mark responses to non-idempotent operations as non-cacheable (for example, Cache-Control: no-store).

Editor's note
Freshness lifetime and Cache-Control directive semantics beyond validation are not yet specified.

7.2 Conditional Requests

A server MUST support [RFC9110] conditional requests to provide optimistic concurrency control on every Resource write path. This mechanism prevents the "lost update" problem, where two clients that both read version N of a Resource each write version N+1 and the second silently clobbers the first.

A Resource carries a strong ETag validator that changes whenever its stored content changes. Servers SHOULD return the ETag on GET/HEAD responses. The validator is opaque to clients: how a server derives it is a server-side concern.

A state-changing request (PUT or DELETE) MAY carry a precondition:

A server MUST evaluate the precondition atomically with the write, so that two concurrent writers cannot both observe the same prior version and both succeed. A client recovers from a 412 by re-reading the current Resource, re-applying its change on top of the new version, and retrying.

Note
The storage engine behind a backend need not offer a precondition primitive of its own. A client never addresses the engine directly, so the server can serialize writes itself (under a per-Resource lock, or a shared lock or compare-and-swap when horizontally scaled) and mint its own opaque validator, such as a content hash or an internal version counter. Emulating over an engine that is also writable outside the server, as a client-registered external provider often is, carries two hazards: an out-of-band write leaves the validator describing stale content, and a validator stored apart from the bytes can be orphaned by a crash between the two writes. Neither has a client-side remedy; embedding the validator in the stored object addresses the second.

As with id-conflict, a server MUST verify the caller's authorization before evaluating a precondition, so a 412 is only ever observed by a caller already authorized to write the target; an under-authorized caller receives the merged not-found (404) instead, per the maximum-privacy rule in 6. Error Handling.

The mechanism extends to a container's Metadata object, whose update operation replaces the whole object -- so two concurrent recipient changes to a key-epoch roster (see F.4 Key Epochs) would otherwise silently clobber one another:

The Space Metadata object carries the same validator, on the same terms: the 10.2 Read Space operation response includes an ETag header over the object's monotonic version, the 10.3 Update (or Create by Id) Space operation request MAY carry If-Match or If-None-Match: *, and a server MUST evaluate either atomically with the write and reject a failed precondition with precondition-failed (412). The Create Space and Update Space responses carry the new ETag. The create-if-absent form is what lets two clients provisioning the same Space at once resolve the race at the server: the loser's replacement PUT would otherwise rewrite the winner's type array, which a server accepts at creation only (see 10.1 Space Metadata Data Model).

Editor's note
Authorization for recipient changes is the plain Collection-update capability in this version. A dedicated action for recipient management (a policy operation, distinct from content writes) remains an open question.

A Collection's metaVersion is independent of every Resource's validator: writing a Resource never advances it, and writing the Metadata object never advances a Resource's. A server implementing the Collection management operations MUST maintain it and MUST support both preconditions, on the same baseline terms as a Resource's validator, and it covers configuration and annotation writes alike; see 11.1.1 Versioning.

A 412 arises only from an explicit If-Match / If-None-Match precondition header. It is deliberately distinct from the header-less 409 conflict kinds -- id-conflict (a POST create whose chosen id is already taken) and reserved-id -- which describe a conflict with current state where the client stated no precondition. Conditional requests are the versioned, header-driven concurrency mechanism; the 409 kinds are not.

7.3 Paginated List Responses

The list operations -- 9.2 List Spaces Operation, 10.5 List All Collections operation, and 11.5 List Collection operation -- MAY paginate their responses, returning one page of items at a time using the cursor-based profile defined in Appendix A. Pagination. Pagination is OPTIONAL: a server that returns every item in a single response is conformant, and a client MUST be prepared for either behavior.

8. Service Description

A server publishes one service description: a JSON document, readable without authorization, that names every specification the server implements and the versions it speaks, and carries the server-wide facts a client needs before it can perform operations. A client choosing a host at signup, or deciding which URL layout to speak after a breaking change, reads this document first. Every other signal in this specification (the Space and Collection linksets, the Backend description) is reached through a Space-scoped URL, which is the very thing a layout change alters, so none of them can serve this purpose.

Serving the service description is REQUIRED at every conformance tier (see 2.4 Scope and Conformance Profiles). It is the one document a client of any version must be able to read, so its own format is frozen: a future version of this specification may add members to it but MUST NOT rename, remove, or change the type of a member defined here.

The service description has no fixed path. A server MAY be mounted under any path of an origin, and a client that holds a PWS URL cannot derive the server root from it, so the document is found by following a link rather than by convention (see 8.1 Discovering the Service Description). This is the deliberate exception to the "description lives at meta" convention of 2.2 Reading This Document: the server level has no container URL of its own to hang a meta segment on.

8.1 Discovering the Service Description

A server MUST include a Link header [RFC8288] with the service link relation [RFC5023] on every response it sends, whose target is the service description's URL. This includes error responses (a maximum-privacy "not found" for an unauthorized request still carries the header), the 308 redirects to a canonical path, and responses to HEAD and OPTIONS. The header is static and server-wide, so it reveals nothing about the existence of any Space, Collection, or Resource.

Link: <https://example.com/pws/service>; rel="service"

The service relation is chosen over the service-desc and service-doc pair of [RFC8631] because this specification defines a single machine-readable document and no separate human-readable one; [RFC8631] section 3.3 recommends service for exactly that case.

Because the primary consumers are browser-hosted clients on other origins, two CORS requirements apply:

The discovery flow is therefore two requests from any PWS URL a client has come across (a link to a shared Resource, a public Collection, a service endpoint in a DID document):

  1. HEAD the URL. The response status does not matter; the client reads the Link header's service target.
  2. GET that target (see 8.4 Read Service Description operation).

A DID document's service endpoint MAY point at the service description itself, in which case a client skips the first step. The service type for that entry is not yet specified.

The service description is also linked from the 13.1 Space Linkset and the 13.2 Collection Linkset under the same service relation, as a convenience for a client that already holds a Space. Those links are secondary: a client that can read a linkset is already speaking a layout.

A response with no service link comes from a server that predates this document, which was introduced in v0.5. A client MUST treat such a server as speaking v0.4.

8.2 Service Description Data Model

The service description is a JSON object with the following members:

Every URL in the service description MUST be absolute. The document is self-contained: nothing in it is resolved against the URL it was fetched from.

Specification identifiers. The key of a specs entry is the persistent identifier that the specification declares for itself in its own text, which is not the same as its current document location. A specification that declares no identifier cannot be advertised. Clients compare keys as opaque strings, with no URL normalization: a trailing slash or a scheme difference is a different key. The version is never part of the key; the key names the specification family, and the version entries under it carry the versions.

This specification's identifier is https://w3id.org/pws.

Editor's note
The identifier above is provisional: it is registered by the rename this document is pending (see 2.1 Version History and Roadmap) and may change with it before v0.5 is final.

Version entries. A version entry is a JSON object describing one version of one specification. Its members are defined by the specification the key names. Two members are common to every entry:

An entry means the server conforms to that version's required profile. What else the entry advertises is the owning specification's business: endpoint URLs, optional sections it also implements, supported algorithms. An optional section that has an endpoint of its own is advertised by a URL member, and its absence means the section is not supported; a feature token is used only for an affordance with no URL of its own. This keeps the service description the root of the URL graph: with no fixed path for anything at the server level, a client learns every server-level URL from here. A companion specification defines the tokens (if any) of its own version entry's features array; this specification does not enumerate them.

This specification's version entry. A version entry under https://w3id.org/pws carries, in addition to version and url:

An authorization profile's version entry. A server lists every authorization profile it implements under the profile's own identifier (see 5.1 Authorization Profiles), and the profile defines its entry's members. The baseline profile's entry, under https://w3id.org/pws/authz-profile, carries the signature algorithms and delegation-proof cryptosuites the server accepts; see the Service Description Entry section of [PWS-AUTHZ]. The example below shows one.

8.3 Instance Disclosure

The instance member identifies the software serving the document. It is OPTIONAL, its presence is the operator's choice, and a client MUST NOT gate any behavior on it: what a client acts on is specs. Its members:

The members follow the software object of NodeInfo, the most widely deployed vocabulary for this purpose, with source in place of its repository.

Two practices pull in opposite directions here, and this specification takes no side beyond the default above. Server-hardening guidance (for example, the OWASP Application Security Verification Standard's requirement that responses not expose detailed version information of system components) says to withhold software versions, because mass scanners and exploit kits select targets by matching an exact version string against a known-vulnerable release. Federated ecosystems (Fediverse NodeInfo, Matrix) routinely advertise software and version, because interop debugging across many independent implementations and an ecosystem-wide vulnerability response both depend on knowing who runs what.

Two facts specific to this specification narrow the gap. The specs member already fingerprints a server by the versions and features it speaks, so withholding the software name buys less than it does for a web server behind a generic page. And a server's conformance behavior identifies its implementation to any client that cares to probe. What instance adds is the cheap path: a scanner keying on a version string. Omitting version, or carrying only its major component, removes that path while keeping name, source, and homepage for debugging and license compliance.

8.4 Read Service Description operation

GET on the URL named by the service link returns the service description as application/json. The operation requires no authorization. The response SHOULD be cacheable (Cache-Control and an ETag), so that a client re-checking a host pays a revalidation rather than a fetch.

Example discovery from a Resource URL the client holds:

HEAD /pws/space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/hello-world HTTP/1.1
Host: example.com

Response (the Resource is private, so the unauthorized request is "not found"; the Link header is present regardless):

HTTP/1.1 404 Not Found
Link: <https://example.com/pws/service>; rel="service"
Access-Control-Expose-Headers: Link

Example service description request and response:

GET /pws/service HTTP/1.1
Host: example.com
Accept: application/json

Response:

HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Link
Cache-Control: public, max-age=3600
ETag: "a1b2c3"
Link: <https://example.com/pws/service>; rel="service"

{
  "url": "https://example.com/pws/service",
  "specs": {
    "https://w3id.org/pws": [
      {
        "version": "0.5",
        "url": "https://w3c-ccg.github.io/wallet-attached-storage-spec/v0.5/",
        "spaces": "https://example.com/pws/spaces/",
        "features": ["listing", "collection-management", "space-management",
                     "linksets", "policy", "backends", "quotas",
                     "created-by"]
      },
      {
        "version": "0.4",
        "url": "https://w3c-ccg.github.io/wallet-attached-storage-spec/v0.4/",
        "spaces": "https://example.com/pws/spaces/",
        "features": ["listing", "collection-management", "space-management",
                     "linksets", "policy", "backends", "quotas"]
      }
    ],
    "https://w3id.org/pws/authz-profile": [
      {
        "version": "0.1",
        "url": "https://w3c-ccg.github.io/wallet-attached-storage-spec/authz-profile/",
        "signatureAlgorithms": ["EdDSA"],
        "zcapCryptosuites": ["eddsa-jcs-2022"]
      }
    ],
    "https://w3id.org/pws/encrypted-collections": [
      {
        "version": "0.1",
        "url": "https://w3c-ccg.github.io/wallet-attached-storage-spec/ec/",
        "features": ["blinded-index-query", "governed-history-logs"]
      }
    ]
  },
  "instance": {
    "name": "was-teaching-server",
    "source": "https://github.com/interop-alliance/was-teaching-server"
  }
}

The server above serves the v0.4 and v0.5 route tables together during the transition described in 2.1 Version History and Roadmap.

8.5 Selecting a Version

A client reads the service description before its first structural request to a server and applies these rules:

How often a client re-reads the document is a client policy. A client that gates a security-relevant behavior on an advertised affordance (for example, publishing only to a host that enforces a verification rule) SHOULD re-check before relying on it again, since a host can stop advertising an affordance it once did.

8.6 Protocol Evolution

This specification changes in two ways, and the service description distinguishes them.

Additive change (a new optional endpoint, a new feature token, a new error kind, a new query profile, a new reserved segment) is advertised through the registries in the appendices and through features. A client that does not know the addition keeps working, because unknown tokens are ignored and absent means not supported.

Incompatible change (an existing URL changing meaning, a member changing type, a required behavior being removed) takes a new specification version, and the new version is advertised as a new version entry. A server MAY list the old and the new version together during a transition, and a client picks the newest it understands.

Versions never appear in URL paths. A PWS URL is a capability target and a long-lived identity: it is what a delegation chain is rooted on and what a client stores to find a Resource again. A version segment in the path would bind every delegation chain to a protocol version and would turn every version bump into a re-delegation of every capability. The version lives in the service description, and a layout change keeps the old URLs meaningful for as long as the server lists the old version.

9. Spaces Repositories

A Spaces Repository is a set of API endpoints that supports the creation and management of multiple spaces on a given server. This /spaces/ set of API endpoints is optional. If a server does not support this feature (for example, if it is a single-tenant server with an existing hardcoded Space), then it can implement only the /space/{space_id}/ endpoints and get most of the functionality of this specification.

Editor's note
The Spaces Repository endpoints are a candidate for extraction into a standalone companion specification (multi-tenant space provisioning) in a future version of this document.

9.1 Create Space operation

To create a Space:

9.1.1 (HTTP API) POST /spaces/

To create a space via HTTP API using the Spaces Repository POST API:

POST /spaces/ HTTP/1.1
Host: example.com
Accept: application/json
Content-type: application/json
Authorization: ...

{
  "type": ["Space"],
  "name": "Example space #1",
  "controller": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW"
}

Example success response:

HTTP/1.1 201 Created
Content-type: application/json
Location: https://example.com/space/81246131-69a4-45ab-9bff-9c946b59cf2e/

{
  "id": "81246131-69a4-45ab-9bff-9c946b59cf2e",
  "type": ["Space"],
  "name": "Example space #1",
  "controller": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW"
}

Note that in the example above:

  • the id was not specified in the body of the request, and so was generated by the server and returned in the response

9.1.2 Create Space Errors

Errors (see H. Error Type Registry for canonical examples):

  • invalid-request-body (400) -- the required controller property is missing from the request body, or is a DID of a method the server does not accept as a Space controller (see B. Space Controller DID Method Registry: did:key is mandatory to support, other methods are optional, and a server may admit them only on update).
  • missing-authorization (401) -- the request lacks a valid proof of possession of the controller DID.
  • controller-mismatch (400) -- the invocation is not currently authorized by the controller DID in the request body: it is neither signed by that DID nor accompanied by a valid, unexpired delegation chain rooted in it. A chain rooted in a different DID, an expired delegation, and a chain whose proof fails verification all fall under this one type. Servers are not required to distinguish these (delegation-chain verifiers often report failure opaquely), but SHOULD differentiate the cause in the non-normative detail string where they can. In a delegated provisioning flow, the cause determines who must act (the user re-delegates an expired capability; the service corrects a request whose chain is rooted in the wrong DID).
  • invalid-id (400) -- the supplied Space id is not URL-safe (see 4. Identifiers).
  • id-conflict (409) -- a Space with the supplied id already exists.

Onboarding requirements, if any, are provider-specific and out of scope for this specification (see the optional onboarding material above). Their error type, title, and status code are likewise provider-defined.

A POST /spaces/ that supplies an id already in use returns the id-conflict error. To create or replace a Space at a client-chosen id without conflict, use the idempotent 10.3 Update (or Create by Id) Space operation instead.

The existence check behind id-conflict is security-critical here, not merely a conformance detail. Create Space is the one operation whose capability invocation is verified against the controller supplied in the request body -- the Space does not exist yet, so there is no stored controller to verify against. A server that instead treats POST /spaces/ as create-or-replace turns this operation into a takeover: any caller able to construct a valid invocation for their own controller could overwrite an existing Space's Metadata object (controller included) simply by POSTing its id. Servers MUST check for an existing Space with the supplied id, and reject with id-conflict, before writing anything.

Note
The id-conflict response on this operation necessarily reveals whether a Space id is taken: any caller permitted to attempt creation can distinguish 409 from 201 -- just as they could by observing whether creation succeeds. This disclosure is inherent to client-chosen ids on a create endpoint, and is not a violation of the maximum-privacy principle of 6. Error Handling, which governs errors about *existing* targets the caller is not authorized for. Unguessable (for example, UUID) Space ids keep the signal worthless to an attacker, and providers that gate Space creation behind onboarding requirements also bound who can observe it.
Note
Differentiated detail strings on controller-mismatch are likewise privacy-safe: Create Space has no existing target to protect, and everything its verification examines (the body's controller, the capability chain, its signatures) is supplied by the caller, so failure granularity reveals nothing the caller does not already hold. This reasoning does *not* extend to failure causes that depend on server-side state (for example, capability revocation status or per-controller onboarding allowances); whether and how to disclose those remains provider-defined.

9.2 List Spaces Operation

9.2.1 (HTTP API) GET /spaces/

Example request:

GET /spaces/ HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...

Example success response (requester has read access to at least one space):

HTTP/1.1 200 OK
Content-type: application/json

{
  "url": "/spaces/",
  "totalItems": 1,
  "items": [
    {
      "id": "81246131-69a4-45ab-9bff-9c946b59cf2e",
      "url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/",
      "type": ["Space"]
    }
  ]
}

Example success response (requester does NOT have access to any spaces):

HTTP/1.1 200 OK
Content-type: application/json

{
  "url": "/spaces/",
  "totalItems": 0,
  "items": []
}

A request that carries no authorization (or authorization for no spaces) is not an error: like any list operation it returns the 200 OK empty-list response shown above, revealing nothing about which spaces exist (see 6. Error Handling).

10. Spaces

A space is a namespace for collections and a unit of general configuration, a volume of storage that contains one or more collections. Conceptually, is maps to a disk partition (for file systems), or a database (for relational databases).

10.1 Space Metadata Data Model

A Space's description is its Metadata object, addressable at the reserved meta path segment under the Space URL (one of the E.1 Space-level reserved endpoints). It is what 10.2 Read Space operation returns and what 10.3 Update (or Create by Id) Space operation writes; the Space URL itself is the container of the Space's Collections (see 10.5 List All Collections operation).

Because the meta segment occupies the {collection_id} position, meta is a reserved Collection id: creating a Collection with the id meta is a reserved-id conflict (see E. Reserved Path Segment Registry).

PUT is not defined at the Space URL itself; the replacement write is a PUT of this object. A server MUST answer a PUT at the Space URL with 405 Method Not Allowed.

Space properties:

Space properties automatically added by the server:

A server that records createdBy on every Resource, Collection, and Space created under a capability invocation MUST advertise the created-by token in its version entry's features array (see 8.2 Service Description Data Model). A server that advertises created-by MUST record createdBy on each of them. A client MUST NOT assume attribution on a server that does not advertise the token. A client feature that depends on attribution refuses to run on such a server rather than reading an absent createdBy as anonymous.

Note
The created-by token is a candidate for promotion to a baseline requirement in a later breaking version. That step waits until a second client feature, beyond a wallet's agent-connection inbox, depends on attribution.

The Space Metadata object therefore splits in two. Its user-writable members are type and name. Its server-derived members are createdBy, url, linkset, and backends. controller is neither: the creating invocation sets it and only the controller rewrites it. That split is the rule an import honours: restoring an archived Space Metadata object writes type and name, leaves the server-derived members to the server, and does not touch controller (see E.1 Space-level reserved endpoints).

A server that supports conditional writes (see 7.2 Conditional Requests) also keeps a server-managed version validator for each Space Metadata object: an opaque strong validator that changes on every write of the object and is never reused after the Space is deleted. It is not a member of the Space Metadata object; it is surfaced only as the ETag header of the Read Space response and of the Create Space and Update Space responses, and consumed only through the If-Match and If-None-Match: * preconditions of the Update (or Create by Id) Space operation.

10.2 Read Space operation

The format of the response is determined based on content negotiation; application/json is the REQUIRED baseline (see 12.3 Content Types and Representations).

10.2.1 (HTTP API) GET /space/{space_id}/meta

Example request:

GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/meta HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...

Example success response:

HTTP/1.1 200 OK
Content-type: application/json
ETag: "z3fkq.2"

{
  "id": "81246131-69a4-45ab-9bff-9c946b59cf2e",
  "url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/",
  "type": ["Space"],
  "name": "Example space #1",
  "controller": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW",
  "createdBy": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW",
  "linkset": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/linkset"
}

10.2.2 Read Space Errors

Errors (see H. Error Type Registry for canonical examples):

  • not-found (404) -- the Space does not exist, or the caller has missing or insufficient authorization. A server MUST return the same error response in both cases, per 6. Error Handling.

10.3 Update (or Create by Id) Space operation

A PUT of a Space's Metadata object is a full replacement of that object, and creates the Space when none exists under that id. The client specifies the id of the Space in the request URL.

The controller property in the request body is the proposed value, not the root of trust for the request. When the Space already exists, the capability invocation MUST be verified against the Space's currently stored controller: only the current controller (or its delegate) may update the Space, including transferring it by writing a new controller. The body's controller participates in verification only when the PUT creates the Space -- there is no stored controller yet, so, as with 9.1 Create Space operation, the invocation MUST be authorized by the body's controller: signed by it directly, or presented with a valid, unexpired delegation chain rooted in it (violations are controller-mismatch, as for POST). A server that verifies an update against the body's controller reopens the takeover described under 9.1.2 Create Space Errors: any caller could seize an existing Space by PUTting its id with themselves as the controller.

A server that records createdBy (see 10.1 Space Metadata Data Model) sets it only on the PUT that creates the Space, to the did of the party that invoked the capability -- which is not necessarily the body's controller. On a PUT that updates an existing Space, the stored createdBy is preserved, so transferring the Space by writing a new controller does not rewrite its creator.

A new controller written by an update is subject to the same method acceptance as one set at creation (see B. Space Controller DID Method Registry), plus the promotion rule of 5.2.1 Setting a controller to an optional DID method: a proposed controller of an optional method MUST resolve and verify before it is stored, and a server MAY admit such controllers only through this operation.

The request MAY carry a precondition (see 7.2 Conditional Requests): If-Match: "<etag>" performs the update only if the Space Metadata object's current ETag matches it, and If-None-Match: * performs the write only if no Space exists under that id yet. A server that supports conditional writes MUST evaluate the precondition atomically with the write, after authorization, and answer a failed one with precondition-failed (412). Both are opt-in: an unconditional PUT remains valid and remains last-writer-wins. A client that creates a Space it has just read as absent SHOULD send If-None-Match: * and, on a 412, re-read the Space instead of treating the response as a failure. The success response carries the Metadata object's new ETag.

10.3.1 (HTTP API) PUT /space/{space_id}/meta

Note that this is a full update (partial updates via http PATCH verb might be supported later). However, some fields cannot be updated (like id) and so may be omitted from the request payload.

Note that this operation is idempotent.

  • When creating a space via PUT, a controller property is required in the PUT request body.

Example request (creating a new space via PUT):

PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e/meta HTTP/1.1
Host: example.com
Accept: application/json
Content-type: application/json
Authorization: ...

{
  "id": "81246131-69a4-45ab-9bff-9c946b59cf2e",
  "type": ["Space"],
  "name": "Example space #1",
  "controller": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW"
}

Example success response. The Location header names the Space that was created, not the Metadata object that was written:

HTTP/1.1 201 Created
Content-type: application/json
Location: https://example.com/space/81246131-69a4-45ab-9bff-9c946b59cf2e/
ETag: "z3fkq.1"

Example request (updating the name property of a space). Note that server-managed properties such as url and linkset are not client-writable and are omitted from the request body:

PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e/meta HTTP/1.1
Host: example.com
Content-type: application/json
Accept: application/json
Authorization: ...

{
  "id": "81246131-69a4-45ab-9bff-9c946b59cf2e",
  "type": ["Space"],
  "name": "Newly renamed space #1",
  "controller": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW"
}

Example success response:

HTTP/1.1 204 No Content
ETag: "z3fkq.2"

Errors (see H. Error Type Registry for canonical examples):

  • not-found (404) -- the Space does not exist, or the caller has missing or insufficient authorization; per 6. Error Handling the two are indistinguishable.
  • invalid-request-body (400) -- the client is attempting to change an immutable field, such as setting a body id that does not match the {space_id} in the request URL; or the proposed controller is a DID of a method the server does not accept, or one that does not resolve and verify (see 5.2.1 Setting a controller to an optional DID method).
  • precondition-failed (412) -- the request carried an If-Match that does not match the Metadata object's current ETag, or an If-None-Match: * against a Space that already exists (see 7.2 Conditional Requests).

10.4 Delete Space operation

10.4.1 (HTTP API) DELETE /space/{space_id}/

Example request (no request body):

DELETE /space/81246131-69a4-45ab-9bff-9c946b59cf2e/ HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...

Example success response:

HTTP/1.1 204 No Content

Errors (see H. Error Type Registry for canonical examples):

  • not-found (404) -- the caller has missing or insufficient authorization. Because DELETE is idempotent, an authorized request for an already-absent Space returns 204; an under-authorized request returns 404 instead, per 6. Error Handling, so that an unauthorized caller cannot probe for existence.
  • invalid-id (400) -- the supplied Space id is not URL-safe.

10.5 List All Collections operation

10.5.1 (HTTP API) GET /space/{space_id}/

Example request (a container URL, so it carries the trailing slash):

GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/ HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...

Example response:

HTTP/1.1 200 OK
Content-type: application/json

{
  "url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/",
  "totalItems": 2,
  "items": [
    {
      "id": "example",
      "url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/example/",
      "name": "Example Collection",
      "public": false
    },
    {
      "id": "73WakrfVbNJBaAmhQtEeDv",
      "url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/",
      "name": "73WakrfVbNJBaAmhQtEeDv",
      "public": true
    }
  ]
}

Errors (see H. Error Type Registry for canonical examples):

  • not-found (404) -- the Space does not exist, or the caller has missing or insufficient authorization; per 6. Error Handling a Space the caller is not authorized to read is indistinguishable from one that does not exist.

11. Collections

A collection is a namespace for Resources, and a unit of configuration, within a space.

In other storage systems, the concept of collections has many different names. For example, Directory, Folder, RDBMS Table, Document Collection, Graph, WebAPI FileList, Bucket, LDP Basic Container, EDV Vault, and so on.

Collections do not contain other collections (this specification adopts a flat collection structure).

Note
Rationale: Nested collections (such as those used by Solid/LDP/LWS) encode query relationships into URL hierarchy, which creates significant complexity (recursive permissions, recursive deletes, path traversal, depth-limited listing) and maps poorly to flat backends like RDBMS tables or S3 buckets. The use cases that motivate nesting (e.g., "all comments on a post") are better served by the query endpoint with field-based filtering.

11.1 Collection Metadata Data Model

A Collection's description is its Metadata object, addressable at the reserved meta path segment under the Collection URL (one of the E.2 Collection-level reserved endpoints). It holds everything recorded about the Collection: what it is called, how its Resources are stored and whether they are client-side encrypted, who created it, and whatever annotations the user keeps on it. The Collection URL itself is the container of the Collection's Resources (see 11.5 List Collection operation).

Because the meta segment occupies the {resource_id} position, meta is a reserved Resource id (see E. Reserved Path Segment Registry). A GET or PUT at this path is always a Collection metadata operation, so no Resource is ever addressed at the id meta. Methods this specification does not define at this path (DELETE in particular) are not Resource operations either; see E.4 Methods at Reserved Endpoints for how a server answers them. The same meta segment also roots the Collection's governing history log, a separate sub-resource at meta/log with operations and a validator of its own (see 11.7 Collection Governing History Log).

PUT is not defined at the Collection URL itself; the replacement write is a PUT of this object. A server MUST answer a PUT at the Collection URL with 405 Method Not Allowed.

Reading and writing this object are part of the OPTIONAL Collection management group (see 2.6 API Summary): on a server where Collections are pre-configured or implicit, they are not implemented, and such a server SHOULD answer requests to this path with an unsupported-operation (501) error.

Writable properties:

Properties automatically added by the server, and read-only: a server MUST ignore any of them supplied in a request body.

There is no writerId member. A Collection's Metadata object is configuration maintained by the Space controller, not replicated per-revision content, so the attribution machinery of 12.8.1 Writer attribution: writerId and createdBy does not apply at this level.

On a Collection that declares an encryption descriptor (see F. Encryption Scheme Registry), custom is stored encrypted, exactly as at Resource level: its value is an envelope of the Collection's declared scheme, validated structurally on write (rejecting a plaintext custom with an encryption-scheme-mismatch) and never decrypted by the server, while every other member of this object stays plaintext. This is what gives an encrypted Collection a client-encrypted display name and tags, and a discoverable, conditionally-writable home for profile-level configuration such as the [PWS-EC] blinded-index schema. On an encrypted Collection a present custom MUST be either a conforming envelope or an empty object; an empty object clears the stored annotations. Omitting custom clears them too, exactly as on a plaintext Collection, because a PUT of this object is a full replacement. That is the same omission-clears rule epoch follows.

Example Metadata object of a plaintext Collection (JSON representation):

{
  "id": "73WakrfVbNJBaAmhQtEeDv",
  "url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/",
  "type": ["Collection"],
  "name": "Verifiable Credentials Collection",
  "createdAt": "2026-06-10T09:12:00Z",
  "updatedAt": "2026-06-12T13:25:00Z",
  "createdBy": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW",
  "generator": {
    "id": "did:key:z6MkfriqYRX3JBqzsbVbKuBUxDR2nsjLTu6AbrxJZAmFmXWb",
    "origin": "https://app.example.com",
    "url": "https://app.example.com/notes/",
    "name": "Notes"
  },
  "linkset": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/linkset",
  "custom": {
    "tags": { "project": "demo" }
  }
}

11.1.1 Versioning

The Metadata object carries a monotonic version, metaVersion, surfaced as a strong ETag validator on read. It is the Collection's one validator: it covers configuration and annotation writes alike, so a change to backend and a change to custom advance the same counter. (The name is the same one the changes query profile reports per Resource; this is its Collection-scoped counterpart.) It is independent of every Resource's validator -- writing a Resource never advances it, and writing this object never advances a Resource's -- and of the governing history log's, which versions its own sub-resource (see 11.7.5 Versioning).

A server implementing these operations MUST maintain the version and MUST honor If-Match and If-None-Match: * preconditions on the update, evaluated atomically with the write; see 7.2 Conditional Requests. The version is surfaced only as the ETag: it is not a member of the object's JSON representation. Because the object exists exactly as long as its Collection does, If-None-Match: * here means "write only if the Collection does not exist" -- the create-if-absent precondition of 11.4 Update (or Create by Id) Collection operation.

Carrying configuration and annotation on one validator means a configuration change invalidates an in-flight annotation write. That is intended. A change to encryption requires the custom envelope to be re-sealed in any case, and for anything else the client re-reads and retries. A client that wants to update custom alone still resends the configuration members it just read, because PUT is a full replacement.

Writes of this object are invisible to replication: the changes query profile is per-Resource and emits no entry for them, so a client mirroring the object re-reads it and compares the returned ETag.

11.1.2 Lifecycle

The Metadata object and its Collection share one lifecycle. The object comes into existence with the Collection, written by the POST or PUT that creates it, and there is no state in which a Collection exists without one: a GET of meta on an existing Collection always succeeds and always carries an ETag.

There is no DELETE at meta. Deleting the Collection removes its Metadata object with it. To clear the user's annotations alone, send a PUT carrying the configuration members unchanged and an empty custom object, or none at all; both forms clear on an encrypted Collection too (as above). Clearing is itself a write and advances metaVersion. A Collection later re-created under the same id starts over: its metaVersion restarts, so a client MUST NOT compare validators across a delete and a re-create.

11.2 Create Collection (Add Collection to a Space) operation

A Collection is added to a Space by POSTing its Metadata object (see 11.1 Collection Metadata Data Model) to the Space. The client can specify the id of the Collection in the body. If the id is not specified, one is auto-generated by the server and returned as part of the Location response header.

11.2.1 (HTTP API) POST /space/{space_id}/

Example request (id not specified, auto-generated by the server and returned in the response Location header):

POST /space/81246131-69a4-45ab-9bff-9c946b59cf2e/ HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...

{
  "name": "Verifiable Credentials Collection",
  "type": ["Collection"]
}

In this example, the id in the body is not specified, and will be auto-generated by the server. The backend is not specified, and will be assigned the default value.

Example response:

HTTP/1.1 201 Created
Content-type: application/json
Location: https://example.com/space/81246131-69a4-45ab-9bff-9c946b59cf2e/21f81693-f4a3-4caa-b81c-b663d6e1e3ae/

Example request, a valid id specified in the body:

POST /space/81246131-69a4-45ab-9bff-9c946b59cf2e/ HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...

{
  "id": "credentials",
  "name": "Verifiable Credentials Collection",
  "type": ["Collection"],
  "backend": { "id": "default" }
}

Example response:

HTTP/1.1 201 Created
Content-type: application/json
Location: https://example.com/space/81246131-69a4-45ab-9bff-9c946b59cf2e/credentials/

Errors (see H. Error Type Registry for canonical examples):

11.3 Read Collection Metadata operation

11.3.1 (HTTP API) GET /space/{space_id}/{collection_id}/meta

Example request:

GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/meta HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...

Example response:

HTTP/1.1 200 OK
Content-type: application/json
ETag: "3"

{
  "id": "73WakrfVbNJBaAmhQtEeDv",
  "url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/",
  "type": ["Collection"],
  "name": "Verifiable Credentials Collection",
  "createdAt": "2026-06-10T09:12:00Z",
  "updatedAt": "2026-06-12T13:25:00Z",
  "createdBy": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW",
  "linkset": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/linkset",
  "backend": { "id": "default" },
  "custom": {
    "tags": { "project": "demo" }
  }
}

Errors (see H. Error Type Registry for canonical examples):

  • not-found (404) -- the Collection does not exist, or the caller has missing or insufficient authorization; per 6. Error Handling a Collection the caller is not authorized to read is indistinguishable from one that does not exist.
  • unsupported-operation (501) -- the server does not implement the optional Collection management operations.

11.4 Update (or Create by Id) Collection operation

A PUT of a Collection's Metadata object is a full replacement of that object, and creates the Collection when none exists under that id. The client specifies the id of the Collection in the request URL; it MUST NOT collide with the list of E.1 Space-level reserved endpoints.

Full replacement means that a writable member the request omits is cleared, so a client that wants to change one member reads the object, edits it, and writes it back. Some members qualify that rule. The server-managed members are read-only: a server MUST ignore createdAt, updatedAt, createdBy, url, and linkset in a request body, so that a read-modify-write roundtrip needs no stripping. On a Collection whose encryption descriptor is governed by its history log, the server derives that member from the log's head and refuses a direct write of it (see 11.7 Collection Governing History Log). An update that omits plaintext leaves the stored member untouched (see its member definition), and so does an update that omits generator. And an update that omits backend MUST keep the stored backend selection. Clearing it would repoint the Collection at the default backend. The Resources already stored in the selected backend would become unreachable, and later writes would land elsewhere. Only a create that omits backend is assigned the default.

The request MAY carry a precondition (see 7.2 Conditional Requests): If-Match: "<etag>" performs the write only if the object's current ETag matches it, and If-None-Match: * performs the write only if the Collection does not exist yet. A failed precondition is precondition-failed (412); an unconditional PUT remains last-writer-wins. The success response carries the object's new ETag.

11.4.1 (HTTP API) PUT /space/{space_id}/{collection_id}/meta

Example successful "create" request. A PUT that creates the Collection and a PUT that reconfigures an existing one are the same operation:

PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/meta HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...

{
  "id": "73WakrfVbNJBaAmhQtEeDv",
  "name": "Verifiable Credentials Collection",
  "type": ["Collection"],
  "backend": { "id": "default" }
}

The Location header names the Collection that was created, not the Metadata object that was written:

HTTP/1.1 201 Created
Location: https://example.com/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/
ETag: "1"

Example request annotating an existing Collection. The configuration members are resent unchanged, because this is a full replacement, and the If-Match precondition makes the write conditional on nothing else having changed in the meantime:

PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/meta HTTP/1.1
Host: example.com
Content-Type: application/json
If-Match: "1"
Authorization: ...

{
  "id": "73WakrfVbNJBaAmhQtEeDv",
  "name": "Verifiable Credentials Collection",
  "type": ["Collection"],
  "backend": { "id": "default" },
  "custom": {
    "tags": { "project": "demo" }
  }
}
HTTP/1.1 204 No Content
ETag: "2"

Errors (see H. Error Type Registry for canonical examples):

11.5 List Collection operation

11.5.1 (HTTP API) GET /space/{space_id}/{collection_id}/

Example request (a container URL, so it carries the trailing slash):

GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/ HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...

Example response:

HTTP/1.1 200 OK
Content-type: application/json

{
  "id": "73WakrfVbNJBaAmhQtEeDv",
  "url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/",
  "name": "Example JSON Documents Collection",
  "type": ["Collection"],
  "totalItems": 2,
  "items": [
    {
      "id": "321efd4e-23cb-497c-aaee-7bd26e66d39e",
      "url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/321efd4e-23cb-497c-aaee-7bd26e66d39e",
      "contentType": "application/json"
    },
    {
      "id": "3943c87f-b617-44bc-ba75-8de2b16c3640",
      "url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/3943c87f-b617-44bc-ba75-8de2b16c3640",
      "contentType": "application/json"
    }
  ]
}
11.5.1.1 Paginated example

A client requests a bounded page with limit (here, two items from a Collection that holds more):

GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/?limit=2 HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...

The response carries the page of items plus a next link, signalling that more items follow. totalItems is omitted here -- the server does not return a total for this (potentially large) Collection:

HTTP/1.1 200 OK
Content-type: application/json

{
  "id": "73WakrfVbNJBaAmhQtEeDv",
  "url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/",
  "name": "Example JSON Documents Collection",
  "type": ["Collection"],
  "items": [
    {
      "id": "321efd4e-23cb-497c-aaee-7bd26e66d39e",
      "url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/321efd4e-23cb-497c-aaee-7bd26e66d39e",
      "contentType": "application/json"
    },
    {
      "id": "3943c87f-b617-44bc-ba75-8de2b16c3640",
      "url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/3943c87f-b617-44bc-ba75-8de2b16c3640",
      "contentType": "application/json"
    }
  ],
  "next": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/?limit=2&cursor=eyJhZnRlciI6IjM5NDNjODdmLWI2MTctNDRiYy1iYTc1LThkZTJiMTZjMzY0MCJ9"
}

The client follows next verbatim to fetch the subsequent page (the cursor is opaque and supplied by the server -- the client does not build it):

GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/?limit=2&cursor=eyJhZnRlciI6IjM5NDNjODdmLWI2MTctNDRiYy1iYTc1LThkZTJiMTZjMzY0MCJ9 HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...

The final page omits next, marking the end of the list:

HTTP/1.1 200 OK
Content-type: application/json

{
  "id": "73WakrfVbNJBaAmhQtEeDv",
  "url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/",
  "name": "Example JSON Documents Collection",
  "type": ["Collection"],
  "items": [
    {
      "id": "9c7b2e51-0d4a-4c2f-8b3e-1f6a5d8e7c90",
      "url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/9c7b2e51-0d4a-4c2f-8b3e-1f6a5d8e7c90",
      "contentType": "application/json"
    }
  ]
}

Where a Resource carries a key-epoch stamp (see F.4 Key Epochs), its item summary additionally carries it as epoch (optional), mirroring the Resource Metadata property -- so a reader walking a listing can select its epoch key without a per-Resource metadata fetch. A writer-attribution label (see 12.8.1 Writer attribution: writerId and createdBy) is likewise carried as writerId (optional), mirroring the Resource Metadata property.

On a Collection that declares an encryption descriptor (see F. Encryption Scheme Registry), each item's user-writable metadata is stored encrypted (see 12.8 Resource Metadata Data Model). Item summaries for an encrypted Collection therefore carry only server-visible fields (id, url, contentType, and, where declared, epoch and writerId) and omit name; a client that needs names decrypts each Resource's Metadata itself.

Errors (see H. Error Type Registry for canonical examples):

  • not-found (404) -- the Collection does not exist, or the caller has missing or insufficient authorization; per 6. Error Handling a Collection the caller is not authorized to read is indistinguishable from one that does not exist.

11.6 Delete Collection operation

11.6.1 (HTTP API) DELETE /space/{space_id}/{collection_id}/

  • Requires appropriate authorization: a root capability invoked directly by the space's controller. A delegated capability never authorizes this operation, whatever its allowedAction (see #the-container-rule).

  • This operation is idempotent

  • (Assuming the request carries appropriate authorization) Sending a DELETE request to a collection that does not exist (or has already been deleted) results in a 204 success response

Example request:

DELETE /space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/ HTTP/1.1
Host: example.com
Authorization: ...

Example success response:

HTTP/1.1 204 No Content

11.7 Collection Governing History Log

A Collection MAY carry one governing history log: an append-only, client-written log that the server stores at the sub-path /space/{space_id}/{collection_id}/meta/log and from whose head entry the server derives a member of the Collection's Metadata object. The clients that share the Collection write the log and read it. The server stores it, checks a minimal line contract on every write, and serves the derived member to every reader. Which members a log governs, what an entry is beyond its state, and how a reader verifies the log are defined by a governing profile, not by this specification. The encryption descriptor of the 11.1 Collection Metadata Data Model is the first governed member, under the resource log profile of [PWS-EC]; see 11.7.6 The first governed member: encryption.

Support is OPTIONAL and discoverable. A server that implements the sub-resource advertises governed-history-logs in its PWS-EC version entry's features array (see 8.2 Service Description Data Model). A server without the affordance serves a client-written member instead, so a producer MUST consult the token before choosing which form to write. A server that does not implement the sub-resource SHOULD return an unsupported-operation (501) error for requests to its path, and MUST NOT treat them as Resource operations (the path lies under the reserved meta segment; see E.2 Collection-level reserved endpoints).

11.7.1 The sub-resource

The log is a sub-resource of the Collection, addressed beside its Metadata object under the reserved meta segment. It is neither a Resource of the Collection nor a member of the Metadata object:

Authorization follows the Collection. The log is readable under any capability whose invocationTarget covers the Collection URL, so a share grantee or an app reads it with the capability it already holds; a read is also served where the Collection's access-control policy grants reads. A write requires a capability that allows the PUT action at the log's URL; a policy grants no writes to it. As everywhere in this specification, a caller that is not authorized receives the merged not-found (404), per 6. Error Handling.

11.7.2 Line contract

The log body is JSON Lines [JSON-LINES]: one JSON object per line, lines separated by U+000A LINE FEED, with an optional trailing line feed. The whole of what a server requires of the content is:

  • each line is a JSON object carrying a state member whose value is a JSON object;
  • the last line is the head entry, whose state is the current state of what the log governs;
  • the first (genesis) line MAY carry a string parameters.method, the log format identifier the derived member's history.method echoes.

A server MUST refuse a write whose body breaks this contract with an invalid-request-body (400) error: an empty body, a line that is not a JSON object, a line without an object state, or a blank line other than the trailing line feed. A server MUST NOT require anything else of a line. Proofs, hash chaining, the type of a state, and any member name reserved inside state are defined by the governing profile (for the resource log format, [PWS-EC]) and are not checked here. The server stores the log; it does not verify it.

11.7.3 Declaration and the derived member

The guarded create of the log is the declaration. A PUT to the sub-resource carrying If-None-Match: * (see 7.2 Conditional Requests) on a Collection that has no log makes the Collection log-governed. Nothing is added to the Collection's Metadata object, and a Collection stays governed until it is deleted.

From that write on, the server derives the governed member from the head entry: the served value is the head's state with one member stamped on,

"history": {
  "method": "<the genesis entry's parameters.method>",
  "resource": "<the absolute URL of the log>"
}

Where the genesis entry carries no parameters.method, the server serves the head state without history. The derived member appears wherever the Metadata object is served; the stored object holds no copy of it. Derivation is last-line parsing, so the derived member is exactly what a verifying reader computes from the head after stripping history.

A direct write of a governed member through the 11.4 Update (or Create by Id) Collection operation MUST be refused; the member is read-only on that path, and the object's other members update normally. For the encryption descriptor the refusal is encryption-history-log-governed (409).

Declaring governance on a Collection whose Metadata object already holds a client-written value of the governed member MUST be refused. There is no conversion between the two forms in this version: a Collection is provisioned in one form or the other. For the encryption descriptor the refusal is encryption-immutable (409).

11.7.4 Trust

The log is the only authoritative serving of what it governs. The derived member is a projection the server computes by parsing the head line, for its own checks and for readers that do not verify; the server verifies neither entry proofs nor the chain. A verifying reader reads the log, verifies it under the governing profile, and compares the derived member to the verified head's state after stripping history. A derived member that differs is a stale projection, and the reader acts on the verified head instead (for the resource log format, the equality check of [PWS-EC]).

11.7.5 Versioning

The log carries its own monotonic version, exposed as a strong ETag validator on read, on the same terms as the Collection Metadata object (see 11.1.1 Versioning). A server implementing the sub-resource MUST maintain it and MUST honor If-Match and If-None-Match: * on write, evaluated atomically with the write. A log that does not exist has no validator, and If-None-Match: * succeeds exactly then. Because a log write changes the derived member, it also advances the Collection's metaVersion: a client holding that ETag sees the change on its next conditional read. A Collection re-created under the same id restarts the log's version, so a client MUST NOT compare validators across a delete and re-create.

11.7.6 The first governed member: encryption

The encryption descriptor (see 11.1 Collection Metadata Data Model) is the first, and in this version the only, member a profile governs. On a log-governed Collection the head entry's state is an encryption descriptor, and the Collection is an encrypted Collection from the declaration onward: the envelope rule applies to its Resources and its Metadata object, and the plaintext member is excluded, exactly as when the descriptor is written on the Metadata object. The profile's state carries a type member naming its schema ([PWS-EC]); the server stores it with the rest of the state and does not interpret it.

On every log write the server runs, on the head state, the same checks the 11.4 Update (or Create by Id) Collection operation runs on a supplied descriptor: the shape validation of F.4.3 Server validation and the scheme recognition rule of F. Encryption Scheme Registry. On an append it also runs the transition invariants against the prior head's state: epochs is append-only, currentEpoch never moves backwards, hmac is permanent once present, and the descriptor's scheme and version are set-once, version-monotonic. A violating write MUST be refused with the error the Update Collection operation raises for the same change, and MUST leave the log unchanged. This is the one clause of this mechanism that is specific to encryption; a later governed member adds its own.

11.8 Read Governing History Log Operation

11.8.1 (HTTP API) GET /space/{space_id}/{collection_id}/meta/log

  • Requires appropriate authorization
    • For example, when using zCaps for authorization, the request must either: be signed by the space's controller, or invoke a delegated capability that allows the GET action whose target covers the Collection URL
  • Returns the log body verbatim, as text/jsonl, with an ETag header over the log's version (see 11.7.5 Versioning)
  • A request carrying If-None-Match with the current validator is answered 304 Not Modified with the ETag and no body (see 7.1 Caching)

Example request:

GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/meta/log HTTP/1.1
Host: example.com
Accept: text/jsonl
Authorization: ...

Example success response (one entry per line; the entry members beyond state are the governing profile's):

HTTP/1.1 200 OK
Content-type: text/jsonl
ETag: "z3fkq.2"

{"versionId":"1-...","versionTime":"2026-09-07T10:00:00Z","parameters":{"method":"resource-log:0.1","scid":"z..."},"state":{"type":"WasEpochConfiguration","scheme":"edv","currentEpoch":"urn:uuid:8d3f...","epochs":[...]},"proof":[...]}
{"versionId":"2-...","versionTime":"2026-09-08T09:30:00Z","parameters":{},"state":{"type":"WasEpochConfiguration","scheme":"edv","currentEpoch":"urn:uuid:c17a...","epochs":[...]},"proof":[...]}

Errors (see H. Error Type Registry for canonical examples):

  • not-found (404) -- the Collection does not exist, it carries no log, or the caller has missing or insufficient authorization; per 6. Error Handling a log the caller is not authorized to read is indistinguishable from one that does not exist.
  • unsupported-operation (501) -- the server does not implement the optional governing history log.

11.9 Write Governing History Log Operation

The body of a write is the whole log. A client appends by sending the stored bytes verbatim followed by the new line, under an If-Match precondition carrying the validator of the log it read. A client creates the log, and so declares the Collection governed, with If-None-Match: *. A client MUST carry one of the two preconditions on every write. A server evaluates the precondition it is given atomically with the write. On a Collection that already carries a log, a server MUST also check, atomically with the write, that the body fast-forwards the stored log: the stored bytes verbatim followed by exactly one new line. A body the stored log is not a prefix of (a stale read, or a rewritten prefix) MUST be refused with a precondition-failed error (412), whether or not the write carries If-Match; a body that extends the stored bytes by no line or by several MUST be refused with an invalid-request-body error (400). A write capability can therefore add to a log but not erase or rewrite it. The server also checks the line contract and, for a governed encryption descriptor, the head-state transition (11.7.6 The first governed member: encryption). It verifies nothing inside an appended entry: a bad proof or a broken hash link is a chain break that a verifying reader detects under the governing profile.

11.9.1 (HTTP API) PUT /space/{space_id}/{collection_id}/meta/log

  • Requires appropriate authorization
    • For example, when using zCaps for authorization, the request must either: be signed by the space's controller, or invoke a delegated capability that allows the PUT action whose target covers the log's URL
  • The request body is the whole log as text/jsonl
  • Carries If-None-Match: * (guarded create, the declaration) or If-Match: "<etag>" (compare-and-swap append); see 7.2 Conditional Requests
  • Does not create a Collection: a write to the log path of a nonexistent Collection returns a not-found (404) error
  • Returns a 204 success response, with an ETag header carrying the log's new validator

Example request (the guarded create, with a genesis entry):

PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/meta/log HTTP/1.1
Host: example.com
Content-Type: text/jsonl
If-None-Match: *
Authorization: ...

{"versionId":"1-...","versionTime":"2026-09-07T10:00:00Z","parameters":{"method":"resource-log:0.1","scid":"z..."},"state":{"type":"WasEpochConfiguration","scheme":"edv","currentEpoch":"urn:uuid:8d3f...","epochs":[...]},"proof":[...]}

Example success response:

HTTP/1.1 204 No Content
ETag: "z3fkq.1"

From this point the Collection Metadata object's encryption member reads as the entry's state plus history:

"encryption": {
  "type": "WasEpochConfiguration",
  "scheme": "edv",
  "currentEpoch": "urn:uuid:8d3f...",
  "epochs": [...],
  "history": {
    "method": "resource-log:0.1",
    "resource": "https://example.com/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/meta/log"
  }
}

Errors (see H. Error Type Registry for canonical examples):

  • not-found (404) -- the Collection does not exist (this operation does not create one), or the caller has missing or insufficient authorization, per 6. Error Handling.
  • invalid-request-body (400) -- the body breaks the line contract (11.7.2 Line contract); or, for a governed encryption descriptor, the head state is malformed or the append violates an epoch invariant, as the Update Collection operation would report it.
  • unsupported-encryption-scheme (400) -- the head state declares a scheme (or a version of one) the server does not recognize.
  • encryption-immutable (409) -- the guarded create targets a Collection whose Description already holds a client-written encryption descriptor; or the append changes the scheme, moves the version backward, or changes or drops the hmac member.
  • precondition-failed (412) -- an If-Match / If-None-Match: * precondition evaluated false: another writer appended first, or the log already exists; see 7.2 Conditional Requests.
  • unsupported-operation (501) -- the server does not implement the optional governing history log.

12. Resources and Blobs

12.1 Blob Data Model

A unit of data, in transit or at rest. (As described in the W3C FileAPI: Blob Interface).

Blob properties:

When a Blob is stored as a Resource, its type and size are reported as the contentType and size properties of the Resource's Metadata object (see 12.8 Resource Metadata Data Model), and the stored bytes are returned verbatim on reads (see 12.3 Content Types and Representations).

12.2 Resource Data Model

A resource is a named (addressable) Blob stored in a given collection, with metadata. The data model is derived from W3C FileAPI: File Interface, but with the addition of a few crucial properties.

In similar storage systems, a resource is called "File", "Object", "Document", "Row", "Graph", and so on.

Resource properties:

12.3 Content Types and Representations

A Resource has exactly one current representation: the stored bytes plus the content type they are stored under. Every successful write (POST or PUT) replaces that representation entirely -- including replacing a representation previously stored under a different content type. Servers are not required to store, convert between, or negotiate multiple representations of a Resource.

Writing. A Resource write request MUST carry a Content-Type header; a server MUST reject a write without one with a missing-content-type (400) error. The request body is interpreted according to its content type:

Reading. A GET returns the current representation, verbatim, with the stored content type as the response Content-Type. Because Resources are single-representation, the request's Accept header is advisory: a server MAY ignore it, and MUST NOT reject a Resource read for lack of an acceptable representation (no 406 Not Acceptable) -- the stored representation is always the answer. A HEAD request returns the same headers without the body; the response Content-Type and Content-Length correspond to the contentType and size properties of the Resource's Metadata object (see 12.8 Resource Metadata Data Model).

Server-generated documents. API documents generated by the server -- Metadata objects, listings, quota reports -- MUST be available as application/json. Linkset documents use application/linkset+json [RFC9264], and error responses use application/problem+json [RFC9457] (see 6. Error Handling). Other representations of these documents (negotiated via Accept) MAY be offered in addition to, never instead of, the JSON baseline.

12.4 Create Resource (Add Resource to Collection) Operation

12.4.1 (HTTP API) POST /space/{space_id}/{collection_id}/

The request MAY include a Key-Epoch header declaring the key epoch the body was encrypted under (see F.4 Key Epochs); the value is stored as the Resource Metadata epoch property. When absent, any stored epoch stamp is cleared. The request MAY likewise include a Writer-Id header declaring the writing agent's attribution label (see 12.8.1 Writer attribution: writerId and createdBy); the value is stored as the Resource Metadata writerId property, and when absent any stored writerId is cleared.

Example request (adds a JSON object to the messages collection). The server generates the Resource id and returns it in the Location response header. A client that chooses the id uses 12.6 Update (or Create By Id) Resource Operation instead.

POST /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/ HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...

{"message":"hi"}

Example success response:

HTTP/1.1 201 Created
Content-type: application/json
Location: https://example.com/space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/6b5be748-5f39-4936-a895-409e393c399c

Example request (multipart upload of a photo to the photos collection, on a server that supports the OPTIONAL multipart form; see 12.3 Content Types and Representations). The stored content type is the file part's own type, image/png, not multipart/form-data:

POST /space/81246131-69a4-45ab-9bff-9c946b59cf2e/photos/ HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----boundary314
Authorization: ...

------boundary314
Content-Disposition: form-data; name="file"; filename="sunset.png"
Content-Type: image/png

...binary PNG bytes...
------boundary314--

Example success response:

HTTP/1.1 201 Created
Location: https://example.com/space/81246131-69a4-45ab-9bff-9c946b59cf2e/photos/d2887cf0-186f-4e2e-a575-c9ce1b9d8a45

Errors (see H. Error Type Registry for canonical examples):

12.5 Read Resource Operation

A read returns the Resource's single stored representation, with the stored content type; see 12.3 Content Types and Representations for content type and Accept header handling (including the HEAD variant).

12.5.1 (HTTP API) GET /space/{space_id}/{collection_id}/{resource_id}

  • Requires appropriate authorization
    • For example, when using zCaps for authorization, the request must either: be signed by the resource's or the space's controller, or invoke a delegated capability that allows the GET action

Example request to retrieve a resource:

GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/hello-world HTTP/1.1
Host: example.com
Accept: application/json

Example success response:

HTTP/1.1 200 OK
Content-type: application/json

{"message":"hi"}

Errors (see H. Error Type Registry for canonical examples):

  • not-found (404) -- the Resource does not exist, or the caller has missing or insufficient authorization; per 6. Error Handling a resource the caller is not authorized to read is indistinguishable from one that does not exist.

12.6 Update (or Create By Id) Resource Operation

When creating or modifying a Resource via PUT, the client specifies the id of the Resource. This Resource id MUST NOT collide with the list of E.2 Collection-level reserved endpoints.

12.6.1 (HTTP API) PUT /space/{space_id}/{collection_id}/{resource_id}

  • Requires appropriate authorization
    • For example, when using zCaps for authorization, the request must either: be signed by the resource's or the space's controller, or invoke a delegated capability that allows the PUT action
  • This operation is idempotent
  • Returns a 204 success response

The request MAY include a Key-Epoch header declaring the key epoch the body was encrypted under (see F.4 Key Epochs); the value is stored as the Resource Metadata epoch property. When absent, any stored epoch stamp is cleared. The request MAY likewise include a Writer-Id header declaring the writing agent's attribution label (see 12.8.1 Writer attribution: writerId and createdBy); the value is stored as the Resource Metadata writerId property, and when absent any stored writerId is cleared.

Example request to create a resource via PUT:

PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/hello-world HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...

{"message":"hi"}

Example success response:

HTTP/1.1 204 No Content

Example request to update the created resource via PUT:

PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/hello-world HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...

{"message":"no I've changed my mind"}

Example success response:

HTTP/1.1 204 No Content

Example request (uploading a binary Blob via PUT; the bytes are stored verbatim under the supplied content type, see 12.3 Content Types and Representations). The Digest header binds the body to the request signature under the baseline authorization profile (see Request Body Integrity in [PWS-AUTHZ]):

PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e/photos/sunset.png HTTP/1.1
Host: example.com
Content-Type: image/png
Digest: mh=uEiCPO-qYr-z0GYV5F75-N1l8Rhjv4xIkKZsnbTZeZ7emSA
Authorization: ...

...binary PNG bytes...

Example success response:

HTTP/1.1 204 No Content

Errors (see H. Error Type Registry for canonical examples):

This operation accepts the If-Match / If-None-Match write preconditions described in 7.2 Conditional Requests.

12.7 Delete Resource Operation

12.7.1 (HTTP API) DELETE /space/{space_id}/{collection_id}/{resource_id}

  • Requires appropriate authorization

    • For example, when using zCaps for authorization, the request must either: be signed by the resource's or the space's controller, or invoke a delegated capability that allows the DELETE action
  • This operation is idempotent

  • (Assuming the request carries appropriate authorization) Sending a DELETE request to a resource that does not exist (or has already been deleted) results in a 204 success response

The request MAY include a Writer-Id header declaring the deleting agent's attribution label (see 12.8.1 Writer attribution: writerId and createdBy). A deletion is labelled on the same declare-or-clear terms as a content write. Where the server keeps a tombstone, the label it carries attributes the deletion (see the tombstone notes of the changes profile).

Example request to delete a resource via DELETE:

DELETE /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/hello-world HTTP/1.1
Host: example.com
Authorization: ...

Example success response:

HTTP/1.1 204 No Content

Errors (see H. Error Type Registry for canonical examples):

  • not-found (404) -- the caller has missing or insufficient authorization. Because DELETE is idempotent, an authorized request for an already-absent resource returns 204; a request that lacks sufficient authorization instead returns 404, per 6. Error Handling, so that an unauthorized caller cannot probe for existence.

12.8 Resource Metadata Data Model

Each Resource has an associated Metadata object, addressable at the reserved /meta path segment under the Resource URL (one of the E.3 Resource-level reserved endpoints). The metadata endpoints are OPTIONAL; a server that does not implement them SHOULD return an unsupported-operation (501) error for requests to /meta paths. (A Collection has a Metadata object of its own, at the reserved meta segment under the Collection URL; see 11.1 Collection Metadata Data Model.)

The Metadata object is a JSON document containing two groups of properties:

Server-managed properties (contentType and size are REQUIRED in every Metadata object; the timestamps and createdBy are OPTIONAL):

This metadata model does not mandate a stored etag / version property as such; a Resource's version is always exposed as an HTTP ETag validator, per 7.2 Conditional Requests.

Editor's note
The broader Transaction (multi-write atomic) mechanism remains deferred.

User-writable properties:

On a Collection that declares an encryption descriptor (see F. Encryption Scheme Registry), the user-writable custom object is stored encrypted: its value is an envelope of the Collection's declared scheme (the same envelope profile used for a Resource's content). The server-managed top-level properties (contentType, size, the timestamps, and createdBy) remain plaintext -- the server needs them for listings, GET/HEAD headers, quotas, and, in the case of createdBy, because it is the very identity the server authenticated on the creating write. The server validates the custom envelope structurally on write (rejecting a plaintext custom with an encryption-scheme-mismatch) and never decrypts it; a client holding the keys decrypts custom back to { name, tags, ... } after reading. An empty custom object is accepted as well and clears the stored value, and so does an omitted custom, here as on a plaintext Collection. Because the server cannot read an encrypted name, 11.5 List Collection operation summaries for an encrypted Collection carry no name.

A Resource's Metadata object is created and deleted together with the Resource itself: it comes into existence (with only server-managed properties) when the Resource is created and is removed when the Resource is deleted. There is no DELETE /meta operation; to clear the user-writable properties, send a PUT with an empty custom object ({ "custom": {} }) or an empty body object ({}); both forms clear on an encrypted Collection too.

12.8.1 Writer attribution: writerId and createdBy

createdBy and writerId (both optional) answer different questions, and neither can substitute for the other:

  • createdBy is a keyed, server-verified identity: the did the server itself authenticated on the creating capability invocation. It is server-managed, read-only, and names the creator only; later writes never change it.
  • writerId is an unkeyed, client-declared attribution label: an opaque string the writing agent volunteers about itself, naming which writing agent made the Resource's last content write. The server MUST store it and serve it verbatim, and MUST NOT verify it, MUST NOT compute or default it, and MUST NOT use it as an input to authorization or any other server decision. It is advisory replication metadata, nothing more.

These two leave room for a third, distinct axis: a client-asserted identity -- a did the controller writes and maintains about another party. The Collection-level generator property (see 11.1 Collection Metadata Data Model), whose id names the requester (an app, service, or agent) a Collection was provisioned for, is this axis. Unlike writerId it is an identity claim and a stable join key; unlike createdBy it is a controller assertion rather than a server-verified fact, and a reader treats it as exactly that.

The label is carried by the Writer-Id request header, and it is an opaque non-empty string. A present but empty or malformed value is an invalid-request-body error.

The reason writerId is necessary: several distinct writing agents may share one invoking did (for example, the same delegated capability held by a user's several installs of one app), so createdBy (or any other server-verified identity) structurally cannot distinguish exactly the writers that most often race each other. A label on each content write lets replication clients break same-timestamp last-writer-wins ties with a shared, deterministic (updatedAt, writerId) sort key. It also helps them recognize their own writes echoed back on the changesfeed without a fetch (on an encrypted Collection, without a decrypt), and attribute each content write for display.

Requirements on the writing client:

  • A writerId MUST NOT be derived from any secret, key material, or other identity: it is an attribution label, never an identity, and it MUST NOT be treated as one (that is the createdBy axis). A random value minted per writing agent is RECOMMENDED.
  • A server ignores a writerId member in the body of an Update Resource Metadata request, so a client performing a read-modify-write of a Metadata object need not remove the one it read.

Privacy considerations: unlike the encrypted custom object, writerId is plaintext to the server, and a stable per-agent label reveals to the server (and to any reader of the Collection) how many writing agents stand behind one invoking did, durably per content write -- information the server could otherwise only approximate from traffic analysis. This is accepted as the explicit price of pre-fetch echo suppression, which only a server-visible field can provide, and it is bounded: the label reveals the multiplicity of one principal's own writing agents, not any cross-party relationship (the axis the F.4 Key Epochs recipients/grantees separation protects). And because it is random, unkeyed, and clearable, it cannot be linked across users or upgraded into a tracking identity. The field is optional end-to-end: a client that does not want the server to hold even this simply never declares one.

12.9 Read Resource Metadata Operation

12.9.1 (HTTP API) GET /space/{space_id}/{collection_id}/{resource_id}/meta

  • Requires appropriate authorization
    • For example, when using zCaps for authorization, the request must either: be signed by the resource's or the space's controller, or invoke a delegated capability that allows the GET action

Example request:

GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/hello-world/meta HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...

Example success response:

HTTP/1.1 200 OK
Content-type: application/json

{
  "contentType": "application/json",
  "size": 16,
  "createdAt": "2026-06-10T09:12:00Z",
  "updatedAt": "2026-06-12T13:25:00Z",
  "createdBy": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
  "writerId": "z6fVXHKn8PdQm2Rt",
  "custom": {
    "name": "Hello World greeting",
    "tags": { "project": "demo", "status": "draft" }
  }
}

Errors (see H. Error Type Registry for canonical examples):

  • not-found (404) -- the Resource does not exist, or the caller has missing or insufficient authorization; per 6. Error Handling a Metadata object the caller is not authorized to read is indistinguishable from one that does not exist.
  • unsupported-operation (501) -- the server does not implement the optional metadata endpoints.

12.10 Update Resource Metadata Operation

A PUT to the /meta endpoint is a full replacement of the Metadata object's custom object: the stored custom object is replaced by the one in the request body, so any user-writable property omitted from it is cleared (and a request body with no custom property clears them all).

One other top-level member is also writable. A request MAY carry epoch to set the Resource's key-epoch stamp; a request that omits it preserves the stored stamp, because the stamp describes the content write, not the metadata write (see F.4 Key Epochs).

The Resource's writerId label is not writable here. It names the writer of the Resource's content, so this operation leaves the stored label as it is (see 12.8.1 Writer attribution: writerId and createdBy).

Server-managed properties are not affected by this operation; a server MUST ignore any top-level properties other than custom and epoch present in the request body (so that a client may read the Metadata object, modify it, and PUT it back without first stripping the server-managed properties).

This operation writes the Metadata object only. It does not change the Resource's stored representation, so a server MUST NOT change the Resource's updatedAt or the ETag validator of the Resource itself in response to it. The Metadata object's own ETag changes with each such write.

Unlike the 12.6 Update (or Create By Id) Resource Operation, a PUT to /meta does not create anything: a Metadata object cannot exist apart from its Resource, so a PUT to the /meta path of a nonexistent Resource returns a not-found (404) error.

12.10.1 (HTTP API) PUT /space/{space_id}/{collection_id}/{resource_id}/meta

  • Requires appropriate authorization
    • For example, when using zCaps for authorization, the request must either: be signed by the resource's or the space's controller, or invoke a delegated capability that allows the PUT action
  • This operation is idempotent
  • Returns a 204 success response

Example request:

PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/hello-world/meta HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...

{
  "custom": {
    "name": "Hello World greeting",
    "tags": { "project": "demo", "status": "draft" }
  }
}

Example success response:

HTTP/1.1 204 No Content

Errors (see H. Error Type Registry for canonical examples):

13. Linksets

Note
Linksets are an OPTIONAL extension. A server MAY omit this feature-detection mechanism entirely and remain conformant; see 2.4 Scope and Conformance Profiles.

Linksets (from [RFC9264]) are the feature detection and extension mechanism for Space- and Collection-scoped facts; server-wide facts, and the versions the server speaks, are in the service description (see 8. Service Description). Linksets can be discovered, via the linkset property, from the following:

13.1 Space Linkset

The space linkset resource (one of the E.1 Space-level reserved endpoints), located at /space/{space_id}/linkset contains a set of links to auxiliary resources and extension points:

Example space linkset resource request and response:

GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/linkset HTTP/1.1
Accept: application/linkset+json
Authorization: ...

Response:

HTTP/1.1 200 OK
Content-type: application/linkset+json

{
  "linkset": [
    {
      "anchor": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/",
      "https://w3id.org/pws#policy": [
        {
          "href": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/policy",
          "type": "application/json"
        }
      ],
      "https://w3id.org/pws#backends-available": [
        {
          "href": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/backends",
          "type": "application/json"
        }
      ],
      "https://w3id.org/pws#quotas": [
        {
          "href": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/quotas",
          "type": "application/json"
        }
      ],
      "service": [
        {
          "href": "https://example.com/service",
          "type": "application/json"
        }
      ]
    }
  ]
}

13.2 Collection Linkset

The collection linkset resource (one of the E.2 Collection-level reserved endpoints), located at /space/{space_id}/{collection_id}/linkset contains a set of links to auxiliary resources and extension points:

Example collection linkset resource request and response:

GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/linkset HTTP/1.1
Accept: application/linkset+json
Authorization: ...

Response:

HTTP/1.1 200 OK
Content-type: application/linkset+json

{
  "linkset": [
    {
      "anchor": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/",
      "https://w3id.org/pws#policy": [
        {
          "href": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/policy",
          "type": "application/json"
        }
      ],
      "https://w3id.org/pws#backend": [
        {
          "href": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/backend",
          "type": "application/json"
        }
      ],
      "https://w3id.org/pws#quota": [
        {
          "href": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/quota",
          "type": "application/json"
        }
      ],
      "service": [
        {
          "href": "https://example.com/service",
          "type": "application/json"
        }
      ]
    }
  ]
}

14. Backends

Note
This section defines an OPTIONAL extension. A server MAY skip pluggable backends entirely and remain conformant; see 2.4 Scope and Conformance Profiles.

Backends are an optional infrastructure concern that is orthogonal to the hierarchical Spaces Repository > Space > Collection > Resource storage model. They exist to serve advanced use cases that need fine-grained control over storage configurations.

Available backends are registered on the Space level, as a combination of server-side configuration and client-side "Bring Your Own Storage" registration.

For example, on the server configuration side, a given server might support several backends -- a file system default backend, an EDV encrypted backend, and a PostgreSQL database backend. And on the client side, a user might register an external storage provider by connecting to their Dropbox account.

When a Collection is created, the client can optionally specify the preferred backend for that Collection. If no preferred backend is specified, one is assigned by the server (usually the default backend).

An implementer or client of a given server can omit the backend property when creating a Collection. By default, if not specified, all Collections are assigned the default backend. Omitting it on a later update of the Collection's Metadata object keeps the backend already selected.

14.1 Backend Data Model

A Backend description object advertises a backend's identity, who operates it, and its persistence, so that clients can select a suitable backend for each Collection (and so that storage management UIs can present meaningful choices to users). Quota constraints on a backend are reported separately, in the Quota report (see 15. Quotas).

Backend description properties:

Note that client-side encryption is deliberately not a backend property: an encrypted document is opaque client-encrypted JSON that any document-capable backend stores faithfully with no server cooperation, and whether a given Collection is encrypted varies per Collection on the very same backend. Encryption is therefore a property of a Collection's data (signaled at the Collection level and held in the client's keys), not a property of the backend. Concretely, this signal is the Collection's optional encryption descriptor (see 11.1 Collection Metadata Data Model): a non-secret declaration any authorized reader discovers from the Collection's Metadata object, while the keys stay in the client.

Every backend on which a Collection may be created honors the conditional writes of 7.2 Conditional Requests; this holds regardless of what persistence primitives the underlying storage engine offers, since the server mediates every write.

Editor's note
The schema of a backend's connection configuration (server-internal connections vs OAuth-style setup flows for external providers) is not yet specified.

14.2 Space Backends Available

The list of backends registered on a Space is discoverable via the backends-available relation in the linkset (see 13.1 Space Linkset).

Example request:

GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/backends HTTP/1.1
Accept: application/json
Authorization: ...

Example success response:

HTTP/1.1 200 OK
Content-type: application/json

[
  {
    "id": "default",
    "name": "Server Filesystem",
    "managedBy": "server",
    "persistence": "durable"
  },
  {
    "id": "dropbox",
    "name": "Dropbox",
    "managedBy": "external",
    "persistence": "durable"
  },
  {
    "id": "edv",
    "name": "Encrypted Data Vault",
    "managedBy": "server",
    "persistence": "durable"
  }
]

14.3 Collection Backend Selected

Each collection has an optional backend property that is set during its creation (see 11.1 Collection Metadata Data Model). If not specified, it is assumed to have the id of default. The selected backend is discoverable via the backend relation in the collection's linkset (see 13.2 Collection Linkset).

Editor's note
**Replicas (planned generalization).** The single backend property is expected to generalize to one or more _replicas_ per Collection, each replica hosted on a backend, with the current single-backend configuration as the degenerate one-replica case. (Collections do not "live" on a single backend; backends are infrastructure endpoints, not a fourth tier of the storage hierarchy.) That generalization will bring with it:
  • Client-evaluated replica roles (active vs available) -- this specification will define the vocabulary for declaring replica topology; the logic for selecting the active replica (based on latency, battery, network status) belongs to the client SDK.
  • A sync-status vocabulary per replica (e.g. synced, syncing, stale); the exact state machine, including how "cold boots" of volatile backends are surfaced, is to be determined.
  • Logical vs physical byte accounting in quota reports: the deduplicated size of the user's data vs the total bytes consumed across all replicas. These only diverge once replication exists, so the 15. Quotas report currently carries plain per-backend usage.
  • Concurrency control (If-Match ETags) for updates to replica topology, building on the per-Resource conditional-writes mechanism (see 7.2 Conditional Requests) generalized to multi-replica writes.
Editor's note
**Lifecycle configuration (administrative retention).** Retention rules such as "delete guest data after one hour" (a time-to-live, a grace period, and an expiry action) are expected to be configurable at the Space or Collection level. Lifecycle configuration is deliberately distinct from both the backend's persistence property (a technical attribute of the storage engine) and from access-control policy (who may act on the data). The property naming is to be determined, precisely to avoid overloading the term "policy".

15. Quotas

Note
This section defines an OPTIONAL extension. A server MAY omit quota reporting and enforcement entirely and remain conformant; see 2.4 Scope and Conformance Profiles.

Quota reporting and enforcement are optional, and support is backend-dependent. The quota API serves two distinct consumers:

Quota endpoints follow the same maximum-privacy invariant as the rest of this specification (see 6. Error Handling): a caller not authorized to read a Space's quota report MUST receive a not-found (404) error, never a 403.

15.1 (HTTP API) GET /space/{space_id}/quotas

Returns the storage report for a Space, grouped by backend, and is discoverable via the quotas relation in the Space's linkset (see 13.1 Space Linkset). Each entry in the backends array carries:

Example request:

GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/quotas HTTP/1.1
Accept: application/json
Authorization: ...

Example success response:

HTTP/1.1 200 OK
Content-type: application/json

{
  "respondedAt": "2026-06-12T13:25:00Z",
  "backends": [
    {
      "id": "default",
      "name": "Server Filesystem",
      "managedBy": "server",
      "state": "ok",
      "usageBytes": 524288000,
      "limit": { "capacityBytes": 10737418240, "isUnlimited": false },
      "restrictedActions": [],
      "measuredAt": "2026-06-12T13:25:00Z"
    },
    {
      "id": "dropbox",
      "name": "Dropbox",
      "managedBy": "external",
      "state": "near-limit",
      "usageBytes": 314572800,
      "limit": { "capacityBytes": 367001600, "isUnlimited": false },
      "constraints": { "maxUploadBytes": 157286400 },
      "restrictedActions": [],
      "measuredAt": "2026-06-12T13:24:45Z"
    }
  ]
}

15.2 (HTTP API) GET /space/{space_id}/quotas?include=collections

With the include=collections query parameter, each backend entry additionally carries a usageByCollection array, giving storage-manager applications a full breakdown in a single request while keeping the hot-path payload lean:

HTTP/1.1 200 OK
Content-type: application/json

{
  "respondedAt": "2026-06-12T13:25:00Z",
  "backends": [
    {
      "id": "default",
      "name": "Server Filesystem",
      "managedBy": "server",
      "state": "ok",
      "usageBytes": 524288000,
      "limit": { "capacityBytes": 10737418240, "isUnlimited": false },
      "restrictedActions": [],
      "measuredAt": "2026-06-12T13:25:00Z",
      "usageByCollection": [
        { "id": "credentials", "usageBytes": 419430400 },
        { "id": "inbox", "usageBytes": 104857600 }
      ]
    }
  ]
}

15.3 (HTTP API) GET /space/{space_id}/{collection_id}/quota

Returns the storage report for a single Collection, scoped to that Collection's backend (the entry has the same shape as a backends array entry, with usageBytes reflecting only this Collection's consumption). It is discoverable via the quota relation in the Collection's linkset (see 13.2 Collection Linkset).

Not all backends support per-collection accounting. Where unsupported, the server returns an unsupported-operation (501) error.

Errors (see H. Error Type Registry for canonical examples):

A. Pagination

This appendix is normative. It defines the full pagination profile summarized in 7.3 Paginated List Responses.

The list operations -- 9.2 List Spaces Operation, 10.5 List All Collections operation, and 11.5 List Collection operation -- return a collection of items in the common envelope (url, totalItems, items). A Space or Collection may hold more items than is practical to return in a single response, so servers MAY paginate these responses, returning one page of items at a time. Pagination is OPTIONAL: a server that returns every item in a single response (as the examples in those sections show) is conformant, and a client MUST be prepared for either behavior.

This profile uses cursor-based (also called "keyset") pagination rather than numeric offsets. A cursor identifies a position in a stably ordered result set, so paging stays correct and cheap even as items are concurrently added or removed, and at any depth into a large collection.

A.1 Ordering

A paginated list operation MUST return items in a stable total order: an ordering that is deterministic and in which no two items compare equal. The default order is ascending by item id (which is unique within its parent, see 4. Identifiers). A server MAY offer additional orderings, but every order it supports for pagination MUST be stable and total -- if the primary sort key is not unique (for example, a timestamp), the server MUST break ties on a unique key (such as id) so that the combined order is total. This stable order is what a cursor seeks within; an unstable order cannot be paginated correctly.

A.2 Requesting a page

A client requests pagination with two OPTIONAL query parameters:

The first page is requested with no cursor (a bare limit, or neither parameter).

A.3 The paginated response

A paginated response carries the usual envelope, plus a next member when more items may follow:

A client consumes a multi-page list by following next from each response until a response omits it. A server SHOULD ensure that an item present throughout the traversal appears exactly once across the pages; an item added or removed concurrently with the traversal MAY or MAY NOT appear. Snapshot consistency (a paginated traversal observing a single point in time) is permitted but not required. A server MAY encode a snapshot identifier into the cursor to provide such consistency.

A cursor that is malformed, or that a server can no longer honor (for example, an expired snapshot), is rejected with an invalid-cursor (400) error. As with other request-validation failures, a server MUST verify the caller's authorization for the target before validating the cursor, so the error is only ever observed by a caller already authorized to list that target; an under-authorized caller receives the merged not-found (404) instead, per 6. Error Handling.

A.4 Pagination parameters and authorization

A capability authorizes a list target independent of which page is being read: a capability that authorizes listing a Space, Collection, or Spaces Repository authorizes retrieval of every page of that list. The limit and cursor parameters select a page within an already-authorized target; they do not narrow, widen, or otherwise change the target a capability must match (see Capability Invocation in [PWS-AUTHZ]). A server MUST NOT require a distinct capability per page.

B. Space Controller DID Method Registry

Note
Every method in this registry other than did:key is OPTIONAL to support. A server that supports only did:key Space controllers is conformant; a server that does support a registered method MUST follow that method's profile here.

This registry lists the DID methods a Space controller (and, by the current-key-set rule of [PWS-AUTHZ], any DID signing in a capability chain) may use, and the verification a server performs for each. See 5.2 Space controller and the Root of Trust for the method-neutral rules: refusal of unsupported methods and the resolve-before-store rule for a proposed controller. The current-key-set rule, which makes the resolved document the set of accepted keys, is the authorization profile's.

Method Support Kind Self-hosted history layout Profile
did:key REQUIRED static n/a B.2 did:key controller profile
did:webvh OPTIONAL verified log /space/{space_id}/{collection_id}/did.jsonl B.3 did:webvh controller profile

B.1 Verified-log DID methods

A verified-log DID method is one whose DID document is derived from a self-certifying history (a log of entries, each bound to the one before it and to the DID's own identifier) rather than from the identifier alone. This subsection states the rules common to every such method in the registry; a method's profile adds the log layout and the verification its own specification requires.

A server that supports a verified-log method MUST resolve a DID of that method by reading its history and fully verifying it, as the method's specification defines, before using the resulting DID document. The history is verified rather than trusted. Once a Space names such a DID as controller, the writes that extend that history may themselves be authorized by the very document being resolved, so an unverified history would let whoever can write it rewrite the Space's root of trust.

B.1.1 Self-hosted histories

The history of a verified-log controller MAY be stored on the PWS server itself, as one or more Resources in a Collection of a Space on that server. The method's standard DID-to-URL transformation then lands on /space/{space_id}/{collection_id}/ followed by the method's history file name(s); each profile gives the exact layout. Such a controller is self-hosted. The Space hosting the history need not be the Space the controller controls, since the DID names the history's own Space and Collection.

A server that supports a verified-log method MUST resolve self-hosted DIDs of that method by reading the history out of its own storage. A network fetch of its own URL is not a substitute: it would pass through the history Collection's access-control policy and through any intermediary in front of the server. Resolution reads the history regardless of the Collection's policy, so a capability-gated history still resolves for authorization while remaining unreadable to clients that hold no capability for it. A server MAY accept only self-hosted controllers of a method and refuse every other DID of that method (one whose history lives on another host) with invalid-request-body (400), the same way it refuses an unsupported method.

Hosting a history under a Space's path is not an endorsement by that Space's controller: any party holding a write grant there can store a resolvable history. A verified-log DID is self-certifying through its own identifier and history. It acquires authority in PWS only by being referenced, as a Space's stored controller or as the controller of a delegated capability, and not by where its history is stored.

A server MAY cache a resolved document. It MUST drop the cached entry whenever the history the document was resolved from may have changed, so that a write to a self-hosted history (such as one adding or removing a key) is reflected by the next verification that resolves that DID.

B.2 did:key controller profile

The mandatory floor. A conformant server MUST accept a did:key using the Multikey encoding of Ed25519 elliptic curve keys, as specified in the Multikey section of the CID spec, as a Space controller. The DID document is derived from the identifier alone; there is nothing to resolve or verify beyond the key encoding, and the document never changes.

B.3 did:webvh controller profile

A verified-log method; the rules of B.1 Verified-log DID methods apply.

History layout. The history is the method's did.jsonl log. Self-hosted, the DID did:webvh:{scid}:{host}:space:{space_id}:{collection_id} resolves from the Resource at https://{host}/space/{space_id}/{collection_id}/did.jsonl under the method's standard DID-to-HTTPS transformation. The {collection_id} is any Collection id; PWS Collection ids are restricted to characters the method's path encoding leaves untouched, so the mapping is direct. A server that accepts did:webvh controllers accepts a moved log whose current id is self-hosted. The log's earlier entries may name a different host or Space; what the server verifies is the log as it stands under its current id.

Verification. Before using the resolved document the server MUST verify the log as the method's specification defines: the SCID is checked against the log's first entry, the hash chain of entries is verified, pre-rotation commitments (nextKeyHashes) are enforced, and each entry's proof is verified against the update keys authorized by the entry before it.

C. Policy Type Registry

This appendix is normative.

This registry lists the type values an access control policy may carry (see 5.4 Access Control Policies) and the specification that defines each one. A server evaluates a policy by looking its type up here. A type not in this registry is unrecognized and grants nothing, per the fail-closed rule. Registering a type adds a row naming the specification that defines it; this specification defines no policy type itself.

type Grants Defined by
PublicCanRead the read access kind to any caller, including unauthenticated ones; no write access [PWS-AUTHZ]

D. Policy Type Registry

This appendix is normative.

This registry lists the type values an access control policy may carry (see 5.4 Access Control Policies) and the specification that defines each one. A server evaluates a policy by looking its type up here. A type not in this registry is unrecognized and grants nothing, per the fail-closed rule. Registering a type adds a row naming the specification that defines it; this specification defines no policy type itself.

type Grants Defined by
PublicCanRead the read access kind to any caller, including unauthenticated ones; no write access [PWS-AUTHZ]

E. Reserved Path Segment Registry

This appendix is normative.

E.1 Space-level reserved endpoints

The following path segments represent reserved API endpoints for 10. Spaces level operations. Usually, the path segment following the /space/{space_id}/ prefix is the id of a Collection. The list of reserved endpoints below means that collections ids MUST NOT collide with the corresponding reserved segments.

Reserved API Endpoint Reserved segment Purpose
/space/{space_id}/meta meta Space metadata; see 10.1 Space Metadata Data Model
/space/{space_id}/policy policy Access control policy
/space/{space_id}/backends backends Storage backends available
/space/{space_id}/collections collections Retired in v0.5. Listing and creating collections moved to the Space URL (see 10.5 List All Collections operation); a server MAY answer this path with a 308 Permanent Redirect to it. The segment stays reserved
/space/{space_id}/export export Export (download) space contents
/space/{space_id}/import import Import (upload) a space export archive
/space/{space_id}/linkset linkset Links to auxiliary resources
/space/{space_id}/query query Reserved for cross-collection queries
/space/{space_id}/quotas quotas Reserved for per-backend quota report

If a client attempts to create a collection with an id that collides with a reserved segment list above, the server MUST return a 409 Conflict error.

E.2 Collection-level reserved endpoints

The following path segments represent reserved API endpoints for 11. Collections level operations. Usually, the path segment following the /space/{space_id}/{collection_id}/ prefix is the id of a Resource. The list of reserved endpoints below means that resource ids MUST NOT collide with the corresponding reserved segments.

Reserved API Endpoint Reserved segment Purpose
/space/{space_id}/{collection_id}/policy policy Access control policy
/space/{space_id}/{collection_id}/backend backend Storage backend selected
/space/{space_id}/{collection_id}/linkset linkset Links to auxiliary resources
/space/{space_id}/{collection_id}/meta meta Collection metadata: its description together with the user's annotations; see 11.1 Collection Metadata Data Model
/space/{space_id}/{collection_id}/meta/log meta Governing history log, a sub-resource under the meta segment; see 11.7 Collection Governing History Log
/space/{space_id}/{collection_id}/query query Query resources within a collection (see G. Query Profile Registry)
/space/{space_id}/{collection_id}/quota quota Storage quota report for collection

If a client attempts to create a resource with an id that collides with a reserved segment list above, the server MUST return a 409 Conflict error.

E.3 Resource-level reserved endpoints

The following path segments represent reserved API endpoints for Resource level operations.

Reserved API Endpoint Reserved segment Purpose
/space/{space_id}/{collection_id}/{resource_id}/meta meta Resource metadata (server-managed and user-writable)
/space/{space_id}/{collection_id}/{resource_id}/chunks chunks Chunk addressing for a chunked Resource; owned by [PWS-EC], which defines the chunk endpoints under this segment. The segment stays reserved on every server whether or not it implements [PWS-EC].

Unlike the Space- and Collection-level reserved segments, which occupy the id position one level down and so constrain Collection and Resource id choice, these sit below the Resource level -- they qualify a {resource_id} -- and impose no constraint of their own on id choice at any level. meta appears in all three tables, holding the description of the thing it hangs under (see 2.2 Reading This Document), but it constrains id choice only where it occupies an id position: the Space-level row makes meta a reserved Collection id and the Collection-level row makes it a reserved Resource id, while the Resource-level /meta and /chunks segments sit one level lower and shadow nothing -- a Resource whose own id is chunks is unaffected.

E.4 Methods at Reserved Endpoints

This subsection governs requests to the reserved endpoints listed in the three tables above.

A request to a reserved endpoint with a method the server does not implement at that endpoint MUST be answered with 405 Method Not Allowed. The response carries an Allow header naming the methods the server does implement there, as [RFC9110] requires of a 405.

An endpoint can belong to an OPTIONAL group the server does not implement at all. Where that is so, the group's own rule governs the request instead, typically an unsupported-operation (501) error (see for example 11.1 Collection Metadata Data Model and 12.8 Resource Metadata Data Model).

HEAD counts as implemented wherever GET is. This subsection does not govern OPTIONS.

The 308 redirect this registry permits for the retired collections segment (see E.1 Space-level reserved endpoints) is the server's implementation of the redirected methods. Those methods are not refused.

The server MUST answer the 405 without regard to whether the Space, Collection, or Resource named in the path exists. So the answer reveals nothing the not-found rule protects (see 6. Error Handling). A server MUST NOT treat such a request as an operation on a Collection or Resource whose id is the reserved segment.

This refusal has no entry in the Error Type Registry. Its meaning is the HTTP status alone. A problem body, where a server sends one, uses [RFC9457]'s about:blank type.

F. Encryption Scheme Registry

This appendix is normative.

Note
This registry defines values used by an OPTIONAL feature (client-side encryption): a server MAY support no encryption schemes and remain conformant; see 2.4 Scope and Conformance Profiles. Optionality is orthogonal to normativity -- a server that does implement encrypted Collections MUST follow the wire formats catalogued here.

A Collection's optional encryption descriptor (see 11.1 Collection Metadata Data Model) names a client-side encryption scheme and a version of that scheme's wire format -- a positive integer, starting at 1 for each scheme and incremented per registered revision (an absent version means 1). This registry maps each scheme/version pair to the wire format the server can expect for Resources in such a Collection, so that a server can hold the collection's fail-closed guarantee structurally, by validating the shape of what is written, rather than relying on every client to encrypt correctly. The server never holds key material and never decrypts; it validates only the non-secret envelope structure, so this enforcement neither requires nor weakens confidentiality.

scheme version Media type Envelope profile Reference
edv 1 application/json An Encrypted Data Vault Encrypted Document: a JSON object whose jwe member is a JWE in JSON Serialization ([RFC7516] §7.2) -- a JSON object carrying at least a ciphertext member and either a recipients array (general serialization) or a top-level encrypted_key/protected (flattened serialization). The document MAY also carry EDV bookkeeping members (id, sequence, indexed); these are opaque to the server. Encrypted Data Vaults

[PWS-EC] owns the envelope, the blinded index, recipient management, and the chunked envelope form built on this scheme. Core keeps only the server-visible structural validation below: the shape a server checks on write, independent of what a client does with the plaintext.

F.1 Server-side write validation

A server that recognizes the scheme/version pair declared by a Collection's encryption descriptor MUST validate the body of every Resource content write (POST or PUT) into that Collection against that pair's envelope profile, and MUST reject a non-conforming body -- or a body sent under a Content-Type other than the scheme's registered media type -- with an encryption-scheme-mismatch error.

This is the structural fail-closed guarantee: a client that has not encrypted the body, whether through a bug or an omission, cannot store server-visible plaintext in an encrypted Collection. The check is purely structural; a server MUST NOT attempt decryption and MUST NOT inspect the envelope's contents.

The rule applies to a Resource's stored representation and, on an encrypted Collection, to the user-writable custom object of its 12.8 Resource Metadata Data Model (see the encrypted-Collection note there). It does not apply to the rest of the server-managed API documents -- Collection Descriptions, the server-managed top-level Metadata properties, policy documents, linksets -- which remain application/json regardless of the Collection's encryption status. For the Metadata object specifically, the document itself stays a plaintext application/json object (so the server keeps its top-level contentType, size, and timestamps); only the custom sub-value MUST be a conforming envelope on an encrypted Collection, validated the same fail-closed way (an encryption-scheme-mismatch on non-conformance). An empty or omitted custom is a clearing write and is not validated.

As with id-conflict, a server MUST verify the caller's authorization before validating the envelope, so that encryption-scheme-mismatch is observable only to callers already authorized to write at that target; an under-authorized caller receives the merged not-found instead, and learns nothing about the Collection.

F.2 Accepting a descriptor only when it can be enforced

When a Collection create or update operation declares an encryption descriptor (see 11.4 Update (or Create by Id) Collection operation), a server SHOULD reject a scheme -- or a version of a recognized scheme -- that it does not recognize with an unsupported-encryption-scheme error, rather than storing a descriptor it cannot enforce. This ensures that every descriptor a server accepts is one it validates on write: "this Collection is declared encrypted" then structurally implies "plaintext writes to it are rejected here," closing the gap that a silently unenforced descriptor would reopen.

A server MAY instead choose to store descriptors for schemes it does not enforce (treating the descriptor as fully opaque, per 11.1 Collection Metadata Data Model), but such a server MUST document that it provides no server-side fail-closed guarantee for those Collections, leaving the guarantee entirely to clients.

F.3 Validation profile

A server MAY implement the edv 1 envelope profile with a JSON Schema equivalent to the following. The outer object MUST carry a jwe member; only the jwe's structural members are checked; their values are opaque ciphertext and are not interpreted. A plaintext object under application/json (one with no valid jwe) fails this profile and is rejected with an encryption-scheme-mismatch, preserving the fail-closed guarantee.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["jwe"],
  "properties": {
    "jwe": {
      "type": "object",
      "required": ["ciphertext"],
      "properties": {
        "protected": { "type": "string" },
        "iv": { "type": "string" },
        "ciphertext": { "type": "string" },
        "tag": { "type": "string" },
        "encrypted_key": { "type": "string" },
        "recipients": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "header": { "type": "object" },
              "encrypted_key": { "type": "string" }
            }
          }
        }
      },
      "anyOf": [
        { "required": ["recipients"] },
        { "required": ["encrypted_key"] },
        { "required": ["protected"] }
      ]
    }
  }
}

F.4 Key Epochs

A key epoch is one generation of a Collection's encryption key. Rather than encrypting every Resource under a single long-lived key, writers encrypt each Resource under whichever epoch was current when it was written; the Collection's encryption descriptor carries the full, append-only roster of epochs, so the roster as a whole reads as the Collection's access history. The full client-side construction is specified in [PWS-EC]; this section defines only what the server stores and validates.

An encrypted Collection with a single shared key set has no cryptographic notion of removing a reader: revoking the reader's capability stops the server serving it ciphertext, but the reader still holds the key. Key epochs make removal meaningful by separating two axes:

Removing a reader means doing both: revoke its capability AND rotate the epoch. Neither alone suffices.

F.4.1 Epoch data model

A Collection's encryption descriptor MAY carry the two key-epoch members below. It MAY also carry the hmac member of F.4.2 The blinding-key member, where its clients use the blinded-index query profile.

  • epochs - An array of epoch objects, each with:
    • id - A unique, opaque epoch identifier (a non-empty string). The reference client uses the did:key identifier of the epoch's public key-agreement key, so that a stored envelope's JWE recipient kid names its epoch directly, but servers MUST treat the value as opaque.
    • recipients - A non-empty array of wrapped-key entries, one per reader. Each entry reuses the JWE General Serialization recipients entry shape verbatim ([RFC7516] section 7.2): a header object with at least a non-empty string kid (the reader's key-agreement key identifier) and alg (e.g. ECDH-ES+A256KW), plus the wrapped epoch key as a string encrypted_key. This is the same shape the edv envelope profile already validates -- one wire vocabulary for "a key wrapped to a recipient".
  • currentEpoch - The id of the epoch new writes encrypt under. MUST name an entry in epochs.

Nothing secret appears in these fields: recipient entries hold public key identifiers and wrapped-key ciphertext only. The server never holds key material, never unwraps, and never verifies that a wrapped key is correct -- that is unverifiable without the keys, by construction.

Example descriptor:

{
  "scheme": "edv",
  "currentEpoch": "did:key:z6LSepoch2...#z6LSepoch2...",
  "epochs": [
    {
      "id": "did:key:z6LSepoch1...#z6LSepoch1...",
      "recipients": [
        {
          "header": {
            "kid": "did:key:z6MkappA...#z6LSkakA...",
            "alg": "ECDH-ES+A256KW",
            "epk": { "kty": "OKP", "crv": "X25519", "x": "..." },
            "apu": "...",
            "apv": "..."
          },
          "encrypted_key": "base64url..."
        }
      ]
    },
    {
      "id": "did:key:z6LSepoch2...#z6LSepoch2...",
      "recipients": [ "..." ]
    }
  ]
}

F.4.2 The blinding-key member

A Collection's encryption descriptor MAY also carry an hmac member: the public reference to the Collection's blinding key, the HMAC key under which a client blinds the attribute names and values it stores in an envelope's indexed member and sends in a blinded-index query (see G.2 blinded-index profile). The member is a JSON object:

  • id - A non-empty string identifying the blinding key, opaque to the server. It is the value an envelope's indexed[].hmac.id carries and the value a blinded-index query names as its index.
  • type - A non-empty string naming the key type (the [PWS-EC] profile uses Sha256HmacKey2019). Servers MUST treat the value as opaque.
  • recipients - A non-empty array of wrapped-key entries, one per reader, in exactly the recipients entry shape of an epoch (see F.4.1 Epoch data model). Each entry carries the blinding secret wrapped to one reader's key-agreement key, so one wrap construction serves epoch keys and the blinding key alike.

As with the epoch members, nothing secret appears here: the blinding secret travels only as per-reader wrapped ciphertext, and the server never unwraps it. How the key is minted, which readers hold it, and how its recipients are maintained alongside the epochs' are client-side matters defined by [PWS-EC]. One property of the member does matter to the server: the blinding key is permanent. Every blinded token in the Collection is computed under it, so replacing or dropping it would orphan every blinded index at once. The server therefore treats a stored hmac member as immutable in its id and type and never removable (see the server validation below). As with the epoch members, this validation is part of recognizing the edv scheme: a server that does not support encrypted Collections, or that stores descriptors it does not enforce (see F. Encryption Scheme Registry), treats hmac as an opaque member. The [PWS-EC] profile goes further and installs the key when the Collection is provisioned, since envelopes written before it carry no tokens. This specification leaves that timing rule to clients and does not require the server to enforce it.

Example descriptor carrying a blinding key (epoch members elided):

{
  "scheme": "edv",
  "hmac": {
    "id": "urn:uuid:6d4a2c3e-9b1f-4c8e-a0d2-7f5e1b3c9a44",
    "type": "Sha256HmacKey2019",
    "recipients": [
      {
        "header": {
          "kid": "did:key:z6MkappA...#z6LSkakA...",
          "alg": "ECDH-ES+A256KW",
          "epk": { "kty": "OKP", "crv": "X25519", "x": "..." },
          "apu": "...",
          "apv": "..."
        },
        "encrypted_key": "base64url..."
      }
    ]
  },
  "currentEpoch": "did:key:z6LSepoch1...#z6LSepoch1...",
  "epochs": [ "..." ]
}

F.4.3 Server validation

A server that recognizes the edv scheme MUST validate the key-epoch members when a Collection create or update supplies them, rejecting a violation with an invalid-request-body error (and a JSON pointer to the offending member):

  • epochs and currentEpoch MUST appear together or not at all.
  • epochs MUST be a non-empty array; each entry MUST have a non-empty string id, unique across the array, and a non-empty recipients array.
  • Each recipients entry MUST have a header object with non-empty string kid and alg members, plus a string encrypted_key.
  • currentEpoch MUST name an id present in epochs.

The same server MUST validate the hmac member (see F.4.2 The blinding-key member) when a create or update supplies it, on the same terms:

  • hmac MUST be an object with non-empty string id and type members and a non-empty recipients array; each recipients entry MUST have the shape required of an epoch's entries above (a header object with non-empty string kid and alg members, plus a string encrypted_key).

A server that does not recognize the scheme applies none of these checks; see F. Encryption Scheme Registry for what such a server does with the descriptor.

On an update of a descriptor that already carries epochs, two server-side invariants also apply. These protect the Collection's own readers from client bugs -- a dropped epoch strands every Resource stamped with it -- and are enforceable without the server reading a single byte of what the epochs protect:

  • epochs is append-only. Every previously stored epoch id MUST still be present. (Entries within an existing epoch's recipients MAY change: adding a reader wraps existing epoch keys to it.)
  • currentEpoch never moves backwards. The updated currentEpoch MUST either equal the stored one or name an epoch id that was not previously present (a newly appended epoch). This formulation is independent of array order.

A third invariant covers the hmac member, on an update of a descriptor that already carries one:

  • hmac is permanent. The member MUST remain present, with its id and type unchanged. Its recipients entries MAY change (a reader being added or removed). Changing either value, or removing the member, would orphan every blinded index in the Collection, just as a scheme change would corrupt its Resources. A violation is therefore rejected with the same encryption-immutable error (see 11.1 Collection Metadata Data Model). Introducing hmac on a stored descriptor that lacks it is not a server-side violation; whether a client may do so is a matter for the client-side profile (see F.4.2 The blinding-key member).

On a Collection whose descriptor is governed by its history log (see 11.7 Collection Governing History Log), the descriptor is not supplied on an update; it is the state of the log's head entry. The shape validation above then runs on the head state of every log write, and the invariants run on each append against the prior head's state, with the same errors.

Beyond this shape validation and these invariants the server MUST NOT interpret the members. In particular it MUST NOT attempt to check that the recipients of an epoch or of hmac match any set of capability grantees -- the two axes are deliberately independent, and keeping them in sync is the client's job.

F.4.4 Epoch stamping on Resources

A reader must know which epoch key to unwrap before attempting decryption. The writer therefore declares the epoch a Resource was encrypted under, and the server stores and serves that declaration:

  • A Resource content write (POST or PUT) MAY carry a Key-Epoch request header whose value is the epoch id (a non-empty string; a present but empty or malformed value is an invalid-request-body error). The server MUST store the value verbatim as the Resource Metadata epoch property. A content write without the header MUST clear any stored stamp: the new representation's epoch is unknown, and a stale stamp is worse than none. The value is client-declared, advisory metadata: the server MUST NOT validate it against the descriptor's epochs (a write may race a rotation) and MUST NOT compute it.
  • An Update Resource Metadata request MAY carry a top-level epoch member (a sibling of custom, and like the header a non-empty string). Unlike custom, which is a full replacement, an omitted epoch PRESERVES the stored value -- the stamp describes the content write, not the metadata write. It MUST NOT live inside custom: on an encrypted Collection custom is the opaque envelope and is replaced wholesale by every metadata write.
  • An 11.4 Update (or Create by Id) Collection operation request MAY likewise carry a top-level epoch member, stamping the epoch the Collection Metadata object's own custom envelope was encrypted under. Its omission rule is inverted: an omitted epoch CLEARS the stored value, because at that level the stamp describes the custom envelope itself, which every metadata write replaces wholesale. See 11.1 Collection Metadata Data Model.
  • The server MUST return the stored stamp as the epoch property of the Resource Metadata object, on each item of a List Collection response, and on each document of the changes query profile -- so a reader walking a listing or replicating from the feed can select its epoch key without a per-Resource metadata fetch.

F.4.5 Client procedures

The client-side epoch construction -- how an epoch key is generated, how recipients entries wrap it, how reads and writes select an epoch, and how a reader is added or removed -- is normatively specified in [PWS-EC]. This section defines only what the server stores and validates.

F.4.6 Security considerations

The security properties and limitations of key-epoch rotation (what revocation does and does not protect, and how a rekey interacts with the changes feed) are discussed in [PWS-EC].

G. Query Profile Registry

This appendix is normative.

Note
This registry defines values used by an OPTIONAL feature (the query endpoint): a server MAY expose no query endpoint and remain conformant; see 2.4 Scope and Conformance Profiles. Optionality is orthogonal to normativity: a server that does serve queries MUST follow the request and response shapes catalogued here.

A collection MAY expose an OPTIONAL query endpoint at POST /space/{space_id}/{collection_id}/query. The endpoint is a single URL that serves several distinct query dialects; a request selects one by carrying a profile discriminator in its body. This registry maps each profile token to the request and response wire shape of that dialect.

The request body is an application/json object with a REQUIRED profile string member. All other members are profile-specific. A server that recognizes the profile answers with 200 OK on success and a profile-specific response body. A server that does not implement a given profile MUST respond with an unsupported-operation (501) error.

The profile vocabulary is open and additive: this specification defines the tokens below, and future profiles register additional tokens. A server MUST treat a profile it does not recognize the same as one it does not serve, with unsupported-operation.

Authorization. The query endpoint requires read authorization (a capability or a policy grant, as for any read); the invoked action is POST. The capability's invocationTarget is the bare /query URL. Every query parameter travels in the request body, which is signed and covered by the authorization profile's body-integrity mechanism (the request Digest of [PWS-AUTHZ]), so no query-string attenuation is involved, and a capability that authorizes the /query target authorizes any query body it signs. As with every other operation, a server MUST verify authorization before validating the request body. An under-authorized caller therefore receives the privacy-preserving not-found (404, see 6. Error Handling) and never learns whether its query body was well-formed, nor whether the target Collection exists.

profile Summary Defining subsection
changes Ordered replication change feed (a resumable pull loop) G.1 changes profile
blinded-index Matching over client-computed blinded (HMAC'd) attributes G.2 blinded-index profile

G.1 changes profile

The changes profile serves an ordered, resumable feed of the content and metadata changes in a collection, so that an offline-first replication client can pull a Collection's state incrementally and keep a local replica in sync. A server that serves this profile advertises changes-query in its service description features array (see 8.2 Service Description Data Model), rather than per Collection or per backend.

Request body:

{
  "profile": "changes",
  "checkpoint": "<checkpoint string from a prior response>",
  "limit": 100
}

Response body:

{
  "documents": [
    {
      "id": "hello-world",
      "_deleted": false,
      "updatedAt": "2026-01-15T12:00:00.000Z",
      "version": 1,
      "etag": "3mJr7AoUXx2.1",
      "metaVersion": 3,
      "metaEtag": "3mJr7AoUXx2.3",
      "createdBy": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
      "writerId": "z6fVXHKn8PdQm2Rt",
      "data": { "message": "hi" },
      "custom": {
        "name": "Hello World greeting",
        "tags": { "project": "demo", "status": "draft" }
      },
      "checkpoint": "eyJmZWVkIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9zcGFjZS84MTI0NjEzMS02OWE0LTQ1YWItOWJmZi05Yzk0NmI1OWNmMmUvbWVzc2FnZXMvIiwicG9zaXRpb24iOjF9"
    }
  ],
  "checkpoint": "eyJmZWVkIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9zcGFjZS84MTI0NjEzMS02OWE0LTQ1YWItOWJmZi05Yzk0NmI1OWNmMmUvbWVzc2FnZXMvIiwicG9zaXRpb24iOjF9"
}

Each entry in documents describes one changed Resource:

Tombstones. A soft-deleted Resource surfaces as { "id", "_deleted": true, "updatedAt", "version", "etag", "checkpoint" } with no data member; the deletion bumps version and etag is the tombstone's validator. A tombstone retains its createdBy, where one was recorded, so that a deletion replicates together with the attribution of the Resource it removes. It also carries as writerId the label the deleting request declared (see 12.8.1 Writer attribution: writerId and createdBy), if any. A deletion's attribution and tie-breaking work like a content write's.

Ordering and resumption. Entries are ordered by the issuing server's feed position: a per-Collection sequence that the server assigns to a write at the moment the write becomes visible. A server MUST assign it so that no write can land at or before a position it has already returned to a client. updatedAt carries no ordering guarantee, and two entries may share it.

The checkpoint is an opaque string, scoped to the server URL and the Collection that issued it. A client stores it, compares it by equality only, and echoes it back verbatim; it MUST NOT parse or construct one. The top-level checkpoint equals the checkpoint of the last entry in documents, or is null when nothing changed past the supplied checkpoint (the end of the feed). A client resumes by sending the returned checkpoint as the checkpoint of its next request; the resulting sequence of calls is the pull loop of a replication protocol. A server MUST reject a checkpoint it did not issue for the target Collection with invalid-request-body (400). A client whose checkpoint is rejected restarts its pull from the beginning, with no checkpoint. This is safe because a replica applies each entry keyed by Resource id. A metadata-only edit re-surfaces the Resource with a bumped metaVersion but an unchanged updatedAt, version and data.

Note
This note is non-normative. The encoding of a checkpoint is the server's own choice, and a client gains nothing by reading inside one. For example, the reference implementation encodes a checkpoint as the base64url encoding, without padding, of the JSON object `{"feed": "", "position": }`. Another server, or a later version of the same server, may encode a checkpoint differently, for instance to carry one position per source.

Scope. Binary (non-JSON) Resources are excluded from the changes feed; blob replication is out of scope for this profile. This also means a write or delete of a chunk (see [PWS-EC] for what a chunk is) is invisible to the feed: it affects only the chunk's own ETag validator and MUST NOT advance the parent Resource's position in the feed.

Example -- request the first batch and receive one changed document plus the checkpoint to resume from:

POST /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/query HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...

{ "profile": "changes", "limit": 100 }
HTTP/1.1 200 OK
Content-type: application/json

{
  "documents": [
    {
      "id": "hello-world",
      "_deleted": false,
      "updatedAt": "2026-01-15T12:00:00.000Z",
      "version": 1,
      "createdBy": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
      "data": { "message": "hi" },
      "checkpoint": "eyJmZWVkIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9zcGFjZS84MTI0NjEzMS02OWE0LTQ1YWItOWJmZi05Yzk0NmI1OWNmMmUvbWVzc2FnZXMvIiwicG9zaXRpb24iOjF9"
    }
  ],
  "checkpoint": "eyJmZWVkIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9zcGFjZS84MTI0NjEzMS02OWE0LTQ1YWItOWJmZi05Yzk0NmI1OWNmMmUvbWVzc2FnZXMvIiwicG9zaXRpb24iOjF9"
}

The client then resumes by echoing that checkpoint in its next request:

POST /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/query HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...

{
  "profile": "changes",
  "checkpoint": "eyJmZWVkIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9zcGFjZS84MTI0NjEzMS02OWE0LTQ1YWItOWJmZi05Yzk0NmI1OWNmMmUvbWVzc2FnZXMvIiwicG9zaXRpb24iOjF9",
  "limit": 100
}

G.2 blinded-index profile

The blinded-index profile serves queries over client-computed blinded (HMAC'd) attributes, so that a server can match encrypted documents without seeing plaintext attribute names or values. Its semantics follow the query operation of the Encrypted Data Vaults specification (the same specification the F. Encryption Scheme Registry references for the edv envelope). This profile is a PWS-EC affordance: a server that serves it advertises blinded-index-query in its PWS-EC version entry's features array (see 8.2 Service Description Data Model).

Documents opt in by carrying the EDV indexed member: an array of entries of the shape { "hmac": { "id", "type" }, "sequence", "attributes": [ { "name", "value", "unique"? } ] }, in which each name and value is a blinded (HMAC'd) token computed by the client. A server stores the indexed member and matches against it, but cannot invert it back to the plaintext attribute.

Request body:

{
  "profile": "blinded-index",
  "index": "<HMAC key id>",
  "equals": [ { "<blindedName>": "<blindedValue>" } ],
  "has": [ "<blindedName>" ],
  "count": false,
  "limit": 10,
  "cursor": "<opaque token>"
}

Response body (a document page):

{ "documents": [ { } ], "hasMore": false, "cursor": "<opaque token>" }
Note

Cursor pagination is a deliberate PWS extension over the Encrypted Data Vaults query operation, which offers only limit together with a hasMore flag and no means to fetch the next page. A server that also implements the EDV query operation directly may expose limit/hasMore there while offering the cursor continuation only through this profile.

Example -- match documents whose blinded index-2 attribute equals a blinded value:

POST /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/query HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...

{
  "profile": "blinded-index",
  "index": "z1A...hmacKeyId",
  "equals": [ { "aBlindedName": "aBlindedValue" } ],
  "limit": 10
}
HTTP/1.1 200 OK
Content-type: application/json

{
  "documents": [
    { "id": "urn:uuid:...", "sequence": 0, "jwe": { "ciphertext": "..." } }
  ],
  "hasMore": false
}

G.2.1 Unique blinded attributes

An indexed attribute entry MAY carry "unique": true, marking its (hmac id, name, value) triple as a uniqueness claim within the Collection. A server MUST reject a Resource content write that would claim a triple already claimed with unique: true by a different Resource in the same collection, with an id-conflict (409) error. A document re-asserting its own existing claim never self-conflicts, and the same triple carried without unique coexists freely.

This uniqueness is enforced at write time, not at query time. As with the id-conflict rule elsewhere in this specification (see the id-conflict privacy note in H. Error Type Registry), a server MUST verify the caller's authorization to write at the target before performing this check, so that the existence-revealing 409 is observable only to a caller already authorized to write; an under-authorized caller receives the merged not-found instead. The check MUST be atomic with the write, so that two concurrent writers cannot both claim the same triple.

H. Error Type Registry

This appendix is normative.

This specification uses [RFC9457] Problem Details for HTTP APIs for error responses (see 6. Error Handling). The type property of a problem response is a URI identifying the kind of problem. Following [RFC9457], a type is reused across operations: a single kind such as invalid-id is emitted by Create, Update, and Read operations across Spaces, Collections, and Resources alike. The per-occurrence specifics belong in the errors array (detail and an optional pointer), and the short human-readable summary in title.

Each type URI is a fragment anchor into this registry. The status codes listed below are typical; a single kind MAY be returned with more than one status code depending on the operation.

An authorization profile registers no kinds of its own. It reports its failures with the kinds here: missing-authorization, invalid-authorization-header, controller-mismatch, and, under the maximum-privacy rule of 6. Error Handling, not-found. The baseline profile [PWS-AUTHZ] lists which it emits when.

type URI Anchor Typical status Description
https://w3id.org/pws#not-found not-found 404 The resource (Space, Collection, or Resource) does not exist, or the caller is not authorized to access it. These two conditions are deliberately indistinguishable -- see the privacy note below.
https://w3id.org/pws#invalid-id invalid-id 400 A Space, Collection, or Resource id is missing or not URL-safe.
https://w3id.org/pws#reserved-id reserved-id 409 A client-supplied id collides with a E. Reserved Path Segment Registry segment.
https://w3id.org/pws#id-conflict id-conflict 409 A client-supplied id in a POST create operation already exists. Also returned when a write would violate a unique: true blinded-attribute claim (see G.2 blinded-index profile) or a unique: true plaintext index claim, and when a Collection update adds a unique: true plaintext index over Resources that already violate it (see 11.1 Collection Metadata Data Model). (Create-or-replace by id is done idempotently via PUT, which does not conflict.)
https://w3id.org/pws#invalid-request-body invalid-request-body 400 The request body is missing or invalid (e.g. a required property is absent). Entries in errors SHOULD carry a pointer to the offending field.
https://w3id.org/pws#invalid-cursor invalid-cursor 400 A pagination cursor query parameter is malformed or can no longer be honored (e.g. an expired snapshot). See A. Pagination.
https://w3id.org/pws#missing-content-type missing-content-type 400 A required Content-Type header is missing.
https://w3id.org/pws#missing-authorization missing-authorization 401 Required Authorization / Capability-Invocation headers (or proof of possession) are missing.
https://w3id.org/pws#invalid-authorization-header invalid-authorization-header 400 An Authorization, Capability-Invocation, or Digest header is malformed, unparseable, or failed verification.
https://w3id.org/pws#controller-mismatch controller-mismatch 400 The capability invocation in a Create Space request is not currently authorized by the controller supplied in the request body: it is neither signed by that DID nor accompanied by a valid, unexpired delegation chain rooted in it. Servers SHOULD differentiate the cause (chain rooted elsewhere, expired delegation, failed proof) in the detail string where they can; see 9.1.2 Create Space Errors.
https://w3id.org/pws#unsupported-backend unsupported-backend 409 A requested backend id is not in the space's 14.2 Space Backends Available list.
https://w3id.org/pws#encryption-immutable encryption-immutable 409 A Collection update tried to change the scheme, decrease or remove the version, or clear an existing encryption descriptor; or it tried to change the id or type of the descriptor's hmac member, or remove that member. The descriptor is set-once, version-monotonic: declaring it on a Collection that lacks one is allowed (and re-declaring the standing values is a no-op), but changing its scheme, moving its version backward, or clearing it on a populated Collection would corrupt the stored, client-encrypted Resources, and replacing or dropping the blinding key would orphan every blinded index. See 11.1 Collection Metadata Data Model and F.4.2 The blinding-key member.
https://w3id.org/pws#encryption-history-log-governed encryption-history-log-governed 409 A Collection update carried an encryption member on a Collection whose descriptor is governed by its history log. The member is read-only on that path: it is derived from the log's head entry, and changes by an append to the log at the Collection's meta/log sub-resource. See 11.7 Collection Governing History Log.
https://w3id.org/pws#encryption-scheme-mismatch encryption-scheme-mismatch 422 A write into an encrypted Collection -- a Resource's content, or the custom object of a Resource's or the Collection's own Metadata -- had a body (or Content-Type) that does not conform to the Collection's declared encryption scheme envelope profile. Reachable only by a caller already authorized to write -- see F. Encryption Scheme Registry.
https://w3id.org/pws#unsupported-encryption-scheme unsupported-encryption-scheme 400 A Collection create/update declared an encryption scheme (or a version of one) the server does not recognize or support. See F. Encryption Scheme Registry.
https://w3id.org/pws#precondition-failed precondition-failed 412 A conditional write's If-Match / If-None-Match precondition evaluated false: the Resource's current version did not match, or a create-if-absent target already exists. Header-driven and distinct from the 409 conflict kinds. See 7.2 Conditional Requests.
https://w3id.org/pws#quota-exceeded quota-exceeded 507 A write was rejected because the target backend's storage quota is exhausted. See 15. Quotas.
https://w3id.org/pws#payload-too-large payload-too-large 413 An upload exceeds the target backend's maxUploadBytes constraint (see 15. Quotas). Note that unlike quota-exceeded, this rejection is per-request: smaller uploads may still succeed.
https://w3id.org/pws#unsupported-operation unsupported-operation 501 An optional operation that this server or the target backend does not support (for example, a per-collection quota report on a backend without per-collection accounting).
https://w3id.org/pws#invalid-import invalid-import 400 An uploaded archive is not a valid PWS space export. The import operation itself is reserved and not yet specified (see E. Reserved Path Segment Registry); this kind is registered ahead of it so implementations converge on one error shape.
https://w3id.org/pws#storage-error storage-error 500 An underlying storage operation failed.
https://w3id.org/pws#internal-error internal-error 500 An unexpected server-side fault with no more specific kind.

Privacy: the not-found kind is intentionally merged. Under the principle of maximum privacy (see 6. Error Handling), an unauthorized client MUST NOT be able to discover the existence of a Space, Collection, or Resource from an error response. The not-found kind therefore covers both "resource absent" and "invalid authorization"; implementations MUST NOT split it into distinguishable type values, and MUST NOT otherwise let the response (status code, title, or detail) reveal whether the resource exists.

This merging applies to "insufficient-authorization" and "absent-authorization" against an existing target. It does not apply to request- or credential-validation failures, which describe the request rather than the target and so MAY use their own precise types -- see 6. Error Handling.

Privacy: id-conflict is existence-revealing by nature -- a 409 confirms that the supplied id is taken. For Create Collection and Create Resource, servers MUST therefore verify the caller's authorization before checking for a conflict, so that only callers already authorized to create at that level can observe the signal; an under-authorized caller receives the merged not-found instead. For Create Space, where any caller permitted to attempt creation learns the same fact from whether creation succeeds, the disclosure is inherent -- see the note under 9.1.2 Create Space Errors.

H.1 Error Response Examples

Canonical example responses for the error kinds defined above, referenced by the per-operation "Errors" lists throughout this specification. The title strings are illustrative, not normative: for example, the noun in a not-found title varies with the target (Space, Collection, or Resource), and a title may name the operation that failed. Kinds not shown here (missing-content-type, invalid-authorization-header, invalid-import, storage-error, internal-error) follow the same shape; they will gain examples as their corresponding sections are drafted.

not-found -- a missing target, or missing or insufficient authorization (deliberately indistinguishable, see the privacy note above):

HTTP/1.1 404 Not Found
Content-type: application/problem+json

{
  "type": "https://w3id.org/pws#not-found",
  "title": "Resource not found or insufficient authorization."
}

invalid-id -- an id that is not URL-safe (see 4. Identifiers):

HTTP/1.1 400 Bad Request
Content-type: application/problem+json

{
  "type": "https://w3id.org/pws#invalid-id",
  "title": "Invalid Space id.",
  "errors": [
    {
      "detail": "Space 'id' must be URL-safe.",
      "pointer": "#/id"
    }
  ]
}

reserved-id -- a client-supplied id that collides with a segment from the E. Reserved Path Segment Registry:

HTTP/1.1 409 Conflict
Content-type: application/problem+json

{
  "type": "https://w3id.org/pws#reserved-id",
  "title": "Invalid collection id (from reserved list)."
}

id-conflict -- a POST create operation supplying an id that already exists:

HTTP/1.1 409 Conflict
Content-type: application/problem+json

{
  "type": "https://w3id.org/pws#id-conflict",
  "title": "A Collection with this id already exists.",
  "errors": [
    {
      "detail": "Use PUT to create-or-replace a Collection at a chosen id.",
      "pointer": "#/id"
    }
  ]
}

precondition-failed -- a conditional PUT whose If-Match precondition did not match the Resource's current version (a concurrent write landed first):

HTTP/1.1 412 Precondition Failed
Content-type: application/problem+json

{
  "type": "https://w3id.org/pws#precondition-failed",
  "title": "The resource was modified by another write.",
  "errors": [
    {
      "detail": "Re-read the current resource, re-apply your change, and retry."
    }
  ]
}

invalid-request-body -- a missing or invalid request body (here, a Create Space request without the required controller property):

HTTP/1.1 400 Bad Request
Content-type: application/problem+json

{
  "type": "https://w3id.org/pws#invalid-request-body",
  "title": "Invalid Create Space body.",
  "errors": [
    {
      "detail": "'controller' property is required.",
      "pointer": "#/controller"
    }
  ]
}

missing-authorization -- required authorization (here, a proof of possession signature on a Create Space request) is missing:

HTTP/1.1 401 Unauthorized
Content-type: application/problem+json

{
  "type": "https://w3id.org/pws#missing-authorization",
  "title": "Invalid Create Space request.",
  "errors": [
    {
      "detail": "Valid proof of possession of the 'controller' DID must be provided."
    }
  ]
}

controller-mismatch -- the invocation is not currently authorized by the DID specified in the controller property of a Create Space request body (the detail here is the generic catch-all; servers that can tell SHOULD name the specific cause instead, e.g. "The delegation chain is rooted in a DID other than the body's 'controller'." or "The delegated capability has expired."):

HTTP/1.1 400 Bad Request
Content-type: application/problem+json

{
  "type": "https://w3id.org/pws#controller-mismatch",
  "title": "Invalid Create Space request.",
  "errors": [
    {
      "detail": "The invocation must be authorized by the 'controller' DID in the request body: signed by it, or via a delegation chain rooted in it.",
      "pointer": "#/controller"
    }
  ]
}

unsupported-backend -- a backend id that is not part of that space's 14.2 Space Backends Available list:

HTTP/1.1 409 Conflict
Content-type: application/problem+json

{
  "type": "https://w3id.org/pws#unsupported-backend",
  "title": "Unsupported backend id, check the space's 'backends available' list."
}

encryption-immutable -- a Collection update tried to change the scheme, decrease or remove the version, or clear an existing encryption descriptor (it is set-once, version-monotonic; see 11.1 Collection Metadata Data Model):

HTTP/1.1 409 Conflict
Content-type: application/problem+json

{
  "type": "https://w3id.org/pws#encryption-immutable",
  "title": "Collection encryption descriptor is immutable."
}

encryption-history-log-governed -- a Collection update carried an encryption member on a Collection whose descriptor is governed by its history log; append to the log instead (see 11.7 Collection Governing History Log):

HTTP/1.1 409 Conflict
Content-type: application/problem+json

{
  "type": "https://w3id.org/pws#encryption-history-log-governed",
  "title": "Collection encryption descriptor is governed by its history log."
}

encryption-scheme-mismatch -- a write into an encrypted Collection whose body (a Resource's content, or the custom object of a metadata write) is not an envelope of the Collection's declared scheme (observable only by a caller already authorized to write; see F. Encryption Scheme Registry):

HTTP/1.1 422 Unprocessable Content
Content-type: application/problem+json

{
  "type": "https://w3id.org/pws#encryption-scheme-mismatch",
  "title": "Body does not conform to the Collection's encryption scheme."
}

quota-exceeded -- a write rejected because the target backend's storage quota is exhausted (see 15. Quotas):

HTTP/1.1 507 Insufficient Storage
Content-type: application/problem+json

{
  "type": "https://w3id.org/pws#quota-exceeded",
  "title": "Storage quota exceeded for backend 'default'."
}

payload-too-large -- an upload exceeding the target backend's maxUploadBytes constraint:

HTTP/1.1 413 Content Too Large
Content-type: application/problem+json

{
  "type": "https://w3id.org/pws#payload-too-large",
  "title": "Upload exceeds the backend's maximum upload size.",
  "errors": [
    {
      "detail": "Upload size 209715200 exceeds 'maxUploadBytes' of 157286400 for backend 'dropbox'."
    }
  ]
}

unsupported-operation -- an optional operation this server or backend does not support (here, a per-collection quota report):

HTTP/1.1 501 Not Implemented
Content-type: application/problem+json

{
  "type": "https://w3id.org/pws#unsupported-operation",
  "title": "Backend 'default' does not support per-collection quota reports."
}

I. Goals and Requirements

This section is non-normative.

This storage specification is intended to support the following goals and requirements.

I.1 Local-first and Offline capable storage

Users and apps need to be able to use (provision, set up, and start reading and writing to) storage spaces without being connected to the internet.

I.2 Storage and sharing of public, permissioned, and private encrypted data

Although the local-first offline functionality is necessary, writing data to stable internet-accessible URLs for the purposes of sharing them is one of the primary use cases of this specification.

I.3 Stored data is opaque to the storage provider

I.4 Replication to user-controlled local and cloud servers

I.5 Serve as a General Purpose application storage backend

Intended to serve as a storage backend for credential wallets, and any other client-side (Single Page Applications), server side, desktop, and mobile apps and services.

I.6 Data Portability

Data written to storage spaces using this specification needs to be portable:

I.7 A Plurality of Data Formats and Protocols

I.8 Permissioned Query and Search functionality

I.9 Upgradeable and legislation-compliant cryptography

All cryptography has a half-life.

I.10 Anti-Goals

I.10.1 Use cases do not include "zero trust" environments

In a "zero trust" storage environment, the sync and replication nodes are assumed to be untrusted: they hold only ciphertext, enforce no authorization of their own, and encryption alone serves as the access control mechanism.

This storage specification is intentionally positioned to not be used in such environments. All encryption has an unpredictable half-life, and some use cases do not permit relying on encryption only for access control.

What this specification pursues instead is a combination of the two: encryption protects data at rest and in transit, while minimally trusted servers independently enforce the applicable policy on every request. A broken cipher alone therefore does not grant an attacker access.

J. IANA Considerations

This section is non-normative.

This section will be submitted to the Internet Engineering Steering Group (IESG) for review, approval, and registration with IANA.

K. References

K.1 Normative references

[DID-CORE]
Decentralized Identifiers (DIDs) v1.0. Manu Sporny; Amy Guy; Markus Sabadello; Drummond Reed. W3C. 19 July 2022. W3C Recommendation. URL: https://www.w3.org/TR/did-core/
[JSON-LINES]
JSON Lines. Ian Ward. URL: https://jsonlines.org/
[PWS-AUTHZ]
Portable Web Spaces Authorization Profile. Dmitri Zagidulin. W3C Credentials Community Group. URL: https://w3c-ccg.github.io/wallet-attached-storage-spec/authz-profile/
[PWS-EC]
Encrypted Collections for Portable Web Spaces. Dmitri Zagidulin. W3C Credentials Community Group. URL: https://w3c-ccg.github.io/wallet-attached-storage-spec/ec/
[RFC2119]
Key words for use in RFCs to Indicate Requirement Levels. S. Bradner. IETF. March 1997. Best Current Practice. URL: https://www.rfc-editor.org/info/rfc2119/
[RFC3339]
Date and Time on the Internet: Timestamps. G. Klyne; C. Newman. IETF. July 2002. Proposed Standard. URL: https://www.rfc-editor.org/info/rfc3339/
[RFC5023]
The Atom Publishing Protocol. J. Gregorio, Ed.; B. de hOra, Ed. IETF. October 2007. Proposed Standard. URL: https://www.rfc-editor.org/info/rfc5023/
[RFC6839]
Additional Media Type Structured Syntax Suffixes. T. Hansen; A. Melnikov. IETF. January 2013. Informational. URL: https://www.rfc-editor.org/info/rfc6839/
[RFC7516]
JSON Web Encryption (JWE). M. Jones; J. Hildebrand. IETF. May 2015. Proposed Standard. URL: https://www.rfc-editor.org/info/rfc7516/
[RFC7578]
Returning Values from Forms: multipart/form-data. L. Masinter. IETF. July 2015. Proposed Standard. URL: https://www.rfc-editor.org/info/rfc7578/
[RFC8174]
Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words. B. Leiba. IETF. May 2017. Best Current Practice. URL: https://www.rfc-editor.org/info/rfc8174/
[RFC8288]
Web Linking. M. Nottingham. IETF. October 2017. Proposed Standard. URL: https://httpwg.org/specs/rfc8288.html
[RFC8631]
Link Relation Types for Web Services. E. Wilde. IETF. July 2019. Informational. URL: https://www.rfc-editor.org/info/rfc8631/
[RFC9110]
HTTP Semantics. R. Fielding, Ed.; M. Nottingham, Ed.; J. Reschke, Ed. IETF. June 2022. Internet Standard. URL: https://httpwg.org/specs/rfc9110.html
[RFC9111]
HTTP Caching. R. Fielding, Ed.; M. Nottingham, Ed.; J. Reschke, Ed. IETF. June 2022. Internet Standard. URL: https://httpwg.org/specs/rfc9111.html
[RFC9264]
Linkset: Media Types and a Link Relation Type for Link Sets. E. Wilde; H. Van de Sompel. IETF. July 2022. Proposed Standard. URL: https://www.rfc-editor.org/info/rfc9264/
[RFC9457]
Problem Details for HTTP APIs. M. Nottingham; E. Wilde; S. Dalal. IETF. July 2023. Proposed Standard. URL: https://www.rfc-editor.org/info/rfc9457/