The W3 protocol governs user interactions within self-certified Public Key Infrastructure (PKI)-based namespaces. Access control to these namespaces, for simplicity referred to as spaces, is managed through delegated capabilities in UCAN format.
Users access their spaces across various user agents operating on multiple devices. Here we introduce an account primitive designed to enable synchronization of access across authorized user agents with a user-friendly experience.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC2119.
In the W3 protocol, a namespace, or space for short, corresponds to an asymmetric keypair and is identified by a did:key URI. The private key holder, is the owner of that namespace and has absolute authority over it. They can delegate limited or absolute authority over the namespace to any other principal. However, managing delegations across multiple user agents on multiple devices presents several user experience challenges:
- To synchronizing namespace access across user agents they need to discover their
did:keyidentifiers and level of access granted to them. - Recovering access in the case when all devices are lost becomes impossible.
To address these issues, we propose the concept of an account. An account SHOULD have a human-meaningful identifier such as email address. We propose use of email addresses as account identifiers so that derived did:mailto can act as the principal in the UCAN delegation chain. This creates principal that can be used to aggregate capabilities and manage them.
Account can be used to solve both discovery and recovery problems:
- Instead of user agents trying to discover each other in order to delegate capabilities, all capabilities get delegated to an account which is then used to re-delegate them as necessary to individual agents.
- Recovery is possible even if all devices have been lost as long as the user retains control of their email, because an account can always delegate capabilities to new agents.
Agent authorization can use familiar email-based authorization flows providing a smooth onboarding experience and hide complexity of the underlying PKI. This approach also better reflects user intuition: they have ambient authority over owned spaces and can authorize user agents (think apps) giving them necessary level of access.
ℹ️ This specification mainly focuses on
did:mailtoidentifiers, but implementations are free to extend it to various other DID methods.Also, note that account is defined as non
did:keyidentifier becausedid:keyidentifiers can aggregate and re-delegate capabilities natively with UCANs. Accounts simply bring human-meaningful identifiers that users can type in on every agent. Use of out of bound authorization also frees users from key management.
UCANs MUST be encoded with some IPLD codec. DAG-CBOR is RECOMMENDED.
There are several distinct roles that [principals] may assume in described specification:
| Name | Description |
|---|---|
| Principal | The general class of entities that interact with a UCAN. Listed in the iss or aud field |
| Account | Principal identified by memorable identifier like did:mailto. |
| Agent | Principal identified by did:key identifier, representing a user in some application installation |
| Issuer | Principal sharing access. It is the signer of the UCAN. Listed in the iss field |
| Audience | Principal access is shared with. Listed in the aud field |
Namespace, or space for short, is an owned resource that can be shared. It corresponds to the asymmetric keypair and is identified by the did:key URI.
A space DID is always listed in the with field of the UCAN capability.
The owner of the space is the holder of its private key. The space owner can share limited or full access to their owned space via a UCAN delegation issued by the space did:key and signed with the space's private key.
An account is a principal identified by a memorable identifier such as did:mailto. It is a principal that aggregates access to user spaces and that manages access of various user agents.
An account enables familiar authorization and recovery email flows.
An agent is a principal identified by a did:key identifier. Users interact with a system through different agents across multiple devices and applications. Agents SHOULD use non-extractable keys where possible.
ℹ️ Note that agents are meant to be ephemeral, implying that they could be disposed of or created on demand.
Authority is a trusted [DID] identifier. For example various subsystems may recognize signatures from a global service authority.
Various services run by different entities MAY also recognize each others authority and choose to trust their signatures as opposed to performing verification work.
Any principal CAN delegate capabilities to an account identified by a did:mailto according to the UCAN specification. It is RECOMMENDED to delegate full authority over created namespace to the user account at creation to offer access recovery mechanism.
sequenceDiagram
actor Alice
participant App as 💻<br/><br/>w3up #32;
participant Space as 📦<br/><br/>did:key:z7Gf…xSpace
participant Email as 📫 alice@web.mail
Note right of Alice: Authorization
App -->> Alice: What is your email ?
Alice -->> App: alice@web.mail
App->>Space:🔑 Create namespace
Space ->> Email: 🎫 Delegate capabilities
note over Space,Email:can:*<br/>with: did:key:z7Gf…xSpace
On first run, an application creates a new namespace and delegates full authority to the user account. For simplicity we omit steps where the application first requests access from an account before deciding to create a new space.
Account CAN authorize user agents by re-delegating a set of capabilities.
sequenceDiagram
participant Alice as 👩💻<br/><br/>did:key:z6Mkk…sxALi
participant Email as 📫 bob@gmail.com
participant Bob as 👨🏽💻 <br/><br/>did:key:z6Mkr…jnz2z
Alice ->> Email: 🎫 Delegate capabilities
note over Alice,Bob:<br/>with: did:key:z6Mkk…sxALi<br/>can:store/add
Email ->> Bob: 🎫 Delegate capabilities
Alice delegates the
store/addcapability to Bobs account. Later Bobs user agent gets re-delegated the capability from the account.
Delegations issued by a did:key principal are authorized by signing the payload with their private key. Delegations issued by a did:mailto principal are not authorized by signing over the payload as there is no private key associated with did:mailto to sign it with. Instead, such delegations are authorized through an interactive email flow in which the account holder is able to review and approve the requested authorization through an action.
Since there is no private key to sign the UCAN payload, we define an extension to the UCAN specification that introduces two new signature types that can be used in delegations issued by did:mailto principals.
We also define a verification mechanism for these signature types.
ℹ️ The signatures for accounts identified by other DID methods are not defined.
⚠️ w3up implementation currently does not support this signature type.
The did:mailto principal MAY issue a delegation signed using a DomainKeys Identified Mail (DKIM) signature.
This signature MUST be generated by sending a message from the account email address corresponding to the issuer did:mailto with the Subject header set to the authorization payload.
The signer MUST derive the "DKIM payload" from the received message according to the RFC6376 specification and encode it in UTF-8 encoding. Resulting bytes MUST be encoded as a Nonstandard VarSig signature with the alg parameter set to "DKIM".
The UCAN data model for the desired delegation issued by did:mailto MUST be structured per UCAN-IPLD Schema specification, except for the s field, which MUST be omitted.
The IPLD link of constructed data model MUST be derived and used the cid when formatting according payload according to the following ABNF definition.
auth := "I am signing ipfs://" cid "to grant access to this account"
cid := z[a-km-zA-HJ-NP-Z1-9]+Delegation MAY be authorized through an interactive email flow where the account holder is emailed a request to approve an authorization that gives an agent access to specific set of capabilities. If the user approves by clicking the embedded link, a signed attestation is issued that confirms that the delegation has been authorized through the interactive flow.
In this scenario a delegation issued by the did:mailto identifier MAY be signed using the attestation signature type. This signature alone MUST NOT be considered as a valid authorization. A delegation signed with an attestation signature MUST be accompanied with a UCAN attestation issued by the trusted authority.
If delegation is signed with an attestation signature, but is not accompanied with a UCAN attestation from a trusted authority it MUST be considered invalid. In this scenario the implementer MAY initiate an interactive verification flow and issue the UCAN attestation retroactively instead of denying service.
When the received delegation is issued by the
did:mailto:web.mail:alice, signed with attestation signature, but is not accompanied by a UCAN attestation, the receiver could iteratively confirm authorization by sending an email toalice@web.mailwith a confirmation link, which, when followed, issues an attestation from the receiver resuming the invocation.
The attestation signature is denoted by a Nonstandard VarSig signature with zero (0) signature bytes.
Attestation Signature in DAG-JSON format
{ "/": { "bytes": "gKADAA" } }Capability can only be delegated (not invoked) to allow an audience to derive any account/usage/* prefixed capability for the account identified in the with field.
type AccountUsage = {
can: 'account/usage/*'
with: AccountDID
}Capability can be invoked by an agent to retrieve usage data for all, or a specified set, of spaces within an account for a given period.
type AccountUsageGet struct {
with AccountDID
nb optional AccountUsageGetNB
}
type AccountUsageGetNB struct {
# Optional list of spaces to filter. If omitted, provider SHOULD aggregate all spaces the account is authorized to include.
spaces optional [SpaceDID]
# Optional time period (Unix timestamps in seconds).
# If omitted, provider MUST return a current snapshot.
# Semantics: from is inclusive; to is exclusive.
period optional Period
}
type Period struct {
from Int # inclusive
to Int # exclusive
}
Example: getting the current total usage
{
"iss": "did:key:z6MktfnQz8Kcz5nsC65oyXWFXhbbAZQavjg6LYuHOOOagent",
"aud": "did:web:storacha.network",
"att": [
{
"with": "did:mailto:web.mail:alice",
"can": "account/usage/get"
}
],
"prf": [
{ "/": "bafyAccountDelegationCid" },
{ "/": "bafySessionAttestationCid" }
],
"sig": "..."
}Example: filtering a single space and period
{
"iss": "did:key:z6MktfnQz8Kcz5nsC65oyXWFXhbbAZQavjg6LYuHOOOagent",
"aud": "did:web:storacha.network",
"att": [
{
"with": "did:mailto:web.mail:alice",
"can": "account/usage/get",
"nb": {
"spaces": [
"did:key:z6MkuxVKbEvYzXw89c9ESd3xoZ988MFrCgqT5JF5wtBvuYWe"
],
"period": {
"from": 1754006400,
"to": 1758111728
}
}
}
],
"prf": [
{ "/": "bafyAccountDelegationCid" },
{ "/": "bafySessionAttestationCid" }
],
"sig": "..."
}The service MUST verify that the Account DID is authorized to access usage data for all requested spaces. If any requested space is not authorized, the entire invocation SHOULD fail with an error message indicating which spaces lack authorization.
type AccountUsageGetReceipt struct {
ran Link<AccountUsageGet>
out Result<AccountUsageGetSuccess, AccountUsageGetFailure>
}
type AccountUsageGetFailure struct {
message String
}
type AccountUsageGetSuccess struct {
total Int
spaces {SpaceDID: SpaceUsage} # keys MUST be sorted
}
type SpaceUsage struct {
total Int
providers {ProviderDID: UsageData} # keys MUST be sorted
}
# UsageData is shared with `usage/report`
type UsageData struct {
provider ProviderDID
space SpaceDID
period PeriodISO
size SizeDelta
events [UsageEvent]
}
type SizeDelta struct {
initial Int
final Int
}
type UsageEvent struct {
cause Link
delta Int
receiptAt ISO8601Date
}
type PeriodISO struct {
from ISO8601Date
to ISO8601Date
}
type ISO8601Date string
type ProviderDID string
In all responses, the keys of the spaces field in AccountUsageGetSuccess and the providers field in SpaceUsage MUST be sorted lexicographically by their respective key (SpaceDID, ProviderDID). This ensures that the same query produces the same output each time.
{
"total": 5356848797,
"spaces": {
"did:key:z6MkuxVKbEvYzXw89c9ESd3xoZ988MFrCgqT5JF5wtBvuYWe": {
"total": 5356848797,
"providers": {
"did:web:web3.storage": {
"provider": "did:web:web3.storage",
"space": "did:key:z6MkuxVKbEvYzXw89c9ESd3xoZ988MFrCgqT5JF5wtBvuYWe",
"period": {
"from": "2025-07-01T00:00:00.000Z",
"to": "2025-08-12T15:55:11.000Z"
},
"size": {
"initial": 1035217049,
"final": 5356848797
},
"events": [
{
"cause": { "/": "bafyreiafouslzry3vunc4okazrqpwpuanrq4d3ehb2oatn4vmrhnmj6rzy" },
"delta": 54394,
"receiptAt": "2025-07-01T14:34:50.947Z"
}
]
}
}
}
}
}Capability can be invoked by an agent to retrieve egress data for all, or a specified set, of spaces within an account for a given period.
type AccountEgressGet struct {
with AccountDID
nb optional AccountEgressGetNB
}
type AccountEgressGetNB struct {
# Optional list of spaces to filter. If omitted, provider SHOULD aggregate all spaces the account is authorized to include.
spaces optional [SpaceDID]
# Optional time period
# If omitted, provider MUST return egress data from the first day of the last full month to the day the request is made.
period optional Period
}
# From and to are both expressed as ISO-8601 date-only strings (e.g. 2026-01-20).
# From is inclusive; to is exclusive.
type Period struct {
from ISO8601Date
to ISO8601Date
}
type ISO8601Date string
Example: getting the current total egress
{
"iss": "did:key:z6MktfnQz8Kcz5nsC65oyXWFXhbbAZQavjg6LYuHOOOagent",
"aud": "did:web:etracker.storacha.network",
"att": [
{
"with": "did:mailto:web.mail:alice",
"can": "account/egress/get"
}
],
"prf": [
{ "/": "bafyAccountDelegationCid" },
{ "/": "bafySessionAttestationCid" },
],
"sig": "..."
}Example: filtering a single space and period
{
"iss": "did:key:z6MktfnQz8Kcz5nsC65oyXWFXhbbAZQavjg6LYuHOOOagent",
"aud": "did:web:etracker.storacha.network",
"att": [
{
"with": "did:mailto:web.mail:alice",
"can": "account/egress/get",
"nb": {
"spaces": ["did:key:z6MkuxVKbEvYzXw89c9ESd3xoZ988MFrCgqT5JF5wtBvuYWe"],
"period": {
"from": "2025-07-01",
"to": "2025-07-03"
}
}
}
],
"prf": [{ "/": "bafyAccountDelegationCid" }, { "/": "bafySessionAttestationCid" }],
"sig": "..."
}The service MUST verify that the Account DID is authorized to access egress data for all requested spaces. If any of the requested spaces is not authorized, the entire invocation SHOULD fail with an error message indicating which spaces lack authorization.
type AccountEgressGetReceipt struct {
ran Link<AccountEgressGet>
out Result<AccountEgressGetSuccess, AccountEgressGetFailure>
}
type AccountEgressGetFailure struct {
name String
message String
}
type AccountEgressGetSuccess struct {
total Int # total egress for the account in the requested period. Unit: bytes.
spaces {SpaceDID: SpaceEgress} # breakout per-space. Keys MUST be sorted.
}
type SpaceEgress struct {
total Int # total egress for the space in the requested period. Unit: bytes.
dailyStats [DailyStat] # sorted by date ascending
}
type DailyStat struct {
date ISO8601Date
egress Int # egress for that date. Unit: bytes.
}
In all responses, the keys of the spaces field in AccountEgressGetSuccess MUST be sorted lexicographically. DailyStats within each SpaceEgress MUST be sorted by date ascending. This ensures that the same query produces the same output each time.
{
"total": 1111111110,
"spaces": {
"did:key:z6MkuxVKbEvYzXw89c9ESd3xoZ988MFrCgqT5JF5wtBvuYWe": {
"total": 1111111110,
"dailyStats": [
{
"date": "2025-07-01",
"egress": 123456789
},
{
"date": "2025-07-02",
"egress": 987654321
}
]
}
}
}