Skip to main content
This is the complete normative specification for the Vana Data Portability Protocol. It is the authoritative reference for implementers. For a guided introduction, see Personal servers and the other Protocol Reference pages. For focused topics, see the other Protocol Reference pages.

1. Introduction

The Data Portability Protocol enables users to:
  • Collect personal data from various platforms
  • Store data under their control
  • Grant third-party applications access to specific data scopes
  • Revoke access at any time
  • Maintain auditable records of all data access

Design principles

PrincipleDescription
User sovereigntyThe user controls their data and who can access it
Local-firstData is stored on the user’s device by default
Protocol-nativeGrants and data registry entries live onchain for verifiability
Encryption by defaultData is encrypted before upload; TLS in transit
ExtensibilityNew data sources and storage backends can be added without protocol changes

2. Terminology

Protocol entities

TermDefinition
UserA human who owns data and controls access. Identified by wallet address.
Personal ServerProtocol-recognized environment that stores user data and responds to access requests. Registered onchain.
BuilderThird-party application requesting data access. Registered onchain with public key and app URL.
Data Portability RPC (Gateway)Service providing fast API access with eventual chain consistency.
Data Portability ClientSoftware enabling protocol interaction (e.g. Data Connect desktop app). NOT a protocol participant.
PassportClient-chosen UX layer for wallet authentication. Non-protocol component.
Storage BackendService storing encrypted data blobs (Vana Storage, Google Drive, Dropbox, IPFS).

Protocol objects

TermDefinition
Data FileBlob containing user data for a specific scope. Immutable after write.
DataRegistry File RecordOnchain registry entry: fileId, URL, schemaId, permissions.
GrantSigned permission allowing a builder to access specific data scopes.
ScopeHierarchical identifier for a data type (e.g. instagram.profile).
Data ConnectorModule that extracts data from a specific platform. Not part of the protocol.
Schema RegistryOnchain registry (DataRefinerRegistry) mapping schemaId to schema definition.

Cryptographic primitives

PrimitiveSpecification
Grant signatureEIP-712 typed data signature proving user consent
Master key materialRaw signature bytes from EIP-191 personal_sign over "vana-master-key-v1"
Scope keyHKDF-SHA256(master_key_material, "vana", "scope:{scope}") — 32 bytes
Data encryptionOpenPGP password-based encryption; password = hex(scope_key) (64-char hex string)
Request authorizationEIP-191 signature over canonicalized JSON payload (Web3Signed scheme)

3. Protocol model

Architecture layers

┌─────────────────────────────────────────────────────────┐
│  Apps Layer                                             │
│  Builder apps (e.g. Flipboard), Vana Trace              │
└────────────────────────┬────────────────────────────────┘
                         │ grants / signed requests
┌────────────────────────▼────────────────────────────────┐
│  Protocol Layer                                         │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────────┐ │
│  │Personal Server│  │Storage Backend│  │DP RPC (Gateway)│ │
│  └──────────────┘  └──────────────┘  └───────────────┘ │
│                                       ┌───────────────┐ │
│                                       │   Vana L1     │ │
│                                       └───────────────┘ │
└────────────────────────┬────────────────────────────────┘
                         │ data collection
┌────────────────────────▼────────────────────────────────┐
│  Client Layer                                           │
│  Desktop App (Passport, Bundled Server, Connectors)     │
└─────────────────────────────────────────────────────────┘

Key distinction

  • Desktop App: NOT a protocol participant. Does NOT register onchain. Controls the Personal Server.
  • Personal Server: IS a protocol participant. MUST register onchain. Can act unattended.
Multiple Desktop Apps can control the same Personal Server.

4. Protocol components

4.1 Personal server

Purpose: Stores user data in plaintext, responds to authorized data requests, maintains access logs, operates unattended. Registration: MUST be registered onchain via DataPortabilityServers. Uses EIP-712 signature-based operations.

Hosting options

OptionURL formatOperatorData visibility
ODL Cloudhttps://server.vana.com/u/{userId}VanaEncrypted at rest
Self-hostedhttps://server.alice.comUserUser’s choice
Desktop-bundledLocal (tunneled via {userId}.server.vana.org)User’s deviceMay store unencrypted

Implementation targets

TargetRuntimeActivationAvailability
Desktop-bundledEmbedded in Tauri appUser opens appWhile app running
ODL CloudFirecracker MicroVM (Sprites.dev)HTTP request auto-activatesAlways (~1-2s cold start)
Self-hostedDocker containerAlways runningUser manages
The Personal Server does NOT require the user’s wallet private key — only the master-key signature for key derivation.

API

Data endpoints:
MethodPathDescription
POST/v1/data/{scope}Create data document
GET/v1/dataList scopes and metadata. Query: ?scopePrefix, ?limit, ?offset
GET/v1/data/{scope}Read data. Query: ?fileId, ?at (ISO 8601)
GET/v1/data/{scope}/versionsList versions (metadata only)
DELETE/v1/data/{scope}Delete data (user-only)
Grant endpoints:
MethodPathDescription
GET/v1/grantsList grants
POST/v1/grantsCreate grant. Body: { granteeAddress, scopes, expiresAt?, nonce? }
POST/v1/grants/verifyVerify grant signature
Other endpoints:
MethodPathDescription
GET/v1/access-logsAccess log history
GET/healthHealth check
Sync endpoints (internal):
MethodPathDescription
POST/v1/sync/triggerForce sync
GET/v1/sync/statusSync status
POST/v1/sync/file/{fileId}Sync specific file

Write flow (POST /v1/data/{scope})

  1. Look up schemaId for scope via Gateway
  2. Validate request body against schema
  3. Generate collectedAt timestamp (UTC)
  4. Construct data file envelope
  5. Store locally: ~/.vana/data/{scope}/{collectedAt}.json
  6. Return 201 Created
  7. Async: encrypt → upload to storage backend → register in DataRegistry

Authentication (builder requests)

All builder-initiated requests MUST include:
Authorization: Web3Signed <base64url(json)>.<signature>
Payload fields (keys sorted alphabetically):
FieldTypeRequiredDescription
audstringYesPersonal Server origin
bodyHashstringYesHash of request body (empty for GET)
expnumberYesExpiration (Unix seconds)
grantIdstringFor data readsOnchain permissionId
iatnumberYesIssued-at (Unix seconds)
methodstringYesHTTP method
uristringYesRequest path and query
Signing: JSON canonicalized (keys sorted) → UTF-8 → base64url (no padding). Signature is EIP-191 over ASCII bytes of base64url string. Verification: Personal Server recovers signer, verifies registered builder, validates aud/method/uri/timestamps, checks grant validity for data reads.

Access control

EndpointAccess
POST /v1/data/{scope}User only
GET /v1/data, GET /v1/data/{scope}, GET /v1/data/{scope}/versionsUser or builder with grant
DELETE /v1/data/{scope}User only
POST /v1/grants, GET /v1/grantsUser only
GET /v1/access-logsUser only

MCP server

Personal Server includes an MCP server for AI assistant integration. Resources: vana://files, vana://file/{scope}, vana://file/{scope}/metadata, vana://grants, vana://schemas, vana://schema/{schemaId} Tools: list_files, get_file, search_files — all require EIP-191 signature verification.

Tunneling

Desktop-bundled servers use Vana-managed FRP (Fast Reverse Proxy) for internet accessibility:
  • FRP server: proxy.server.vana.org
  • User URL: https://{userId}.server.vana.org
  • Wildcard DNS with Let’s Encrypt TLS
  • Tunnel starts on app open, terminates on close

Local data hierarchy

~/.vana/
├── data/{scope}/{YYYY-MM-DDTHH-mm-ssZ}.json    # Decrypted user data
├── logs/access-{YYYY-MM-DD}.log                  # Access logs (JSON lines)
├── index.db                                       # Local registry index (SQLite)
└── server.json                                    # Server configuration

4.2 Data portability RPC (Gateway)

Purpose: Provides fast API access to protocol operations with eventual chain consistency. Trust model:
ModeLatencyTrust
Gateway only~50msTrust Vana
Gateway + signature verification~50msTrust user signed
Gateway + spot-check chain~50ms + asyncTrust but verify
Chain only~5-10sTrustless

Gateway API

Server operations:
MethodPathDescription
POST/v1/serversRegister/update Personal Server
GET/v1/servers/{address}Get server info
GET/v1/servers/{address}/statusConfirmation status
Grant operations:
MethodPathDescription
POST/v1/grantsCreate grant
DELETE/v1/grants/{grantId}Revoke grant
GET/v1/grants/{grantId}Get grant details
GET/v1/grants?user={address}List user grants
GET/v1/grants?builder={address}List builder grants
GET/v1/grants/{grantId}/statusConfirmation status
File operations:
MethodPathDescription
POST/v1/filesRegister file record (schemaId required)
GET/v1/files/{fileId}Get file record
GET/v1/files?user={address}List user files
GET/v1/files?user={address}&since={ISO8601}Files since timestamp
GET/v1/files/{fileId}/statusConfirmation status
Schema operations:
MethodPathDescription
GET/v1/schemas/{schemaId}Get schema metadata
GET/v1/schemas?scope={scope}Look up schemaId by scope
Builder operations:
MethodPathDescription
POST/v1/buildersRegister builder
GET/v1/builders/{address}Get builder info
GET/v1/builders/{address}/statusConfirmation status
Utility:
MethodPathDescription
GET/v1/sync/statusChain sync status
GET/v1/nonces?user={address}&operation={op}Current and next nonce

Request format

POST /v1/files
Authorization: Signature 0xabc123...def
Content-Type: application/json

{
  "url": "https://storage.vana.com/alice/encrypted/instagram-profile.enc",
  "schemaId": 7,
  "nonce": 42
}

Response format

{
  "data": {
    "fileId": "0x...",
    "url": "https://storage.vana.com/...",
    "schemaId": 7
  },
  "proof": {
    "userSignature": "0x...",
    "gatewaySignature": "0x...",
    "timestamp": 1737500000,
    "status": "pending",
    "estimatedConfirmation": "30s",
    "chainBlockHeight": null
  }
}

ID computation

The Gateway assigns deterministically computed bytes32 IDs:
fileId    = keccak256(abi.encode(domainSeparator, ownerAddress, url, schemaId))
serverId  = keccak256(abi.encode(domainSeparator, serverAddress, publicKey, serverUrl))
builderId = keccak256(abi.encode(domainSeparator, owner, granteeAddress, publicKey))
grantId   = keccak256(abi.encode(domainSeparator, granteeId, grant, fileIds))

4.3 Vana L1 (onchain contracts)

DataPortabilityServers

Address: 0x1483B1F634DBA75AeaE60da7f01A679aabd5ee2c (Moksha Testnet) Manages Personal Server registration and trust relationships.
struct ServerInfo {
    uint256 id;
    address owner;
    address serverAddress;
    string publicKey;
    string url;
}

struct TrustedServerInfo {
    uint256 id;
    address owner;
    address serverAddress;
    string publicKey;
    string url;
    uint256 startBlock;
    uint256 endBlock;
}

function addServerWithSignature(AddServerWithSignatureInput input, bytes signature) external;
function addAndTrustServerWithSignature(AddServerWithSignatureInput input, bytes signature) external;
function addAndTrustServerByManager(address ownerAddress, AddServerInput input) external;
function updateServer(uint256 serverId, string memory url) external;
function trustServer(uint256 serverId) external;
function trustServerWithSignature(TrustServerInput input, bytes signature) external;
function trustServerByManager(address userAddress, uint256 serverId) external;
function untrustServer(uint256 serverId) external;
function untrustServerWithSignature(UntrustServerInput input, bytes signature) external;
function servers(uint256 serverId) external view returns (ServerInfo memory);
function serverByAddress(address serverAddress) external view returns (ServerInfo memory);
function userServerValues(address userAddress) external view returns (TrustedServerInfo[] memory);
function userServers(address userAddress, uint256 serverId) external view returns (TrustedServerInfo memory);
function userNonce(address user) external view returns (uint256);

DataPortabilityGrantees

Address: 0x8325C0A0948483EdA023A1A2Fd895e62C5131234 (Moksha Testnet) Manages builder registration.
struct GranteeInfo {
    address owner;
    address granteeAddress;
    string publicKey;
    string appUrl;
    uint256[] permissionIds;
}

function registerGrantee(address owner, address granteeAddress, string memory publicKey, string memory appUrl) external returns (uint256);
function grantees(uint256 granteeId) external view returns (GranteeInfo memory);
function granteeByAddress(address granteeAddress) external view returns (GranteeInfo memory);
function granteeAddressToId(address granteeAddress) external view returns (uint256);

DataPortabilityPermissions

Address: 0xD54523048AdD05b4d734aFaE7C68324Ebb7373eF (Moksha Testnet) Manages grant creation and revocation.
struct PermissionInfo {
    uint256 id;
    address grantor;
    uint256 nonce;
    uint256 granteeId;
    string grant;
    uint256 startBlock;
    uint256 endBlock;
    uint256[] fileIds;
}

function addPermission(PermissionInput calldata permission, bytes calldata signature) external returns (uint256);
function revokePermission(uint256 permissionId) external;
function revokePermissionWithSignature(RevokePermissionInput calldata input, bytes calldata signature) external;
function permissions(uint256 permissionId) external view returns (PermissionInfo memory);
function permissionFileIds(uint256 permissionId) external view returns (uint256[] memory);

DataRegistry

Address: 0x8C8788f98385F6ba1adD4234e551ABba0f82Cb7C (Moksha Testnet) Stores immutable file records.
struct FileResponse {
    uint256 id;
    address ownerAddress;
    string url;
    uint256 schemaId;
    uint256 addedAtBlock;
}

function addFileWithSchema(string memory url, uint256 schemaId) external returns (uint256);
function addFileWithPermissionsAndSchema(string memory url, address ownerAddress, Permission[] memory permissions, uint256 schemaId) external returns (uint256);
function files(uint256 index) external view returns (FileResponse memory);
function fileIdByUrl(string memory url) external view returns (uint256);
Clients MUST register files with schemaId. Omitting schemaId is invalid for protocol compliance.

4.4 Session relay

Standalone service coordinating the “Connect Data” flow between builder web popup and Desktop App.

Session states

pending → claimed → approved
                 → expired (15 minutes)
                 → denied

API

MethodPathAuthDescription
POST/v1/session/initWeb3SignedCreate session
GET/v1/session/{sessionId}/pollNonePoll for completion
POST/v1/session/claimSecretClaim session (Desktop App)
POST/v1/session/{sessionId}/approveSecretApprove with grant
POST/v1/session/{sessionId}/denySecretDeny request
Init request body:
{
  "granteeAddress": "0x...",
  "scopes": ["instagram.profile"],
  "webhookUrl": "https://api.example.com/vana/webhook",
  "app_user_id": "optional"
}
Init response:
{
  "sessionId": "...",
  "deepLinkUrl": "vana://connect?sessionId=xxx&secret=yyy",
  "expiresAt": "..."
}
Approved grant payload (via poll or webhook):
{
  "grantId": "0x...",
  "userAddress": "0x...",
  "builderAddress": "0x...",
  "scopes": ["instagram.profile", "instagram.likes"],
  "expiresAt": 0,
  "app_user_id": "optional"
}

5. Data formats

5.1 Scope taxonomy

{source}.{category}[.{subcategory}]
Source is the first segment. Schema definitions MUST encode the canonical scope.

5.2 Data file format

JSON envelope (v1):
{
  "$schema": "https://ipfs.io/<cid>",
  "version": "1.0",
  "scope": "instagram.profile",
  "collectedAt": "2026-01-21T10:00:00Z",
  "data": { ... }
}
The entire plaintext JSON is encrypted as a single OpenPGP blob before storage. No plaintext metadata alongside ciphertext.

5.3 Grant format (EIP-712)

{
  domain: {
    name: "Vana Data Portability",
    version: "1",
    chainId: 14800,
    verifyingContract: "0x...", // DataPortabilityPermissions
  },
  types: {
    Grant: [
      { name: "user", type: "address" },
      { name: "builder", type: "address" },
      { name: "scopes", type: "string[]" },
      { name: "expiresAt", type: "uint256" },
      { name: "nonce", type: "uint256" },
    ],
  },
  primaryType: "Grant",
  message: {
    user: "0x...",
    builder: "0x...",
    scopes: ["instagram.profile"],
    expiresAt: 0,
    nonce: 1,
  },
}

5.4 Access log format

{
  "logId": "uuid",
  "grantId": "0x...",
  "builder": "0x...",
  "action": "read",
  "scope": "instagram.profile",
  "timestamp": "2026-01-21T10:00:00Z",
  "ipAddress": "1.2.3.4",
  "userAgent": "BuilderSDK/1.0"
}

5.5 App manifest

W3C Web App Manifest with custom vana block. Discoverable via <link rel="manifest"> at the builder’s appUrl. Required vana fields: appUrl, privacyPolicyUrl, termsUrl, supportUrl, webhookUrl, signature. Signature: EIP-191 by builder address over canonical JSON of vana block (keys sorted, signature excluded). Verification: Desktop App MUST verify manifest origin, appUrl match, and signature recovery before rendering consent.

5.6 Data connector metadata

{
  "connectorId": "instagram",
  "displayName": "Instagram",
  "scopes": [
    { "scope": "instagram.profile", "label": "Your Instagram profile", "description": "..." }
  ],
  "version": "1.0"
}
Data Connectors are NOT part of the protocol — they are implementation details of specific clients.

6. Protocol operations

6.1 User registration

  1. User opens Desktop App
  2. Redirect to Passport (identity provider)
  3. Authenticate (social/email)
  4. Wallet created or retrieved
  5. Setup complete

6.2 Data collection

  1. User clicks “Connect ” in Desktop App
  2. Embedded browser opens; user logs in
  3. Data Connector scrapes data (user’s IP, user’s browser session)
  4. Raw data sent to Personal Server via POST /v1/data/{scope}
  5. Personal Server stores locally, then async: encrypt → upload → register
If no storage backend is selected, only local storage occurs.

6.3 Connect data flow

  1. User clicks “Connect data” on builder’s web app
  2. Builder backend creates session via Session Relay (signed)
  3. Frontend displays deep link popup
  4. User opens Desktop App via vana://connect?sessionId=...&secret=...
  5. Desktop App claims session, fetches builder metadata, verifies manifest
  6. User reviews and approves grant
  7. Personal Server signs EIP-712 grant, submits to Gateway
  8. Desktop App approves session with { grantId, userAddress, scopes }
  9. Builder receives grant via poll or webhook

6.4 Grant revocation

  1. User clicks “Revoke” in Desktop App
  2. Signs revocation
  3. Submit DELETE /v1/grants/{grantId} to Gateway
  4. Gateway marks revoked immediately; async chain sync
  5. Personal Server blocks future requests
  6. Builder receives 410 Grant revoked on next request

6.5 Data deletion

DataRegistry entries are immutable. Deletion is implemented as:
  1. User requests deletion in Desktop App
  2. Personal Server deletes encrypted blob from storage backend
  3. Local decrypted copy removed
  4. Tombstone written via Gateway
  5. Other Personal Servers treat tombstoned records as non-existent (410 or 404)

7. Security

7.1 Encryption

  • All user data MUST be encrypted with OpenPGP password-based encryption before writing to storage backends
  • Password is hex(scope_key) where scope key is derived per section 2
  • Personal Servers serve decrypted data to authorized builders over TLS
  • Vana MUST NOT have access to plaintext data

7.2 Authentication

  • Onchain operations MUST be signed using EIP-712 typed data
  • Builder requests to Personal Servers MUST include Authorization: Web3Signed (EIP-191)
  • Nonces MUST prevent replay attacks for onchain operations

7.3 Authorization

Personal Server MUST verify before serving data:
  • Signature valid
  • Grant not revoked
  • Grant not expired
  • Requested scope within granted scopes
  • Authorization signer matches onchain grantee

7.4 Transport

  • All HTTP endpoints MUST use TLS 1.3
  • Personal Servers SHOULD implement certificate pinning
  • Gateway SHOULD implement rate limiting

7.5 Threat model

ThreatMitigation
Vana sees user dataData encrypted before upload; Vana has no key
Builder exceeds granted scopePersonal Server validates scope on each request
Grant replayNonces and timestamps in grant signature
Malicious builderUser must explicitly approve; can revoke anytime
Gateway lies about grantsGrants include user signature; verifiable onchain
Personal Server compromisedStorage backend data remains encrypted

8. Error handling

Error code structure

Following SMTP convention, the protocol uses a 3-digit error code system:
First digitMeaning
2xxSuccess
3xxIntermediate (more input needed)
4xxTemporary failure (retry may succeed)
5xxPermanent failure (do not retry)
Second digitCategory
x0xSyntax/format
x1xAuthentication/authorization
x2xData/storage
x3xGrant/permission
x4xProtocol/network
x5xRate limiting

Error codes

CodeDescription
200Success
201Created
301Redirect to Personal Server
400Bad request (syntax error)
401Unauthorized (invalid signature)
403Forbidden (valid auth but not permitted)
404Not found
410Grant revoked
411Grant expired
412Scope not granted
420Data not found at registry entry
421Storage backend unavailable
429Rate limited
440Chain sync pending
500Internal server error
503Service unavailable

Error response format

{
  "error": {
    "code": 412,
    "message": "Scope not granted",
    "details": {
      "requestedScope": "instagram.messages",
      "grantedScopes": ["instagram.profile", "instagram.likes"]
    }
  }
}

9. Extensibility

Adding data sources

  1. Define scope taxonomy ({source}.{category})
  2. Create JSON Schema for data structure
  3. Register schema in DataRefinerRegistry
  4. Build a Data Connector (optional — not protocol)

Adding storage backends

Implement the StorageBackend interface:
interface StorageBackend {
  upload(key: string, data: Uint8Array): Promise<string>;
  download(url: string): Promise<Uint8Array>;
  delete(url: string): Promise<boolean>;
  exists(url: string): Promise<boolean>;
}

Adding capabilities

Personal Servers declare capabilities in registration:
capabilities: ["storage", "compute", "mcp"]
New capabilities can be defined without protocol changes.

10. Builder SDK

Published as @opendatalabs/connect on NPM with three entrypoints:
EntrypointPurpose
@opendatalabs/connect/serverSession Relay client, request signer, data client
@opendatalabs/connect/reactPolling hook, Connect button component
@opendatalabs/connect/coreShared types and errors

Server-side usage

import {
  createSessionRelay,
  createDataClient,
  createRequestSigner,
} from "@opendatalabs/connect/server";

// Session management
const relay = createSessionRelay({
  privateKey: process.env.VANA_APP_PRIVATE_KEY,
  granteeAddress: "0x...",
  sessionRelayUrl: "https://session-relay.vana.org",
});
const session = await relay.initSession({
  scopes: ["instagram.profile"],
});
const result = await relay.pollUntilComplete(session.sessionId);

// Data access
const data = createDataClient({
  privateKey: process.env.VANA_APP_PRIVATE_KEY,
  gatewayUrl: "https://gateway.vana.org",
});
const serverUrl = await data.resolveServerUrl(result.grant.userAddress);
const profile = await data.fetchData({
  serverUrl,
  scope: "instagram.profile",
  grantId: result.grant.grantId,
});

React client usage

import { useVanaConnect, ConnectButton } from "@opendatalabs/connect/react";

// Hook-based
const { connect, status, grant, deepLinkUrl, reset } = useVanaConnect({
  sessionRelayUrl: "https://session-relay.vana.org",
});

// Component-based
<ConnectButton
  sessionId={sessionId}
  sessionRelayUrl="https://session-relay.vana.org"
  onComplete={(grant) => { /* handle grant */ }}
/>

Appendix A: SMTP analogy

SMTP conceptData Portability equivalent
RFC 5321This specification
Mail User Agent (MUA)Data Portability Client
Mail Transfer Agent (MTA)Personal Server
Mail Delivery Agent (MDA)Storage Backend
SMTP ServerData Portability RPC (Gateway)
Email addressWallet address + Personal Server URL
Email messageData File
MailboxScope
SMTP EHLOServer registration
SMTP MAIL FROMGrant creation
SMTP RCPT TOBuilder address
SMTP DATAData file upload
SMTP QUITGrant revocation
Spam filterGrant approval
Bounce messageError response

Appendix B: ODL Cloud reference architecture

Sprites.dev integration

ODL Cloud uses Sprites.dev for per-user stateful MicroVMs (Firecracker):
  • Hardware-level isolation per user
  • Stateful storage persists between activations
  • HTTP auto-activation wakes sleeping VMs
  • Pay-per-use billing; scales to zero when idle
  • Up to 8 CPU, 16 GB RAM per Sprite

Cold start flow

  1. Builder calls GET https://user-abc.server.vana.com/data
  2. Sprites edge detects inactive Sprite → assigns compute (~300ms)
  3. Sprite boots with persisted filesystem
  4. Personal Server starts, decrypts data using delegated signature
  5. Request proxied to port 8080, response returned
  6. After idle timeout → Sprite sleeps (billing stops, data persists)
Total cold start latency: ~1-2 seconds.

Delegated signature lifecycle

EventAction
User enables ODL CloudSprite provisioned; user signs "vana-master-key-v1", signature encrypted and stored
Sprite activatesSignature decrypted in-memory; master key derived; data decrypted
User disables ODL CloudSprite deleted; data remains in storage backend
The delegated signature never expires. Revocation = disabling ODL Cloud.

Cost estimates

Usage patternMonthly cost per user
Light (few requests/month)~$0.10-0.50
Medium (daily builder access)~$0.50-2.00
Heavy (continuous access)~$2.00-10.00