Unofficial Draft
Copyright © 2026 the document editors/authors. Text is available under the Creative Commons Attribution 4.0 International Public License; additional terms may apply.
Wallet Attached Storage is a general purpose permissioned storage API.
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.
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.
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:
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.
Initial use cases that are motivating this work:
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.
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.
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.
Resource CRUD (Create, Read, Update, Delete):
POST /space/{space_id}/{collection_id}/ -- 11.4 Create Resource (Add Resource to Collection) OperationGET /space/{space_id}/{collection_id}/{resource_id} -- 11.5 Read Resource OperationPUT /space/{space_id}/{collection_id}/{resource_id} -- 11.6 Update (or Create By Id) Resource OperationDELETE /space/{space_id}/{collection_id}/{resource_id} -- 11.7 Delete Resource OperationHEAD instead of a GET to check a resource's headers/metadata without
fetching the whole resource. However, do not rely on this for atomic inserts
(that is, to check if a resource exists before attempting to create it).
Instead, use the backend-specific Transaction mechanisms when appropriate.List resources in a Collection:
If not implemented on a server, implies that only individual Key/Value operations are supported.
GET /space/{space_id}/{collection_id}/ -- 10.5 List Collection operationManage Collections in a Space:
If not implemented on a server, implies that collections are pre-configured or implicit, controlled by the server.
POST /space/{space_id}/collections/ -- 10.2 Create Collection (Add Collection to a Space) operationGET /space/{space_id}/collections/ -- 9.5 List All Collections operationGET /space/{space_id}/{collection_id} -- 10.4 Get Collection Description operationPUT /space/{space_id}/{collection_id} -- 10.3 Update (or Create By Id) Collection operationDELETE /space/{space_id}/{collection_id} -- 10.6 Delete Collection operationSpaces 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.
POST /spaces/ -- 8.1 Create Space operationGET /spaces/ -- 8.2 List Spaces OperationSpace Endpoints -- Manage an individual Space:
GET /space/{space_id} -- 9.2 Read Space operationPUT /space/{space_id} -- 9.3 Update (or Create by Id) Space operationDELETE /space/{space_id} -- 9.4 Delete Space operationPOST /space/{space_id}/export -- Reserved / not yet specified. Export
(download) a Space's contents (all collections and resources).Advanced Resource Endpoints:
GET /space/{space_id}/{collection_id}/{resource_id}/meta -- 11.9 Read Resource Metadata OperationPUT /space/{space_id}/{collection_id}/{resource_id}/meta -- 11.10 Update Resource Metadata OperationPolicy 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.
GET /space/{space_id}/linkset -- 12.1 Space LinksetGET /space/{space_id}/{collection_id}/linkset -- 12.2 Collection LinksetQuery 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):
GET /space/{space_id}/backends -- 13.2 Space Backends AvailableGET /space/{space_id}/{collection_id}/backend -- 13.3 Collection Backend Selected
(the backend summary is also displayed in the Collection description object)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)GET, POST, PUT, DELETE) as its action
vocabulary. See section
5.1.5 Authorization Actions and the Root Capability.backend property, and is assigned
the default backend when it does not. See section
13. Backends.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./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.invocationTarget MUST match the request target for the invocation to be
valid. See section 5.1.5 Authorization Actions and the Root Capability.Space, Collection, and Resource identifiers used in this specification are required to have the following properties.
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.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:
/posts/2020-01-01-hello-world).This specification uses [RFC9457] Problem Details for HTTP APIs for error responses.
application/problem+json
content type.type and title properties are REQUIRED.errors array with { detail, pointer } objects is encouraged where appropriate.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:
Content-Type, or a capability invocation or
signature that is present but malformed or fails to verify -- MAY be reported
with precise status codes and types (for example invalid-request-body,
missing-content-type, invalid-authorization-header, or
controller-mismatch). These responses describe the request and do not
disclose whether any particular target exists."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.
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.
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).
Cache-Control directive semantics beyond validation
are not yet specified.
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:
If-Match: "<etag>" -- perform the write only if the Resource's current
ETag matches it (an "update-if-unchanged"). If it does not match, the server
MUST NOT perform the write and MUST respond with precondition-failed
(412).If-None-Match: * -- perform the write only if the Resource does not yet
exist (a "create-if-absent"). If it already exists, the server MUST NOT
perform the write and MUST respond with precondition-failed (412).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.
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.
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.
To create a Space:
Perform an authorized Create Space operation that includes a Proof of (cryptographic material) Possession via a mechanism such as HTTP Signatures.
If no id is provided, it will be generated by the storage server.
A controller DID MUST be provided in the request body, and the request MUST
be authorized by that DID.
controller is provided, the server MUST return an HTTP 400 error
responsecontroller:
either signed directly by the controller DID (the common case), or signed
by another DID presenting a valid, unexpired delegation chain rooted in the
controller (see 5.1.5.2 Delegation). This is how the root of trust is
initially set up (see 5.1.2 Space controller and the Root of Trust for
more details).POST capability for /spaces/ by the user,
and create a Space that the user's DID controls from the start. What is
never acceptable is installing a controller that has authorized nothing
at all: that would let anyone bind arbitrary DIDs to Spaces without their
consent -- squatting meaningful ids "in their name", or burning
per-controller onboarding allowances such as "one free Space per
controller".(Optional, out of scope) A given storage provider MAY impose additional requirements to create a Space for a given controller, such as:
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:
id was not specified in the body of the request, and so was generated by
the server and returned in the responseErrors (see E. Error Type Registry for canonical examples):
controller property is
missing from the request body.controller DID.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).id is not URL-safe (see
4. Identifiers).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.
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.
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.
Requires appropriate authorization (root zcap invoked by the controller of one or more spaces, or a zcap granting permission to read a one or more spaces)
Lists only the spaces the requester is authorized to see
MAY be paginated (see A. Pagination)
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).
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).
Space properties:
id - A unique identifier for a space at a given server. Created by the
server if not provided. Note: the {space_id} template parameter used in URL
templates in this spec MUST match that space's id property. See
4. Identifiers for additional constraints.type - An array of strings, MUST include the type Space. The array SHOULD
be lexically sorted so that the object has a canonical, stable serialization
(useful for hashing or signing).name (optional) - An arbitrary human-readable name for the space. Does not
have to be unique.controller - A cryptographic identifier (a did)
of the entity that is authorized to perform operations on the space (or to
delegate authorization to other entities)Space properties automatically added by the server:
createdBy (optional) - The did of the party whose capability invocation
created the space. It is recorded on the first write and preserved unchanged
by later writes, so it names the creator rather than the last writer. It is
read-only: a server MUST ignore a createdBy supplied in a request body.
createdBy is distinct from controller -- under delegated provisioning
(see 5.1.2 Space controller and the Root of Trust) the party that creates a
space need not be the party that controls it. Recording it is OPTIONAL, so a
client MUST treat an absent createdBy as "not recorded" rather than as an
assertion that the space has no creator.url - A relative URL to the space's description resource.
Added by the server, used in the Space Description object as well as
the 8.2 List Spaces Operation result.linkset - A relative URL to a resource which contains
a set of links to auxiliary resources (such as to access control policy
documents). See section 12.1 Space Linkset.
Note that this is one of the B.1 Space-level reserved endpoints.idThe format of the response is determined based on content negotiation;
application/json is the REQUIRED baseline (see
11.3 Content Types and Representations).
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"
}
Errors (see E. Error Type Registry for canonical examples):
When creating or modifying a Space via PUT, the client specifies the id
of the Space.
namecontrollerThe 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.
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.
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):
id that does not match the
{space_id} in the request URL.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):
204; an under-authorized request returns 404 instead, per
6. Error Handling, so that an unauthorized caller cannot probe for
existence.id is not URL-safe.GET actionname property is optional, default it to be the same
value as id when name is missing. (The name is intended to drive UIs, so
defaulting to id simplifies consuming client logic.)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):
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).
query endpoint with field-based filtering.
Collection properties (user-writable):
id - A unique collection identifier (within a given space). Created by the
server if not provided. Note: the {collection_id} template parameter used in
URL templates in this spec MUST match that collection's id property.
See 4. Identifiers for additional constraints.type - An array of strings, MUST include the type Collection. As with a
Space's type, the array SHOULD be lexically sorted for a canonical, stable
serialization.name (optional) - An arbitrary human-readable name for the collection. Does not
have to be unique.backend (optional) - An object describing the storage backend selected for
this collection. If not specified, defaults to the value { "id": "default" }.
The backend object's id property MUST be from the list of 13.2 Space Backends Available
for the given space. If an unavailable (unsupported) backend is specified,
the server MUST throw an error.
See section 13. Backends for more details.encryption (optional) - A non-secret marker declaring that this collection's
Resources are client-side encrypted, and naming the scheme. The value is an
object with a required string scheme property (e.g. { "scheme": "edv" } for the
EDV-over-WAS scheme); absent means the collection is plaintext. The server
MUST NOT interpret the marker beyond validating its shape -- it never holds key
material and stores the marker opaquely. Its purpose is discovery: any
authorized reader (including a delegated consumer that did not create the
collection) learns from the Collection Description that the collection is
encrypted, and decrypts with its own keys. The marker is set-once: a server
MUST allow declaring it on a collection that lacks one (e.g. migrating a
pre-existing collection), but MUST reject (with an encryption-immutable error)
any attempt to change its scheme or clear it on an existing collection, since
that would corrupt the already-stored encrypted Resources. See section
13. Backends (client-side encryption note) for the rationale.
A server that recognizes the declared scheme enforces it structurally on
write -- rejecting any non-envelope body so plaintext can never be stored in
an encrypted Collection; see C. Encryption Scheme Registry.Collection properties automatically added by the server:
createdBy (optional) - The did of the party whose capability invocation
created the collection, on the same terms as a Space's createdBy (see
9.1 Space Data Model): recorded on the first write, preserved by later
writes, read-only, and OPTIONAL.url - A relative URL to the collection's description resource.
Added by the server, used in the Collection Description object as well as
the List Collections operations result.linkset - A relative URL to a resource which contains
a set of links to auxiliary resources (such as to access control policy
documents). See section 12.2 Collection Linkset.
Note that this is one of the B.2 Collection-level reserved endpoints.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"
}
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.
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):
id collides with one of the
B.1 Space-level reserved endpoints (for example, query or collections).id already exists.
To create or replace a Collection at a client-chosen id without conflict,
use the idempotent 10.3 Update (or Create By Id) Collection operation instead.
Servers MUST perform this existence check only after the caller's
authorization has been verified: an under-authorized caller receives the
privacy-merged not-found (404) per 6. Error Handling, so that it
cannot use the 409 to probe a Space for existing Collection ids.backend id is not in that
space's 13.2 Space Backends Available list.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.
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):
id collides with one of the
B.1 Space-level reserved endpoints (for example, collections or
linkset).encryption marker (the marker is set-once; see
10.1 Collection Data Model).GET actionExample 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):
GET actionnext link
and cursor continuation.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"
}
]
}
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):
Requires appropriate authorization
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
A unit of data, in transit or at rest. (As described in the W3C FileAPI: Blob Interface).
Blob properties:
size - length of the byte stream in bytestype (from the IANA mime type registry) - If not specified, defaults to
application/octet-streamWhen 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).
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:
id - A unique identifier for a resource for a given collection. Created by the
server if not provided. Note: the {resource_id} template parameter used in URL
templates in this spec MUST match that resource's id property (as returned by
the 10.5 List Collection operation).name - (optional) - Human-readable name for the resource, useful for building
user interfaces for browsing collection contents.type (optional) - An array of strings describing the resource. Unlike Space
and Collection, no specific type value is required of a Resource.url (optional) - A relative URL (provided by the server when listing resources
via the 10.5 List Collection operation)contentType (optional) - The MIME type of the default representation of the
resource (provided by the server when listing resources
via the 10.5 List Collection operation)contentType, size, optional timestamps) plus user-writable
ones (name and tags, nested under custom) -- addressable at the
reserved /meta path segment under the Resource URL. See
11.8 Resource Metadata Data Model.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:
application/json, or any type with a
+json structured-syntax suffix [RFC6839] (for example,
application/ld+json), is stored as a JSON document.application/octet-stream.multipart/form-data [RFC7578] uploads (the HTML form workflow). The
request MUST contain exactly one file part; that part supplies the stored
bytes, and its own content type (not multipart/form-data) becomes the
stored content type. Because a write targets a single Resource, there is no
batch form: a server MUST reject a multipart request with no file part, or
with more than one, with an invalid-request-body (400) error.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.
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):
Content-Type header (see 11.3 Content Types and Representations).id collides with one of the
B.2 Collection-level reserved endpoints (for example, query or
linkset).id already exists.
To create or replace a Resource at a client-chosen id without conflict,
use the idempotent 11.6 Update (or Create By Id) Resource Operation instead.
Servers MUST perform this existence check only after the caller's
authorization has been verified: an under-authorized caller receives the
privacy-merged not-found (404) per 6. Error Handling, so that it
cannot use the 409 to probe a Collection for existing Resource ids.maxUploadBytes constraint (see 14. Quotas).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).
GET actionExample 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):
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.
PUT action204 success responseExample 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):
Content-Type header (see 11.3 Content Types and Representations).id collides with one of the
B.2 Collection-level reserved endpoints (for example, query or
linkset).maxUploadBytes constraint (see 14. Quotas).If-Match /
If-None-Match precondition evaluated false, on a backend that advertises
the conditional-writes feature (see 7.2 Conditional Requests).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.
Requires appropriate authorization
DELETE actionThis 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):
204; a request that lacks sufficient authorization instead
returns 404, per 6. Error Handling, so that an unauthorized caller
cannot probe for existence.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:
custom property and
are set by the client via the 11.10 Update Resource Metadata Operation.
Keeping them in their own object makes the writable surface explicit and
leaves the top level free for future server-managed properties.Server-managed properties (contentType and size are REQUIRED in every
Metadata object; the timestamps and createdBy are OPTIONAL):
contentType - The MIME type of the stored representation of the Resource,
recorded from the Content-Type header of the request that last wrote the
Resource's content (application/octet-stream if none was provided). The
same value is reported in 10.5 List Collection operation results and as
the Content-Type header of GET/HEAD responses for the Resource itself.size - The length in bytes of the stored representation of the Resource
(see the size property of the 11.1 Blob Data Model).createdAt (optional) - The [RFC3339] date-time at which the Resource
was created.updatedAt (optional) - The [RFC3339] date-time at which the Resource's
content or user-writable metadata was last modified.createdBy (optional) - The did of the party whose capability invocation
created the Resource. It is recorded on the first write and preserved
unchanged by later writes, so it names the creator rather than the last
writer -- in a Collection written by several delegated agents, createdBy
and the party responsible for the current content need not be the same. Like
the other server-managed properties it is read-only: it cannot be set through
the 11.10 Update Resource Metadata Operation. Recording it is OPTIONAL, so a
client MUST treat an absent createdBy as "not recorded" rather than as an
assertion that the Resource has no creator.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:
custom (optional) - A JSON object holding the user-writable properties
below. A Metadata object with no user-writable properties set MAY omit
custom or report it as {}.name (optional) - A human-readable name for the Resource. This is the
same name property defined in the 11.2 Resource Data Model.tags (optional) - A JSON object of application-defined annotations.
Keys are strings chosen by the application; values SHOULD be strings, so
that tags stay cheap to index and filter on. Applications that need
richer structured metadata SHOULD store it as a Resource in its own
right rather than in tags.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 ({}).
GET actionExample 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):
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.
PUT action204 success responseExample 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):
custom object (or a property within it) does not have the shape
described in 11.8 Resource Metadata Data Model.Linksets (from [RFC9264]) serve as the main feature detection and extension
mechanism. They can be discovered, via the linkset property, from the following:
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:
policy relation
(https://wallet.storage/spec#policy) - A link to the
/space/{space_id}/policy resource, which contains a set of links to access
control policy documents.backends-available
(https://wallet.storage/spec#backends-available) - A link to the
/space/{space_id}/backends "Backends Available" resource.quotas relation
(https://wallet.storage/spec#quotas) -
A link to the /space/{space_id}/quotas per-backend storage quota report
(see 14. Quotas)./space/{space_id}/query - Reserved for cross-space query operations.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"
}
]
}
]
}
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:
https://wallet.storage/spec#policy) - A link to the
/space/{space_id}/{collection_id}/policy resource, which contains a set of links
to access control policy documents.backend relation
(https://wallet.storage/spec#backend) - A link to the
detailed /space/{space_id}/{collection_id}/backend "Backend Selected for this
collection" resource.quota relation
(https://wallet.storage/spec#quota) - A
link to the /space/{space_id}/{collection_id}/quota storage quota report
for this collection (see 14. Quotas)./space/{space_id}/{collection_id}/query - (Optional) Query operations within
a collection, discriminated by a request-body profile (see
D. Query Profile Registry).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"
}
]
}
]
}
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.
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:
id (required) - A unique backend identifier (within a given space). The id
default is conventionally used for the server-assigned default backend.
See 4. Identifiers for additional constraints.name (optional) - An arbitrary human-readable name for the backend. Does
not have to be unique.managedBy (optional) - Who operates the backend. One of:server - configured and operated server-side (for example, the server's
filesystem or database backends). This is the default if unspecified.external - a "Bring Your Own Storage" provider registered by the client
(for example, a connected Dropbox account).storageMode (optional) - An array of storage modalities the backend
supports. This specification defines two values: document (structured JSON
resources) and blob (opaque binary byte streams). Advertising modalities up
front prevents mismatches, such as attempting to store a multi-gigabyte
binary file on a JSON-only document store. If unspecified, clients SHOULD
assume both modalities are supported.persistence (optional) - Either durable (the engine stores data on
persistent media, e.g. disk, and it survives a restart) or volatile (the
engine stores data in memory, e.g. a RAM-backed cache tier, and data may not
survive a restart). Defaults to durable. Note that this is a technical
property of the storage engine, deliberately distinct from any administrative
data-retention rules (see the editor's note on lifecycle configuration below).features (optional) - An array of feature tokens advertising optional
server affordances the backend provides beyond the baseline read/write API,
so that clients can gate behavior on what the backend actually does. The
vocabulary is open and additive: a client MUST ignore tokens it does not
recognize, and MUST treat an absent token (or an absent features array) as
"not supported" rather than assuming a default. Tokens this specification
defines:conditional-writes - the backend enforces If-Match / If-None-Match
write preconditions (see 7.2 Conditional Requests).changes-query - the backend serves the changes profile of the query
endpoint (see D. Query Profile Registry).blinded-index-query - the backend serves the blinded-index profile of the
query endpoint (see D. Query Profile Registry).chunked-streams - the backend supports chunk addressing for large blobs.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).
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"]
}
]
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).
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: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.synced, syncing, stale);
the exact state machine, including how "cold boots" of volatile backends
are surfaced, is to be determined.storageMode declaration, validated against each
replica's backend at creation time.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.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".
Quota reporting and enforcement are optional, and support is backend-dependent. The quota API serves two distinct consumers:
include query
parameter.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.
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:
id, name, managedBy - identifying properties from the corresponding
Backend description object (see 13.1 Backend Data Model).state - the backend's current condition, one of: ok, near-limit,
over-quota, or unreachable.usageBytes - total bytes consumed on this backend by this Space.limit - an object with capacityBytes and an isUnlimited boolean (when
isUnlimited is true, capacityBytes MAY be omitted).constraints (optional) - operational constraints such as
maxUploadBytes, the largest single upload the backend accepts.restrictedActions - an array of actions (uppercase HTTP verbs, the
same vocabulary as the WAS Authorization Profile, see
5.1.5 Authorization Actions and the Root Capability) currently unavailable
on this backend. For example, a full backend reports ["POST", "PUT"]
while still permitting reads and deletes.measuredAt - when the usage numbers were measured. For external
("Bring Your Own Storage") backends, the server proxies the provider's
reported numbers, which may be cached or stale; measuredAt lets clients
judge freshness independently of the report's top-level respondedAt.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"
}
]
}
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 }
]
}
]
}
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):
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 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 client requests pagination with two OPTIONAL query parameters:
limit - the maximum number of items the client wants in the page, a
positive integer. A server applies its own default when limit is absent, and
MAY clamp an oversized limit down to a server maximum rather than rejecting
the request. A server MAY return fewer items than limit (including zero) and
still indicate more pages follow; clients MUST NOT treat a short page as the
end of the list (see next, below).cursor - an opaque token, obtained from a prior page's next value (see
below), naming the position to continue from. A client MUST treat the cursor as
opaque: it MUST NOT construct, parse, or modify it, and MUST NOT carry a cursor
from one collection, ordering, or server to another. A server encodes whatever
it needs into the cursor to resume the ordered scan (typically the sort-key
value of the last item already returned).The first page is requested with no cursor (a bare limit, or neither
parameter).
A paginated response carries the usual envelope, plus a next member when more
items may follow:
next - a URL the client dereferences (with the same authorization) to
retrieve the following page. The server bakes the appropriate cursor (and
any limit) into this URL, so the client follows it without constructing query
parameters itself. next is present if and only if more items may follow;
its absence marks the last page. This presence/absence is the authoritative
end-of-list signal.totalItems - when present, the total number of items in the entire
collection, not the number in the current page (the page count is simply the
length of items). Computing an exact total can be expensive at scale, so a
paginating server MAY omit totalItems. A client MUST NOT infer the number of
pages, or whether more pages exist, from totalItems; only next is
authoritative.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.
This appendix is normative.
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.
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.
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) |
This appendix is normative.
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 |
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.
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.
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"] }
]
}
}
}
This appendix is normative.
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 |
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
}
checkpoint (OPTIONAL) - the position to resume from, echoed from a prior
response (see below). When present it MUST be an object with a string id and a
string updatedAt; a malformed checkpoint is rejected with an
invalid-request-body (400) error. When absent, the feed starts at the
beginning.limit (OPTIONAL) - the requested batch size. A non-finite value, or a value
less than 1, falls back to a server default (a server default of 100 is
RECOMMENDED); a server MAY clamp limit down to an implementation maximum.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:
id - the Resource id._deleted - a boolean; true on a tombstone (a soft-deleted Resource), false
otherwise. The underscore-prefixed member name is deliberate: it matches the
wire convention of offline-first replication clients.updatedAt - the ISO-8601 timestamp of the change; the feed's ordering key.version - a monotonic content version. This is the same validator a server
surfaces as the ETag where conditional writes are supported (see
7.2 Conditional Requests); it is always present, and a content write bumps
it.metaVersion - a monotonic metadata version, present only once metadata has
been written for the Resource, and bumped by a metadata-only edit.createdBy - the did of the Resource's creator, as defined in
11.8 Resource Metadata Data Model. A server that records createdBy SHOULD
surface it here, so that a replica learns each Resource's creator from the feed
rather than dereferencing the meta sub-resource of every Resource it pulls.
Present only when recorded; a client MUST treat an absent createdBy as "not
recorded". It is server-managed and read-only, as it is everywhere else.data - the stored JSON body verbatim. The body is nested under data (rather
than spread onto the entry) so that a non-object JSON body round-trips
faithfully. Omitted on a tombstone.custom - the user-writable metadata object (see
11.8 Resource Metadata Data Model), present only when set. On an encrypted
Collection this is the opaque client-encrypted envelope, passed through
untouched.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" }
}
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>"
}
index (REQUIRED) - a non-empty string naming which blinded index (the HMAC
key id) to search. Matching is scoped to entries under this index.equals or has MUST be present; supplying neither, or both,
is an invalid-request-body (400) error.equals - a non-empty array of objects, each mapping a blinded attribute name
to a blinded attribute value (all strings). The array is a disjunction (OR
across its elements); the pairs within one element are a conjunction (AND). An
empty object element matches nothing.has - a non-empty array of blinded attribute name strings; matches every
document that carries every named attribute under the index, regardless of
value.count (OPTIONAL) - a boolean. When true, the response is exactly
{ "count": <number> } (the number of matching documents) rather than a
document page.limit (OPTIONAL) - the requested page size, handled leniently as for the
D.1 changes profile profile (a non-finite or < 1 value falls back to
a server default; a server MAY clamp to a maximum).cursor (OPTIONAL) - an opaque pagination token from a prior response (see
below). A malformed cursor is rejected with an invalid-cursor (400)
error.Response body (a document page):
{ "documents": [ { } ], "hasMore": false, "cursor": "<opaque token>" }
documents - the matching stored documents, verbatim (an encrypted envelope
passes through untouched), in ascending Resource id order.hasMore - a boolean, true when more matching documents follow the page.cursor - an opaque continuation token, present if and only if hasMore is
true. A client resumes by echoing it as the cursor of its next request. As
with pagination cursors elsewhere (see A. Pagination), a client MUST treat
the token as opaque and MUST NOT construct, parse, or modify it.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
}
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.
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.
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."
}
This section is non-normative.
This storage specification is intended to support the following goals and requirements.
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.
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.
A user needs to be able to write data (that is intended to be world-readable) to a cloud-accessible URL, and be able to send that URL to intended recipients via any out-of-band mechanism such as email, chat, and so on.
User needs to be able to change or revoke permissions at any point after sharing. Note that changed permissions apply only to operations that come after the change (this spec is not intended to solve the general problem of DRM).
The sharing and permission system needs to be primarily based on authorization capabilities (zCaps). It also needs support storage-side authorization policies (even if only as a way for an authorized client to receive an appropriate zcap)
The sharing mechanism needs to be flexible and granular. For example, a given data resource needs to be: world-readable, or readable by groups or categories, or by only those possessing the required authorization capabilities, or by no one except the author or controller, etc
Advanced sharing conditions are also desirable (such as "this share expires after X amount of time" or "this is a one-time share and will expire after the first successful read request")
Replication reconciles the first two requirements (data reads and writes must be offline-capable, but the data must eventually be able to be shared on the web via traditional URLs)
Replication also provides critical availability and disaster recovery functionality
Replication needs to be multi-primary (to reflect the multi-device and multi-client user environment)
Multi-primary replication requires support for a versioning or conflict resolution mechanism
Data, metadata, and permissions all need to be replicated
Authorship and data provenance (the ability to tell which user or service created or edited a given set of data) must work in this permissioned multi-primary-write environment
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.
Data written to storage spaces using this specification needs to be portable:
Authorized agents need to be able to export or backup all the data written, including all corresponding metadata and permissions
The sharing and storage system needs to be able to support web domain independent identifiers. That is, a user must be able to share data at a given URL, then be able to migrate to a different storage service provider (potentially operating on a different web domain than the previous one), and the shared permissions to that data must not break after service migration
While portability (and the not breaking of URLs) is relatively easy to achieve via redirect mechanisms (such as HTTP 301 and 302 redirect codes), this requires the previous service provider to be alive, available, and cooperative. However, this is true only of public-readable URLs, and the moment permissions are involved, cross-domain redirects become almost impossible to implement. In addition, portability from "dead servers" is also required. That is, if a cloud-based service provider disappears (or is otherwise unavailable), but a user still has a backup/export available, they should be able to set up another storage server (on another web domain or network address), and import/restore the data from backup, without shares and permissions breaking. Agents that the data was previously shared with must still be able to find the data at the new storage server location, and their permissions must still work.
Spec needs to support the storage of data in any format -- binary files and objects, structured documents such as JSON or CBOR, contents of relational database tables, graphs, and anything else, all using the same unified metadata, sharing, and permission mechanisms.
Storage-side schema enforcement is available but not required.
Spec needs to be able to support multiple protocols and APIs, such as HTTP, JSON-RPC, DIDComm, local client APIs, and more.
Where appropriate (such as for unstructured text, structured documents, RDBMSs etc), storage needs to be queryable or searchable
Any query/search mechanism needs to work well with the sharing/permission and replication requirements
All cryptography has a half-life.
Any cryptographic operations (such as hashing, signatures, and encryption) used in this specification must be able to be obsoleted or upgraded, as techniques and algorithms break. To put it another way, the spec cannot "hardcode" any given algorithm (although it can recommend current best practices)
Implementations of this spec need to be usable with FIPS-compliant cryptographic algorithms
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.
This section is non-normative.
This section will be submitted to the Internet Engineering Steering Group (IESG) for review, approval, and registration with IANA.
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in: