Skip to main content

eVault

eVault is the core storage system for W3DS. It provides a GraphQL API for storing and retrieving user data, manages access control, and delivers webhooks to platforms when data changes.

Overview​

An eVault is a personal data store identified by a W3ID. Each user, group, or object has their own eVault where all their data is stored in a standardized format called MetaEnvelopes.

Key Features​

  • GraphQL API: Store, retrieve, update, and search data
  • Access Control: ACL-based permissions for data access
  • Webhook Delivery: Automatic notifications to platforms when data changes
  • Key Binding: Stores user public keys (generated in the eID wallet) for signature verification

Architecture​

eVault Core consists of several components:

Data Model​

MetaEnvelopes​

A MetaEnvelope is the top-level container for an entity (post, user, message, etc.). It contains:

  • id: Unique identifier (W3ID). Note: Only IDs registered in the Registry are guaranteed to be globally unique.
  • ontology: Schema identifier (W3ID, e.g., "550e8400-e29b-41d4-a716-446655440001"). Schema W3IDs can be resolved to their schema definitions via the Ontology service. See W3DS Basics for more information on ontology schemas.
  • acl: Legacy access control list (who can access this data)
  • _acl: The granular access policy — grants, denials, and ontology conditions. Takes precedence over acl when present. See Access Control.
  • envelopes: Array of individual Envelope nodes

Envelopes​

Each field in a MetaEnvelope becomes a separate Envelope node in Neo4j:

  • id: Unique identifier
  • fieldKey: The field name from the payload (e.g., "content", "authorId", "createdAt") - this identifies which field in the payload this envelope represents
  • ontology: Alias for fieldKey (kept for backward compatibility)
  • value: The actual field value (string, number, object, array)
  • valueType: Type of the value ("string", "number", "object", "array")

Storage Structure​

In Neo4j, the structure looks like:

(MetaEnvelope {id, ontology, acl, aclBlock}) -[:LINKS_TO]-> (Envelope {id, value, valueType})

This graph always holds the current state. History is kept alongside it, never linked into it:

(MetaEnvelopeHistory {metaEnvelopeId, eName, latestVersion})
(MetaEnvelopeVersion {metaEnvelopeId, eName, version, operation, ontology, acl, aclBlock, payloadJson, fieldsJson, requestingPlatform, author, restoredFromVersion, createdAt})

A removed record is relabelled PrunedMetaEnvelope, and its Envelopes PrunedEnvelope, so no read returns it while its data is kept.

This flat graph structure allows:

  • Efficient field-level updates
  • Flexible querying
  • Easy reconstruction of the original object

Trade-offs:

  • Increased storage overhead (each field becomes a separate node)
  • More complex queries when reconstructing full objects
  • Potential performance impact with deeply nested structures

Binding Documents​

A Binding Document is a special type of MetaEnvelope that ties a user to their eName. It establishes identity verification or claims through cryptographic signatures. See Binding Documents for full details.

Key characteristics:

  • Stored as MetaEnvelope: Binding documents use the same MetaEnvelope structure, with ontology ID b1d0a8c3-4e5f-6789-0abc-def012345678
  • ID is MetaEnvelope ID: The binding document is identified by its MetaEnvelope ID (no separate ID field)
  • Always signed: Owner signature is required; counterparty signatures can be added
  • Types: id_document, photograph, social_connection, self

GraphQL API​

eVault exposes a GraphQL API at /graphql for all data operations. All operations require the X-ENAME header to identify the eVault owner.

Required Header for all operations:

X-ENAME: @user-a.w3id

Queries​

metaEnvelope​

Retrieve a single MetaEnvelope by its ID.

Query:

query {
metaEnvelope(id: "global-id-123") {
id
ontology
parsed
envelopes {
id
fieldKey
value
valueType
}
}
}

metaEnvelopes​

Retrieve MetaEnvelopes with cursor-based pagination and optional filtering.

Query:

query {
metaEnvelopes(
filter: {
ontologyId: "550e8400-e29b-41d4-a716-446655440001"
search: {
term: "hello"
caseSensitive: false
mode: CONTAINS
}
}
first: 10
after: "cursor-string"
) {
edges {
cursor
node {
id
ontology
parsed
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}

Filter Options:

  • ontologyId: Filter by ontology schema ID
  • search.term: Search term to match against envelope values
  • search.caseSensitive: Whether search is case-sensitive (default: false)
  • search.fields: Specific field names to search within (optional)
  • search.mode: CONTAINS, STARTS_WITH, or EXACT (default: CONTAINS)

Pagination:

  • first / after: Forward pagination
  • last / before: Backward pagination

metaEnvelopeHistory​

Retrieve every recorded version of a MetaEnvelope, newest first. Each create, update and remove appends an immutable version holding the full payload as it stood after that write; nothing is overwritten. History stays readable after removeMetaEnvelope, guarded by the access policy the record last carried.

Query:

query {
metaEnvelopeHistory(id: "global-id-123", first: 20) {
edges {
cursor
node {
version
operation
ontology
parsed
requestingPlatform
author
restoredFromVersion
createdAt
}
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}
  • operation is create, update or delete. A delete version has parsed: null.
  • requestingPlatform is the platform that made the write. author is the user it said it acted for, taken from X-ON-BEHALF-OF or from a wallet-signed token. It is the caller's assertion, recorded for history, not proof of identity.
  • restoredFromVersion is set on a version written by rollbackMetaEnvelope.
  • Records written before versioning existed get a baseline create version from their state at their first later write. Its platform and author are unknown.

Mutations​

createMetaEnvelope​

Create a new MetaEnvelope. Returns a structured payload with the created entity or errors.

Mutation:

mutation {
createMetaEnvelope(input: {
ontology: "550e8400-e29b-41d4-a716-446655440001"
payload: {
content: "Hello, world!"
mediaUrls: []
authorId: "..."
createdAt: "2025-01-24T10:00:00Z"
}
acl: ["*"]
}) {
metaEnvelope {
id
ontology
parsed
envelopes {
id
fieldKey
value
}
}
errors {
field
message
code
}
}
}

updateMetaEnvelope​

Update an existing MetaEnvelope. Returns a structured payload with the updated entity or errors.

Mutation:

mutation {
updateMetaEnvelope(
id: "global-id-123"
input: {
ontology: "550e8400-e29b-41d4-a716-446655440001"
payload: {
content: "Updated content"
mediaUrls: []
}
acl: ["*"]
}
) {
metaEnvelope {
id
ontology
parsed
}
errors {
message
code
}
}
}

rollbackMetaEnvelope​

Restore a MetaEnvelope to the state it had at an earlier version. Nothing is rewritten: the restore is written as a new version on top of the history, so the version number keeps increasing and the rollback can itself be rolled back.

  • The payload is replaced exactly. Unlike updateMetaEnvelope, which patches, fields the earlier version did not have are pruned.
  • The record keeps the access policy it has now, so restoring old data never restores old access.
  • Rolling back a removed record brings it back. Access to it is decided by the policy it last carried.
  • A version that records a removal cannot be restored. Use removeMetaEnvelope instead.
  • Requires UPDATE permission.

Mutation:

mutation {
rollbackMetaEnvelope(id: "global-id-123", version: 2) {
metaEnvelope { id parsed }
version
restoredFromVersion
errors { field message code }
}
}

Error codes: VERSION_NOT_FOUND, VERSION_IS_DELETE, INVALID_VERSION, ROLLBACK_FAILED.

removeMetaEnvelope​

Remove a MetaEnvelope. Removal prunes rather than destroys: the record and its Envelopes disappear from every read, but their data is kept and the record's version history remains available through metaEnvelopeHistory. Returns a structured payload confirming the removal.

Mutation:

mutation {
removeMetaEnvelope(id: "global-id-123") {
deletedId
success
errors {
message
code
}
}
}

bulkCreateMetaEnvelopes​

Create multiple MetaEnvelopes in a single operation. This is optimized for bulk data import and migration scenarios. Returns a structured payload with per-item results and aggregated success/error counts.

Mutation:

mutation {
bulkCreateMetaEnvelopes(
inputs: [
{
id: "custom-id-1" # Optional: preserve specific IDs during migration
ontology: "550e8400-e29b-41d4-a716-446655440001"
payload: {
content: "First item"
authorId: "..."
createdAt: "2025-02-04T10:00:00Z"
}
acl: ["*"]
}
{
# id omitted: will generate a new ID
ontology: "550e8400-e29b-41d4-a716-446655440001"
payload: {
content: "Second item"
authorId: "..."
createdAt: "2025-02-04T10:01:00Z"
}
acl: ["platform-a.w3id"]
}
]
skipWebhooks: false # Optional: set to true to skip webhook delivery
) {
results {
id # ID of the created envelope (or attempted ID if failed)
success # Whether this individual item succeeded
error # Error message if failed (null if succeeded)
}
successCount # Total number of successful creates
errorCount # Total number of failed creates
errors { # Global errors (usually empty)
message
code
}
}
}

Features:

  • Batch Creation: Create multiple MetaEnvelopes in a single request
  • ID Preservation: Optionally specify IDs for created envelopes (useful for migrations)
  • Partial Success: Returns individual results for each item, allowing some to succeed and others to fail
  • Webhook Control: skipWebhooks parameter can suppress webhook delivery (requires platform authorization)

Use Cases:

  • Data Migration: Import existing data from another system while preserving IDs
  • Bulk Import: Efficiently create many envelopes at once
  • Initial Setup: Populate an eVault with default or seed data

Authentication: This mutation requires a valid Bearer token in the Authorization header in addition to the X-ENAME header:

X-ENAME: @user-a.w3id
Authorization: Bearer <jwt-token>

Webhook Suppression: The skipWebhooks parameter only suppresses webhooks when:

  1. The parameter is set to true, AND
  2. The requesting platform is authorized for migrations (e.g., Emover)

For regular platform requests, webhooks are always delivered regardless of this parameter.

Binding Document Operations​

eVault provides dedicated GraphQL operations for managing Binding Documents — MetaEnvelopes that tie users to their eNames.

bindingDocument Query​

Retrieve a single binding document by its MetaEnvelope ID.

Query:

query {
bindingDocument(id: "meta-envelope-id") {
subject
type
data
signatures {
signer
signature
timestamp
}
}
}

bindingDocuments Query​

Retrieve binding documents with cursor-based pagination and optional filtering by type.

Query:

query {
bindingDocuments(
type: id_document
first: 10
after: "cursor-string"
) {
edges {
cursor
node {
subject
type
data
signatures {
signer
signature
timestamp
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}

Filter Options:

  • type: Filter by binding document type (id_document, photograph, social_connection, self)

Pagination:

  • first / after: Forward pagination
  • last / before: Backward pagination

createBindingDocument Mutation​

Create a new binding document. This stores a MetaEnvelope with ontology b1d0a8c3-4e5f-6789-0abc-def012345678.

Mutation:

mutation {
createBindingDocument(input: {
subject: "@e4d909c2-5d2f-4a7d-9473-b34b6c0f1a5a"
type: id_document
data: {
vendor: "onfido"
reference: "ref-12345"
name: "John Doe"
}
ownerSignature: {
signer: "@e4d909c2-5d2f-4a7d-9473-b34b6c0f1a5a"
signature: "sig_abc123..."
timestamp: "2025-01-24T10:00:00Z"
}
}) {
metaEnvelopeId
bindingDocument {
subject
type
data
signatures {
signer
signature
timestamp
}
}
errors {
message
code
}
}
}

Input fields:

  • subject: The eName being bound (will be normalized to include @ prefix)
  • type: One of id_document, photograph, social_connection, self
  • data: Type-specific payload (see Binding Documents)
  • ownerSignature: Required signature from the subject

createBindingDocumentSignature Mutation​

Add a signature to an existing binding document. Used for counterparty verification.

Mutation:

mutation {
createBindingDocumentSignature(input: {
bindingDocumentId: "meta-envelope-id"
signature: {
signer: "@counterparty-uuid"
signature: "sig_counterparty_xyz..."
timestamp: "2025-01-24T11:00:00Z"
}
}) {
bindingDocument {
subject
type
signatures {
signer
signature
timestamp
}
}
errors {
message
code
}
}
}

Input fields:

  • bindingDocumentId: The MetaEnvelope ID of the binding document
  • signature: The signature to add (signer, signature, timestamp)

Legacy API​

The following queries and mutations are preserved for backward compatibility but are considered legacy. New integrations should use the idiomatic API above.

Legacy Queries​

  • getMetaEnvelopeById(id: String!) - Use metaEnvelope(id: ID!) instead
  • findMetaEnvelopesByOntology(ontology: String!) - Use metaEnvelopes(filter: {ontologyId: ...}) instead
  • searchMetaEnvelopes(ontology: String!, term: String!) - Use metaEnvelopes(filter: {search: ...}) instead
  • getAllEnvelopes - Returns all envelopes (no pagination)

Legacy Mutations​

  • storeMetaEnvelope(input: MetaEnvelopeInput!) - Use createMetaEnvelope instead
  • updateMetaEnvelopeById(id: String!, input: MetaEnvelopeInput!) - Use updateMetaEnvelope instead
  • deleteMetaEnvelope(id: String!) - Use removeMetaEnvelope instead (returns Boolean!)
  • updateEnvelopeValue(envelopeId: String!, newValue: JSON!) - Update individual envelope value

HTTP API​

/whois​

Get information about a W3ID, including key binding certificates.

Request:

GET /whois HTTP/1.1
Host: evault.example.com
X-ENAME: @user-a.w3id

Response:

{
"w3id": "@user-a.w3id",
"evaultId": "@evault-identifier",
"keyBindingCertificates": [
"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...",
"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..."
]
}
  • w3id: The W3ID (eName) from the request.
  • evaultId: The eVault instance identifier (when configured via EVAULT_ID). Matches the evault value registered with the Registry.
  • keyBindingCertificates: JWTs binding the eName to public keys.

Example:

curl -X GET http://localhost:4000/whois \
-H "X-ENAME: @user-a.w3id"

Use Case: Platforms use this endpoint to retrieve public keys for signature verification and the eVault instance id for routing or auditing.

/logs​

Get paginated envelope operation logs for an eName. Each log entry describes a create, update, delete, or update_envelope_value operation (metaEnvelope id, hash, operation type, platform, author, timestamp). author is the user the writing platform said it acted for, or null..

Request:

GET /logs HTTP/1.1
Host: evault.example.com
X-ENAME: @user-a.w3id

Optional query parameters:

  • limit — Page size (default 20, max 100).
  • cursor — Opaque cursor for the next page (returned as nextCursor in the response).

Response:

{
"logs": [
{
"id": "log-entry-id",
"eName": "@user-a.w3id",
"metaEnvelopeId": "meta-envelope-id",
"envelopeHash": "sha256-hex",
"operation": "create",
"platform": "https://platform.example.com",
"author": "@user-a.w3id",
"timestamp": "2025-02-04T12:00:00.000Z",
"ontology": "550e8400-e29b-41d4-a716-446655440001"
}
],
"nextCursor": "2025-02-04T12:00:00.000Z|log-entry-id",
"hasMore": true
}

Example — first page:

curl -X GET "http://localhost:4000/logs?limit=20" \
-H "X-ENAME: @user-a.w3id"

Example — next page (using cursor):

curl -X GET "http://localhost:4000/logs?limit=20&cursor=2025-02-04T12:00:00.000Z%7Clog-entry-id" \
-H "X-ENAME: @user-a.w3id"

(Use the nextCursor value from the previous response; URL-encode the cursor if it contains special characters such as |.)

Access Control​

A MetaEnvelope can carry access rules two ways. The granular _acl policy is the current model; the legacy acl array predates it and still works.

The _acl policy​

_acl holds grants (an eName plus a READ/CREATE/UPDATE/DELETE bitmask), denials, and Resource Link Ontology conditions. Decisions run in a fixed order — denials, then the most specific grant, then the ontology groups — and the most specific grant wins without unioning less specific ones.

Full model, wire format, and current limits: Access Control.

It is stored on the MetaEnvelope node as the aclBlock property (JSON), so the policy travels with the record when it syncs. No migration is needed to start using it: the property is optional, and a node without one is read through its legacy array exactly as before.

Rolling back

Once records begin carrying policies, treat the deployment as forward-only. Earlier builds do not read aclBlock and fall back to the acl array — which platforms write as ["*"] — so a record an owner had locked down would become world-readable again on a rollback.

Legacy ACL format​

Arrays of W3IDs or special values:

  • ["*"]: Public read access (anyone can read, but only the eVault owner can write)
  • ["@user-a.w3id"]: Only User A can access (read and write)
  • ["@user-a.w3id", "@user-b.w3id"]: User A and User B can access (read and write)

The array is all-or-nothing: there is no read-only-without-write except ["*"]. That is what _acl replaces. A record with an _acl block ignores its array entirely; a record without one behaves exactly as it always has.

Access Enforcement​

The Access Guard middleware enforces access on every operation, with the permission the operation needs (read for queries, create/update/delete for the corresponding mutations):

  1. Extract W3ID: From X-ENAME header or Bearer token
  2. Check the policy: If the record carries _acl, decide by it. Otherwise fall back to the legacy array.
  3. Filter Results: Remove the legacy acl array from responses; _acl is returned as the policy in force
  4. Allow/Deny

A valid Registry-issued platform token satisfies the legacy path — but it does not bypass an _acl policy. A record carrying a policy is decided by that policy for every caller.

Special Cases​

  • storeMetaEnvelope: Only requires X-ENAME (no Bearer token needed)
  • Public Data: ACL ["*"] allows any authenticated request
  • Private Data: Only listed W3IDs can access

Webhook Delivery​

When data is created, updated, or deleted, eVault atomically records an awareness outbox event beside the mutation. A restart-safe dispatcher sends it to AaaS, which owns subscription matching and webhook delivery.

Webhook Process​

  1. Atomic capture: The MetaEnvelope mutation and immutable outbox event commit in one Neo4j transaction.
  2. Durable ingest: The outbox dispatcher retries POST AWARENESS_SERVICE_URL/ingest until AaaS acknowledges persistence.
  3. Match and filter: AaaS matches subscriptions and excludes the requesting platform.
  4. Send webhooks: AaaS posts to each matching endpoint (see Webhook Controller Guide).

Webhook Payload​

{
"eventId": "7fd6c06c-80ae-4137-9d62-c15af53f92cf",
"id": "global-id-123",
"w3id": "@user-a.w3id",
"schemaId": "550e8400-e29b-41d4-a716-446655440001",
"data": {
"content": "Hello, world!",
"mediaUrls": [],
"authorId": "...",
"createdAt": "2025-01-24T10:00:00Z"
},
"evaultPublicKey": "z...",
"operation": "update",
"streamVersion": 2,
"occurredAt": "2026-09-15T03:00:00.000Z"
}

Webhook Delivery Details​

  • Timeout: 5 seconds per network attempt.
  • Retry: eVault-to-AaaS retries until acknowledged; AaaS-to-subscriber retries with backoff for 24 hours and then dead-letters.
  • Idempotency: Subscribers must deduplicate by eventId because delivery is at least once.
  • Ordering: Events are ordered per subscription and MetaEnvelope while unrelated streams are delivered concurrently.

Key Binding Certificates​

eVault stores public keys for users and issues key binding certificates (JWTs) that bind public keys to W3IDs. These certificates serve two important purposes:

  1. Tamper Protection: Even if HTTPS is not used (though it should be), the JWT signature prevents tampering with public keys in transit. The Registry signs each certificate, ensuring the public key hasn't been modified.

  2. Registry Accountability: The Registry is accountable for the W3ID-to-public-key binding. By signing the certificates, the Registry attests to the binding between a W3ID and a public key, preventing spoofing of W3ID resolution.

Certificate Structure​

Key binding certificates are JWTs signed by the Registry:

{
"ename": "@user-a.w3id",
"publicKey": "zDnaerx9Cp5X2chPZ8n3wK7mN9pQrS7tUvWxYz",
"exp": 1737734400,
"iat": 1737730800
}

Certificate Lifecycle​

  1. Provisioning: When eVault is created, public key is stored and certificate is requested from Registry
  2. Storage: Certificates stored in eVault (retrieved via /whois)
  3. Expiration: Certificates expire after 1 hour
  4. Verification: Platforms verify certificates using Registry's JWKS

Multi-Tenancy​

The provisioning layer supports shared tenancy (multiple W3IDs can be provisioned on the same infrastructure). However, each eVault instance is dedicated to a single tenant (one W3ID per eVault):

  • W3ID Index: Database index on W3ID for fast queries
  • Isolation: All queries filtered by W3ID
  • No Cross-Tenant Access: Users can only access their own data (unless ACL allows)

API Examples​

Creating a Post​

curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-H "X-ENAME: @user-a.w3id" \
-d '{
"query": "mutation { createMetaEnvelope(input: { ontology: \"550e8400-e29b-41d4-a716-446655440001\", payload: { content: \"Hello!\", authorId: \"...\", createdAt: \"2025-01-24T10:00:00Z\" }, acl: [\"*\"] }) { metaEnvelope { id ontology } errors { message } } }"
}'

Querying Posts with Pagination​

curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-H "X-ENAME: @user-a.w3id" \
-H "Authorization: Bearer <token>" \
-d '{
"query": "{ metaEnvelopes(filter: { ontologyId: \"550e8400-e29b-41d4-a716-446655440001\" }, first: 10) { edges { node { id parsed } } pageInfo { hasNextPage endCursor } } }"
}'

Searching Posts​

curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-H "X-ENAME: @user-a.w3id" \
-d '{
"query": "{ metaEnvelopes(filter: { ontologyId: \"550e8400-e29b-41d4-a716-446655440001\", search: { term: \"hello\", mode: CONTAINS } }, first: 10) { edges { node { id parsed } } totalCount } }"
}'

Deleting a Post​

curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-H "X-ENAME: @user-a.w3id" \
-d '{
"query": "mutation { removeMetaEnvelope(id: \"global-id-123\") { deletedId success errors { message } } }"
}'

Getting Key Binding Certificates​

curl -X GET http://localhost:4000/whois \
-H "X-ENAME: @user-a.w3id"

References​