Wallet Attached Storage v0.3

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/wallet-attached-storage-spec/
History:
Commit history
Editor:
Dmitri Zagidulin
Feedback:
GitHub w3c-ccg/wallet-attached-storage-spec (pull requests, new issue, open issues)

Abstract

Wallet Attached Storage 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, 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 Wallet Attached Storage (WAS) 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 (11. Resources and Blobs) and the authorization profile (5.1 WAS Authorization Profile v0.1); 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

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.
  • (upcoming) v0.4 (mid-July 2026) -- Migrated to the W3C Credentials Community Group (CCG) for further incubation. Likely pending a rename (to be discussed by the group).

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 WAS just as writes are. The expanded form -- with the Digest, Capability-Invocation, and Signature headers -- appears once, in 5.1.3 Performing Authorized API Calls.

A trailing slash means the container. A path ending in / addresses a container: GET lists its members and POST adds a member to it. The same path without the trailing slash addresses the item itself: GET returns that item's description, PUT creates or replaces it, DELETE removes it. So /space/{space_id}/{collection_id}/ is the Collection's member list, while /space/{space_id}/{collection_id} is the Collection's description.

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 E. Error Type Registry, path segments this specification reserves in B. Reserved Path Segment Registry, and client-side encryption schemes in C. 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 11. Resources and Blobs plus 5.1 WAS Authorization Profile v0.1.

Profile Adds Defining sections
Minimal Resource CRUD (KV + blob read/write) + authorization 11. Resources and Blobs, 5.1 WAS Authorization Profile v0.1
+ Listing list resources / collections / spaces 10.5 List Collection operation, 9.5 List All Collections operation, 8.2 List Spaces Operation
+ Collection mgmt create / manage collections in a Space 10. Collections
+ Space mgmt manage an individual Space 9.2 Read Space operation (Space endpoints)
+ Multi-tenant create / manage many Spaces on a server 8. Spaces Repositories
+ Extensions linksets, policy, metadata, export, backends, query, quotas, encryption, replication, versioning 12. Linksets

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.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 13. Backends, query, quotas, client-side encryption (via Encrypted Data Vaults), replication, and versioning -- discovered through the linkset feature-detection mechanism (from [RFC9264]; see 12. Linksets).

Normative status. Section back-placement does not imply informative status. Everything from 2. Introduction through 14. Quotas is normative, as are the appendices A. Pagination, B. Reserved Path Segment Registry, C. Encryption Scheme Registry, and E. Error Type Registry (each of which also carries an inline "This appendix is normative." banner). The remaining appendices -- F. Goals and Requirements and G. 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). WAS reads are authorized too, so the header is required on the GET just as on the PUT. How that invocation is constructed is defined in 5.1.3 Performing Authorized API Calls, and its fully expanded form -- with the Digest, Capability-Invocation, and Signature headers spelled out -- is shown once in the worked example within that section. Producing that signature requires a conformant WAS 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 B. Reserved Path Segment Registry) but does not yet define an operation for; a server MUST NOT repurpose a reserved path.

2.6.1 Core

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:

Advanced Resource Endpoints:

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 D. Query Profile Registry.

Backend Management Endpoints (see 13. Backends):

Quota Endpoints (see 14. Quotas):

  • GET /space/{space_id}/quotas -- 14. 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 -- 14. Quotas: the Quota report object for the specific Collection (not all Backends will support per-collection quotas however)

3. Terminology

action (allowedAction)
The kind of operation a request performs on a target, named by a capability so it can be authorized. WAS uses the uppercase HTTP method names (GET, POST, PUT, DELETE) as its action vocabulary. See section 5.1.5 Authorization Actions and the Root Capability.
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 13. 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 10. 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.1.7 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 14. Quotas.
root capability
The implied capability for a target whose controller is the Space's controller; it is the root of trust from which all other capabilities for that target are delegated. See section 5.1.5.1 Root Capability.
target (invocationTarget)
The resource a request acts on, including the full request URL (scheme, host, port, and path) -- and the scope a capability authorizes. A capability's invocationTarget MUST match the request target for the invocation to be valid. See section 5.1.5 Authorization Actions and the Root Capability.
zCap (Authorization Capability)
See [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:

As the state of the art in cross-domain authorization advances, we expect there to be multiple profiles and specs that could be used to perform WAS API calls. However, to start with, this specification will focus on a single minimal authorization profile.

5.1 WAS Authorization Profile v0.1

Like many authorization specifications, the WAS Authorization Profile tries to address opposing tensions. On the one hand, to cover the full range of use cases, it needs to be delegatable, revocable, secure, flexible, and thus capability-based. On the other hand, for ease of implementation and adoption, and for maximum developer usability, the profile must make the most common operations as simple and friction-free as possible.

To that end, the profile offers the following layered mechanisms.

  1. Root Access: For basic admin CRUD operations, use the space's controller DID directly to sign API calls with HTTP Signatures.
  2. Public Read: For the common "public read" use case (the typical web publishing workflow, where a site or a file is shared for anyone to access via an HTTP GET), use the simple { "type": "PublicCanRead" } WAS Authorization syntax, see below.
  3. Advanced Delegatable Capabilities ("anyone with the link..." style): Use zCaps Authorization Capabilities v0.3
  4. Policy Based Access Control (including the familiar "share with this list of people or groups" style): Use the space's linkset property to point to a linkset that includes a URL to an access control policy document.

5.1.1 Authorization Specification Dependencies at a Glance

The initial WAS Authorization Profile uses the following specifications.

  1. Identity (for controllers or clients/agents): DID 1.0
  2. Capability data model: Authorization Capabilities for Linked Data v0.3
  3. Protocol for getting authorization: Out of scope (implementers are encouraged to use VC-API, OpenId4VP, OAuth2, or GNAP, as appropriate)
  4. Proof of Possession / authorization invocation: HTTP Signatures. MUST - RFC 9421 HTTP Message Signatures, MAY - HTTP Signatures (Cavage draft 12)
  5. Request body integrity: the Digest header, bound to the request signature -- see 5.1.4 Request Body Integrity (Digest Header)
  6. Access Control / Policy language data model: see 5.1.7 Access Control Policies (PublicCanRead is the only normative type for v0.1)

5.1.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, by one of the following:

  1. Direct: Provide a root capability invoked directly by the controller, or
  2. Delegated: Invoke a capability delegated to some other agent by the controller, or
  3. Matching Policy: (if using any kind of access control policy mechanism) Match an authorization policy specified in the linkset property of the space. This resource is related to the space controller because initially, it can only be modified either by the controller or an authorized party delegated to by the controller.

Space controllers MUST be in the form of a DID.

For minimal compatibility, all WAS 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.

When a space is created via an HTTP POST or PUT operation (see 8.1.1 (HTTP API) POST /spaces/ and 9.3.1 (HTTP API) PUT /space/{space_id}), 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 invocation is authorized by that controller, by one of the first two mechanisms above: either directly -- the signing key (key ID) used in the headers is authorized in the capabilityInvocation section of the controller's DID document -- or via a capability delegated by the controller to the signing DID. (The third mechanism, matching policy, does not apply: no Space, and therefore no policy, exists yet.)

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

5.1.3 Performing Authorized API Calls

Unless otherwise explicitly allowed via access control policy (see below), all WAS API calls require authorization.

This can be done in one of two ways:

  1. (for admin-like root access) Use the controller DID directly to sign HTTP API requests using the HTTP Signatures specification, invoking the target's root capability.
  2. (for advanced delegatable use cases) Use HTTP Signatures in combination with Authorization Capabilities v0.3, and include a capability invocation header in the API request.

Throughout this specification, the request examples abbreviate this header as a placeholder, Authorization: ..., rather than reproducing a full signature or capability invocation. In each case it stands for a credential constructed as described in 5.1.3 Performing Authorized API Calls.

5.1.4 Request Body Integrity (Digest Header)

When an authorized request carries a body (a Resource write, a Space create, and so on), this profile binds the body to the request's HTTP Signature so that the payload cannot be substituted without invalidating the signature:

  1. The client MUST include a Digest header whose value is the hash of the request body, carried as a mh (multihash) parameter: a multibase base64url-encoded (u prefix) multihash of the body's SHA-256 digest (Multihash and Multibase as defined in the CID 1.0 specification). For example:

    Digest: mh=uEiCPO-qYr-z0GYV5F75-N1l8Rhjv4xIkKZsnbTZeZ7emSA
    
  2. The content-type and digest headers MUST be included in the signature's covered (signed) headers list, alongside (key-id), (created), (expires), (request-target), host, and capability-invocation.

  3. For any request that carries a Content-Type header, the server MUST require digest among the covered headers, and SHOULD independently recompute the digest of the received body and compare it to the Digest header value. A missing, malformed, or non-matching Digest on a request with a body is rejected with an invalid-authorization-header (400) error.

Bodyless requests (GET, HEAD, DELETE) carry no Digest header.

Example authorized write request, showing the Digest header and the covered headers list (the space's controller invoking the root capability for the target; line breaks within the Authorization header are for display only):

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
Capability-Invocation: zcap id="urn:zcap:root:https%3A%2F%2Fexample.com%2Fspace%2F81246131-69a4-45ab-9bff-9c946b59cf2e%2Fphotos%2Fsunset.png",action="PUT"
Authorization: Signature keyId="did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW#z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW",
  headers="(key-id) (created) (expires) (request-target) host capability-invocation content-type digest",
  signature="6GoRQ+rW69wBhNyERkafAXEZXZArezHvGRNUWC0HNI4Ss1xAiiMHdayS5aA2R6hLuYRNw6h9J9eCmQVMuHE1Bw==",
  created="1758150502",expires="1758151102"

...binary PNG bytes...
Editor's note
**Digest vs Content-Digest.** The Digest header used by this profile descends from [RFC3230] (Instance Digests in HTTP). [RFC9530] (Digest Fields) obsoletes RFC 3230 and replaces Digest with Content-Digest / Repr-Digest. The current WAS implementation stack uses the legacy header with a multihash value; migration to Content-Digest (alongside the move to [RFC9421] HTTP Message Signatures) is a future direction for this profile.

5.1.5 Authorization Actions and the Root Capability

A capability invocation names an action that the invoked capability must permit. WAS uses the uppercase HTTP method names as its action vocabulary:

  • GET -- read a Space, Collection, or Resource. A HEAD request is authorized as a GET.
  • POST -- create a child item in a container (add a Resource to a Collection, a Collection to a Space, or a Space to the Spaces Repository).
  • PUT -- create-by-id or replace a Space, Collection, or Resource.
  • DELETE -- delete a Space, Collection, or Resource.

A request is authorized by a capability when all the following hold:

  1. the capability's invocationTarget matches the request's target -- the full request URL (scheme, host, port, and path);
  2. the capability's allowedAction includes the request's action (the HTTP method); and
  3. the invocation is signed by a key the capability authorizes, carried as a valid HTTP Signature over the request (see 5.1.3 Performing Authorized API Calls).
5.1.5.1 Root Capability

Every target has an implied root capability whose controller is the Space's controller. It is identified by the URI urn:zcap:root: followed by the percent-encoded target URL:

{
  "@context": "https://w3id.org/zcap/v1",
  "id": "urn:zcap:root:https%3A%2F%2Fexample.com%2Fspace%2F81246131-69a4-45ab-9bff-9c946b59cf2e%2Fmessages%2Fhello-world",
  "invocationTarget": "https://example.com/space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/hello-world",
  "controller": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW"
}

The Space controller MAY invoke the root capability directly -- signing the request with a key listed in the capabilityInvocation section of the controller's DID document -- to perform any operation. This is the "root access" path. All other authorized access derives from a capability delegated, directly or transitively, from this root.

5.1.5.2 Delegation

To grant another agent access, the controller (or any agent holding a sufficiently broad capability) delegates a capability that names the grantee as its new controller, the invocationTarget to scope it to, and the allowedActions to permit. A delegation MAY set an expires time. For example, granting another DID read-only access to a single Collection:

{
  "@context": "https://w3id.org/zcap/v1",
  "id": "urn:uuid:6c9f3a1e-2b4d-4f8a-9c1e-7d2b3a4c5e6f",
  "parentCapability": "urn:zcap:root:https%3A%2F%2Fexample.com%2Fspace%2F81246131-69a4-45ab-9bff-9c946b59cf2e%2Fmessages%2F",
  "invocationTarget": "https://example.com/space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/",
  "controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
  "allowedAction": ["GET"],
  "expires": "2026-12-31T23:59:59Z",
  "proof": { "...": "delegation proof signed by the parent capability's controller" }
}

The delegated capability is handed to the recipient out of band. The recipient invokes it by signing a request with their own key and including the capability in the Capability-Invocation header.

5.1.7 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 12.1 Space Linkset and 12.2 Collection Linkset).

Evaluation contract:

  • Capability first, policy second. A request is first checked against any capability invocation it carries. The effective policy is consulted only as a fallback, and it can only broaden access -- a policy never denies a caller who presents a valid capability.
  • Fail-closed. An absent policy, or a policy whose type an implementation does not recognize, grants nothing.
  • Most-specific-wins inheritance. The effective policy for a target is the one set at the most specific level that has a policy document: a Resource policy overrides a Collection policy, which overrides a Space policy.
  • Access kind. For policy evaluation, the request action is reduced to a coarse access kind: GET (and HEAD) is a read; POST, PUT, and DELETE are a write.
5.1.7.1 PublicCanRead

For v0.1, the only normative policy type is PublicCanRead:

{ "type": "PublicCanRead" }

It grants the read access kind to any caller (including unauthenticated ones) and grants no write access. This is the canonical "public read" pattern -- for example, hosting an HTML file or an image that anyone may GET, while writes still require a capability. Setting it on a Space makes the whole Space public-readable (subject to any more specific Collection or Resource policy); setting it on a single Resource exposes only that Resource.

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 E. 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 8.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

WAS 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

Servers and backends MAY support [RFC9110] conditional requests to provide optimistic concurrency control on writes. This mechanism helps prevent 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 backend that supports this advertises the conditional-writes feature in its Backend description (see 13.1 Backend Data Model); a client SHOULD use these preconditions only against a backend that advertises support.

When supported, 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 backend derives it is a server-side concern (see 13.1 Backend Data Model).

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

A server that supports conditional writes MUST evaluate the precondition atomically with the write, so that two concurrent writers cannot both observe the same prior version and both succeed; how a backend achieves this atomicity is a server-side concern (see 13.1 Backend Data Model). A client recovers from a 412 by re-reading the current Resource, re-applying its change on top of the new version, and retrying.

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.

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 -- 8.2 List Spaces Operation, 9.5 List All Collections operation, and 10.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. 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.

8.1 Create Space operation

To create a Space:

8.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

8.1.2 Create Space Errors

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

  • invalid-request-body (400) -- the required controller property is missing from the request body.
  • 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 9.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 description (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.

8.2 List Spaces Operation

8.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"
    }
  ]
}

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).

9. 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).

9.1 Space Data Model

Space properties:

Space properties automatically added by the server:

9.2 Read Space operation

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

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

Example request:

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

Example success response:

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

{
  "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"
}

9.2.2 Read Space Errors

Errors (see E. 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.

9.3 Update (or Create by Id) Space operation

When creating or modifying a Space via PUT, the client specifies the id of the Space.

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 8.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 8.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 9.1 Space 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.

9.3.1 (HTTP API) PUT /space/{space_id}

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), note the lack of trailing slash:

PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e 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:

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

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 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

Errors (see E. 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.

9.4 Delete Space operation

9.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 E. 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.

9.5 List All Collections operation

9.5.1 (HTTP API) GET /space/{space_id}/collections/

Example request (note the trailing slash):

GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/collections/ 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/collections/",
  "totalItems": 2,
  "items": [
    {
      "id": "example",
      "url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/example",
      "name": "Example Collection"
    },
    {
      "id": "73WakrfVbNJBaAmhQtEeDv",
      "url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv",
      "name": "73WakrfVbNJBaAmhQtEeDv"
    }
  ]
}

Errors (see E. 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.

10. 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.

10.1 Collection Data Model

Collection properties (user-writable):

Collection properties automatically added by the server:

Example collection (JSON representation):

{
  "id": "73WakrfVbNJBaAmhQtEeDv",
  "url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv",
  "type": ["Collection"],
  "name": "Verifiable Credentials Collection",
  "createdBy": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW",
  "linkset": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/linkset"
}

10.2 Create Collection (Add Collection to a Space) operation

When a Collection is created via a POST, the client can specify the id of the Collection. If the id is not specified, one is auto-generated by the server and returned as part of the Location response header.

10.2.1 (HTTP API) POST /space/{space_id}/collections/

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

POST /space/81246131-69a4-45ab-9bff-9c946b59cf2e/collections/ 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/collections/ 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 E. Error Type Registry for canonical examples):

10.3 Update (or Create By Id) Collection operation

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

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

Example successful "create" request (note the lack of trailing slash):

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

{
  "id": "73WakrfVbNJBaAmhQtEeDv",
  "name": "Verifiable Credentials Collection",
  "type": ["Collection"],
  "backend": { "id": "default" }
}
HTTP/1.1 201 Created

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

10.4 Get Collection Description operation

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

Example request (note: no 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": "Verifiable Credentials Collection",
  "type": ["Collection"],
  "linkset": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/linkset",
  "backend": { "id": "default" }
}

Errors (see E. 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.

10.5 List Collection operation

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

Example request (note 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" 
    }
  ]
}
10.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"
    }
  ]
}

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

Errors (see E. 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.

10.6 Delete Collection operation

10.6.1 (HTTP API) DELETE /space/{space_id}/{collection_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 collection that does not exist (or has already been deleted) results in a 204 success response

Example request (note the lack of trailing slash):

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. Resources and Blobs

11.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 11.8 Resource Metadata Data Model), and the stored bytes are returned verbatim on reads (see 11.3 Content Types and Representations).

11.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:

11.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 11.8 Resource Metadata Data Model).

Description objects. API documents generated by the server -- Space and Collection descriptions, listings, quota reports, Metadata objects -- 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.

11.4 Create Resource (Add Resource to Collection) Operation

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

Example request (adds a JSON object to the messages collection). Note that since no Resource id was specified, the server auto-generated an id and returned it as part of the Location response header.

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 11.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 E. Error Type Registry for canonical examples):

11.5 Read Resource Operation

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

11.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 E. 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.

11.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 B.2 Collection-level reserved endpoints.

11.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

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 11.3 Content Types and Representations). The Digest header binds the body to the request signature when using the WAS Authorization Profile (see 5.1.4 Request Body Integrity (Digest Header)):

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 E. Error Type Registry for canonical examples):

This operation accepts the If-Match / If-None-Match write preconditions described in 7.2 Conditional Requests when the target backend advertises the conditional-writes feature.

11.7 Delete Resource Operation

11.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

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 E. 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.

11.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 B.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.

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):

Editor's note
**Versioning is optional and backend-advertised.** This metadata model does not mandate a stored etag / version property. When a backend advertises the conditional-writes feature it exposes a Resource version as an HTTP ETag validator and honors If-Match / If-None-Match preconditions; see 7.2 Conditional Requests. A backend without that feature performs unconditional last-writer-wins upserts. The broader Transaction (multi-write atomic) mechanism remains deferred.

User-writable properties:

On a Collection that declares an encryption marker (see C. 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. Because the server cannot read an encrypted name, 10.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 ({}).

11.9 Read Resource Metadata Operation

11.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",
  "custom": {
    "name": "Hello World greeting",
    "tags": { "project": "demo", "status": "draft" }
  }
}

Errors (see E. 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.

11.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). Server-managed properties are not affected by this operation; a server MUST ignore any top-level properties other than custom 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).

Unlike the 11.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.

11.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 E. Error Type Registry for canonical examples):

12. 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]) serve as the main feature detection and extension mechanism. They can be discovered, via the linkset property, from the following:

12.1 Space Linkset

The space linkset resource (one of the B.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://wallet.storage/spec#policy": [
        { 
          "href": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/policy",
          "type": "application/json"
        }
      ],
      "https://wallet.storage/spec#backends-available": [
        {
          "href": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/backends",
          "type": "application/json"
        }
      ],
      "https://wallet.storage/spec#quotas": [
        {
          "href": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/quotas",
          "type": "application/json"
        }
      ]
    }
  ]
}

12.2 Collection Linkset

The collection linkset resource (one of the B.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://wallet.storage/spec#policy": [
        { 
          "href": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/policy",
          "type": "application/json"
        }
      ],
      "https://wallet.storage/spec#backend": [
        {
          "href": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/backend",
          "type": "application/json"
        }
      ],
      "https://wallet.storage/spec#quota": [
        {
          "href": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/quota",
          "type": "application/json"
        }
      ]
    }
  ]
}

13. 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.

13.1 Backend Data Model

A Backend description object advertises a backend's identity and features so that clients can select a suitable backend for each Collection (and so that storage management UIs can present meaningful choices to users).

Backend description properties:

Each token names something the server must actively do. Note that client-side encryption is deliberately not a backend feature: 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 capability of the backend. Concretely, this signal is the Collection's optional encryption marker (see 10.1 Collection Data Model): a non-secret declaration any authorized reader discovers from the Collection Description, while the keys stay in the client.

A backend that advertises conditional-writes takes on the server-side half of the client contract in 7.2 Conditional Requests. It MAY derive the ETag from an internal monotonic version counter (such as an Encrypted Data Vault document sequence); the validator is opaque to clients. It MUST evaluate a write's If-Match / If-None-Match precondition atomically with the write (for example, under a per-Resource lock), so that two concurrent writers cannot both observe the same prior version and both succeed. An in-process per-Resource lock satisfies this for a single-instance server only; a horizontally-scaled deployment needs to coordinate the check-and-write across instances (e.g. an atomic compare-and-swap on the stored version or a shared lock).

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.

13.2 Space Backends Available

The list of backends registered on a Space is discoverable via the backends-available relation in the linkset (see 12.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",
    "storageMode": ["document", "blob"],
    "persistence": "durable"
  },
  {
    "id": "dropbox",
    "name": "Dropbox",
    "managedBy": "external",
    "storageMode": ["blob"],
    "persistence": "durable"
  },
  {
    "id": "edv",
    "name": "Encrypted Data Vault",
    "managedBy": "server",
    "storageMode": ["document", "blob"],
    "persistence": "durable",
    "features": ["conditional-writes", "blinded-index-query", "chunked-streams"]
  }
]

13.3 Collection Backend Selected

Each collection has an optional backend property that is set during its creation (see 10.1 Collection 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 12.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.
  • A Collection-level storageMode declaration, validated against each replica's backend at creation time.
  • 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 14. 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".

14. 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.

14.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 12.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"
    }
  ]
}

14.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 }
      ]
    }
  ]
}

14.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 12.2 Collection Linkset).

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

Errors (see E. 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 -- 8.2 List Spaces Operation, 9.5 List All Collections operation, and 10.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 5.1.5.1 Root Capability). A server MUST NOT require a distinct capability per page.

B. Reserved Path Segment Registry

This appendix is normative.

B.1 Space-level reserved endpoints

The following path segments represent reserved API endpoints for 9. 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}/policy policy Access control policy
/space/{space_id}/backends backends Storage backends available
/space/{space_id}/collections collections List and create collections
/space/{space_id}/export export Export (download) space contents
/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.

B.2 Collection-level reserved endpoints

The following path segments represent reserved API endpoints for 10. 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}/query query Query resources within a collection (see D. 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.

B.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)

C. 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 marker (see 10.1 Collection Data Model) names a client-side encryption scheme. This registry maps each scheme token 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 Media type Envelope profile Reference
edv 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

C.1 Server-side write validation

A server that recognizes the scheme declared by a Collection's encryption marker MUST validate the body of every Resource content write (POST or PUT) into that Collection against the scheme'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 11.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).

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.

C.2 Accepting a marker only when it can be enforced

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

A server MAY instead choose to store markers for schemes it does not enforce (treating the marker as fully opaque, per 10.1 Collection 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.

C.3 Validation profile

A server MAY implement the edv 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"] }
      ]
    }
  }
}

D. 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 serve a given profile, whether because the server does not implement it at all, or because the Collection's backend lacks the capability (see 13.1 Backend Data Model), MUST respond with an unsupported-operation (501) error.

The profile vocabulary is open and additive, mirroring the backend features vocabulary (see 13.1 Backend Data Model): 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 request Digest (see 5.1.4 Request Body Integrity (Digest Header)), 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) D.1 changes profile
blinded-index Matching over client-computed blinded (HMAC'd) attributes D.2 blinded-index profile

D.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 SHOULD advertise the changes-query backend feature (see 13.1 Backend Data Model).

Request body:

{
  "profile": "changes",
  "checkpoint": { "id": "<resourceId>", "updatedAt": "<ISO-8601 timestamp>" },
  "limit": 100
}

Response body:

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

Each entry in documents describes one changed Resource:

Tombstones. A soft-deleted Resource surfaces as { "id", "_deleted": true, "updatedAt", "version" } with no data member; the deletion bumps version. A tombstone retains its createdBy, where one was recorded, so that a deletion replicates together with the attribution of the Resource it removes.

Ordering and resumption. Entries are ordered by an ascending (updatedAt, id) keyset. The top-level checkpoint echoes the position 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 metadata-only edit re-surfaces the Resource with a bumped updatedAt and metaVersion but an unchanged version and data.

Scope. Binary (non-JSON) Resources are excluded from the changes feed; blob replication is out of scope for this profile.

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": { "id": "hello-world", "updatedAt": "2026-01-15T12:00:00.000Z" }
}

D.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 C. Encryption Scheme Registry references for the edv envelope). A server that serves this profile SHOULD advertise the blinded-index-query backend feature (see 13.1 Backend 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 WAS 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
}

D.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 E. 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.

E. 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.

type URI Anchor Typical status Description
https://wallet.storage/spec#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://wallet.storage/spec#invalid-id invalid-id 400 A Space, Collection, or Resource id is missing or not URL-safe.
https://wallet.storage/spec#reserved-id reserved-id 409 A client-supplied id collides with a B. Reserved Path Segment Registry segment.
https://wallet.storage/spec#id-conflict id-conflict 409 A client-supplied id in a POST create operation already exists. (Create-or-replace by id is done idempotently via PUT, which does not conflict.)
https://wallet.storage/spec#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://wallet.storage/spec#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://wallet.storage/spec#missing-content-type missing-content-type 400 A required Content-Type header is missing.
https://wallet.storage/spec#missing-authorization missing-authorization 401 Required Authorization / Capability-Invocation headers (or proof of possession) are missing.
https://wallet.storage/spec#invalid-authorization-header invalid-authorization-header 400 An Authorization, Capability-Invocation, or Digest header is malformed, unparseable, or failed verification.
https://wallet.storage/spec#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 8.1.2 Create Space Errors.
https://wallet.storage/spec#unsupported-backend unsupported-backend 409 A requested backend id is not in the space's 13.2 Space Backends Available list.
https://wallet.storage/spec#encryption-immutable encryption-immutable 409 A Collection update tried to change or clear an existing encryption marker. The marker is set-once: declaring it on a Collection that lacks one is allowed, but changing its scheme (or clearing it) on a populated Collection would corrupt the stored, client-encrypted Resources. See 10.1 Collection Data Model.
https://wallet.storage/spec#encryption-scheme-mismatch encryption-scheme-mismatch 422 A Resource write into an encrypted Collection 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 C. Encryption Scheme Registry.
https://wallet.storage/spec#unsupported-encryption-scheme unsupported-encryption-scheme 400 A Collection create/update declared an encryption scheme the server does not recognize or support. See C. Encryption Scheme Registry.
https://wallet.storage/spec#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://wallet.storage/spec#quota-exceeded quota-exceeded 507 A write was rejected because the target backend's storage quota is exhausted. See 14. Quotas.
https://wallet.storage/spec#payload-too-large payload-too-large 413 An upload exceeds the target backend's maxUploadBytes constraint (see 14. Quotas). Note that unlike quota-exceeded, this rejection is per-request: smaller uploads may still succeed.
https://wallet.storage/spec#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://wallet.storage/spec#invalid-import invalid-import 400 An uploaded archive is not a valid WAS space export.
https://wallet.storage/spec#storage-error storage-error 500 An underlying storage operation failed.
https://wallet.storage/spec#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 8.1.2 Create Space Errors.

E.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://wallet.storage/spec#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://wallet.storage/spec#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 B. Reserved Path Segment Registry:

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

{
  "type": "https://wallet.storage/spec#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://wallet.storage/spec#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://wallet.storage/spec#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://wallet.storage/spec#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://wallet.storage/spec#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://wallet.storage/spec#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 13.2 Space Backends Available list:

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

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

encryption-immutable -- a Collection update tried to change or clear an existing encryption marker (it is set-once; see 10.1 Collection Data Model):

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

{
  "type": "https://wallet.storage/spec#encryption-immutable",
  "title": "Collection encryption marker is immutable."
}

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

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

{
  "type": "https://wallet.storage/spec#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://wallet.storage/spec#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://wallet.storage/spec#unsupported-operation",
  "title": "Backend 'default' does not support per-collection quota reports."
}

F. Goals and Requirements

This section is non-normative.

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

F.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.

F.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.

F.3 Stored data is opaque to the storage provider

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

F.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.

F.6 Data Portability

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

F.7 A Plurality of Data Formats and Protocols

F.8 Permissioned Query and Search functionality

F.9 Upgradeable and legislation-compliant cryptography

All cryptography has a half-life.

F.10 Anti-Goals

F.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.

G. 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.

H. References

H.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/
[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/
[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/
[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/

H.2 Informative references

[RFC3230]
Instance Digests in HTTP. J. Mogul; A. Van Hoff. IETF. January 2002. Proposed Standard. URL: https://www.rfc-editor.org/info/rfc3230/
[RFC9421]
HTTP Message Signatures. A. Backman, Ed.; J. Richer, Ed.; M. Sporny. IETF. February 2024. Proposed Standard. URL: https://www.rfc-editor.org/info/rfc9421/
[RFC9530]
Digest Fields. R. Polli; L. Pardue. IETF. February 2024. Proposed Standard. URL: https://www.rfc-editor.org/info/rfc9530/