- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
- [CORE] Vana Data Portability Protocol Specification
The Data Portability Protocol (DP) enables users to:
- Collect their personal data from various platforms
- Store that data under their control
- Grant third-party applications access to specific data scopes
- Revoke access at any time
- Maintain an auditable record of all data access
- User Sovereignty — User controls their data and who accesses it
- Local-First — Data stored on user's device by default
- Protocol-Native — Grants and data registry entries on-chain for verifiability
- Encryption by Default — Data encrypted before upload to the storage backend; TLS in transit
- Extensibility — New data sources and storage backends can be added
This specification covers:
- Protocol entities and their roles
- Data formats and schemas
- Grant operations (create, revoke, verify)
- Storage operations (write, read, delete)
- Identity and authentication
- Error handling
This specification does NOT cover:
- Data connector implementations (platform-specific scrapers)
- Application-specific features (search, vector DB, AI integration)
- User interface design
User : A human who owns data and controls access to it. Identified by a cryptographic wallet address.
Data Portability Client : Software that enables users to interact with the protocol. Analogous to an email client. The Vana Desktop App is the reference implementation. NOT a protocol participant (not registered on-chain). It may bundle a Personal Server; in that case the Personal Server (not the client) is the protocol participant and must be registered on-chain.
Personal Server : A protocol-recognized environment integrated with user storage which responds to access requests and offers to certain compute operations on user data. Registered on-chain.
Builder : A third-party application that requests access to user data. Registered on-chain (via Desktop App + Gateway relayer) with a public key and an app URL used to resolve app metadata.
Data Portability RPC (Gateway) : A service that provides fast API access to protocol operations with eventual chain consistency. May be operated by Vana or federated providers.
Passport (Client-side) : A client-chosen UX layer that authenticates users and manages wallets on their behalf, abstracting wallet concepts from web2 users. Passport is NOT a protocol component; from the protocol POV, only wallet addresses matter. The Desktop App uses Privy for Passport today, but other clients may use different vendors.
Storage Backend : A service that stores encrypted data blobs. Receives only pre-encrypted data from Personal Servers; never sees plaintext. Can be Hosted Storage (Vana Storage), IPFS, user's cloud (Google Drive, Dropbox), or local storage. Storage backends verify requester authorization before accepting operations.
Data File : An blob containing user data for a specific scope. File contents are immutable after write; on-chain fileId linkage is tracked in the Personal Server's local index.
DataRegistry File Record : An on-chain registry entry (fileId, URL, schemaId, permissions) that points to an encrypted Data File in a storage backend. schemaId is REQUIRED for all file records. fileId is deterministic and can be pre-computed before the file is written to the Gateway or onchain.
Grant : A signed permission allowing a Builder to access specific data scopes.
Scope : A hierarchical identifier for a type of data (e.g., instagram.profile, chatgpt.conversations). Scope is always the full path; source is derived from the first segment.
Data Connector : A module that extracts data from a specific platform. Implementation-specific, not part of the protocol.
Schema Registry (DataRefinerRegistry) : On-chain registry mapping schemaId → schema definition (IPFS CID/URL) used to resolve canonical scope prior to decryption.
Grant Signature : An EIP-712 typed data signature proving user consent.
Key Derivation :
- Master key material = raw signature bytes produced by EIP-191
personal_signover the fixed message"vana-master-key-v1". - Scope key =
HKDF-SHA256(master_key_material, "vana", "scope:{scope}")(32 bytes output).
Data Encryption :
- Encryption scheme = OpenPGP password-based encryption (
openpgplibrary, symmetric mode). - Encryption password =
hex(scope_key)— the 32-byte scope key hex-encoded as a 64-character string, used as the OpenPGP password. - Produces standard OpenPGP binary messages; compatible with vana-sdk encryption format.
┌────────────────────────────────────────────────────────────────────────────┐
│ APPS LAYER │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Builder App 1 │ │ Builder App 2 │ │ Vana Trace │ │
│ │ (e.g. Flipboard)│ │ │ │ (First-party) │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │Connect data │ │ │
└───────────┼────────────────────┼────────────────────┼──────────────────────┘
│ │ │
▼ ▼ ▼
┌────────────────────────────────────────────────────────────────────────────┐
│ PROTOCOL LAYER │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Personal Server (Protocol Participant) │ │
│ │ - Arbitrary compute over user data │ │
│ │ - Syncs data to storage backends │ │
│ │ - Serves builders + enforces grants │ │
│ │ - Hosted (ODL Cloud) or self-hosted │ │
│ │ │ │
│ │ Storage Adapter: ODL Hosted Storage, IPFS, Google Drive, Dropbox │ │
│ │ Compute: LLMs/agents, data transformations │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Storage Backends │ │
│ │ - Hosted Storage (default) │ │
│ │ - IPFS │ │
│ │ - Google Drive │ │
│ │ - Dropbox │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Data Portability RPC (Gateway) │ │
│ │ - Fast reads/writes (cached) │ │
│ │ - Async chain sync │ │
│ │ - May be federated │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Vana L1 (Source of Truth) │ │
│ │ - File Registry (DataRegistry) │ │
│ │ - Permissions/Grants (DataPortabilityPermissions) │ │
│ │ - Builder Registry (DataPortabilityGrantees) │ │
│ │ - Personal Server Registry (DataPortabilityServers) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────────┘
▲
│
┌───────────┴────────────────────────────────────────────────────────────────┐
│ DATA PORTABILITY CLIENT │
│ (Desktop App - NOT a protocol participant; │
│ may bundle a Personal Server) │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Passport (Privy or other) │ │
│ │ - Manages user wallet + maps to on-chain address │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Bundled Personal Server (protocol participant; same interface) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Data Connectors (NOT part of protocol) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │Instagram │ │ ChatGPT │ │ YouTube │ │ Gmail │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Local Storage (Unencrypted, user's device only) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
Why this matters: The Desktop App configures and controls the Personal Server, but the Personal Server is what participates in the protocol. This remains true even when the Personal Server is bundled inside the Desktop App (the server is registered on-chain; the app is not). Multiple Desktop Apps (or mobile apps, or CLI tools) can control the same Personal Server.
A protocol-recognized environment that:
- Stores user data in plain text, ready to process
- Responds to authorized data requests from Builders
- Maintains access logs
- Can operate unattended (without user actively present)
Personal Server MUST be registered on-chain via DataPortabilityServers (see Section 4.3.1). Registration and trust relationships use EIP-712 signature-based operations (e.g., addAndTrustServerWithSignature).
| Option | URL Format | Operator | Data Visibility |
|---|---|---|---|
| ODL-Hosted | https://server.vana.com/u/{userId} |
ODL | Unencrypted OK |
| Self-Hosted | https://server.alice.com |
User | Unencrypted OK (user's security zone) |
| Desktop-Bundled | Local server within Desktop App | User's device | Unencrypted OK (user's security zone) |
| Desktop-as-Server (tunneled) | https://{userId}.server.vana.org |
User's device | Unencrypted OK (user's security zone) |
The protocol is NOT opinionated about whether the Personal Server stores data encrypted or unencrypted at rest.
Protocol Requirements (ALL implementations MUST):
- Encrypt data in transit (TLS 1.3)
- Verify grant validity before serving data
Desktop-Bundled Personal Server:
The Desktop App MAY bundle a Personal Server implementation that:
- Stores data unencrypted locally (within user's security zone)
- Runs computations on unencrypted data
- Is only available when the Desktop App is running
The bundled Personal Server is still a Personal Server and MUST be registered on-chain; the Desktop App submits the registration on the user's behalf.
┌─────────────────────────────────────────────────────────────────┐
│ USER'S DEVICE (Security Zone) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Desktop App │ │
│ │ ├── Data Connectors (scraping) │ │
│ │ └── Bundled Personal Server │ │
│ │ ├── Local Storage (unencrypted OK) │ │
│ │ ├── Serves decrypted data to authorized builders │ │
│ │ ├── Runs local computations │ │
│ │ └── Serves builders (when app is open) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ Data never leaves user's security zone in plaintext │
└─────────────────────────────────────────────────────────────────┘
Users MAY configure both:
- Desktop-Bundled for local compute and when app is open
- ODL-Hosted as fallback when app is closed
POST /v1/data/{scope}
Create a new data document for the specified scope
Headers:
Content-Type: application/json (only JSON documents supported for now)
Body:
Raw JSON data payload (the "data" field content only)
GET /v1/data
List available scopes and latest version metadata
Query params:
?scopePrefix={scopePrefix} # optional filter by scope prefix
?limit={limit} # optional pagination
?offset={offset} # optional pagination
GET /v1/data/{scope}
Read data file (user-initiated; or builder-initiated with grantId + Web3Signed Authorization)
Query params:
?fileId={fileId} # specific version by DataRegistry fileId
?at={ISO8601} # specific version by collectedAt (closest <= at)
(default: latest by collectedAt)
GET /v1/data/{scope}/versions
List available versions (metadata only)
Query params:
?limit={limit} # optional pagination
?offset={offset} # optional pagination
DELETE /v1/data/{scope}
Delete data file
GET /v1/grants
List all grants for this user
POST /v1/grants
Create a new grant (owner-only, called by Desktop App)
Body: { granteeAddress, scopes, expiresAt?, nonce? }
Response: { grantId } (201 Created)
POST /v1/grants/verify
Verify a grant signature
GET /v1/access-logs
Get access log history
GET /health
Health check (unversioned)
POST /data/{scope} (plaintext ingest):
- Personal Server MUST:
-
Look up the
schemaIdfor the given scope via Gateway (GET /v1/schemas?scope={scope}). -
Reject with
400 Bad Requestif no schema is registered for the scope. -
Validate the request body against the schema definition.
-
Reject with
400 Bad Requestif validation fails. -
Generate
collectedAttimestamp (current UTC time). -
Construct the full Data File envelope:
{ "$schema": "<schema URL from registry>", "version": "1.0", "scope": "{scope}", "collectedAt": "<generated timestamp>", "data": <request body> } -
Store locally in
~/.vana/data/{scope}/{collectedAt}.json. -
Return
201 Createdimmediately with the response below. -
Async (background): encrypt the Data File, upload to the configured storage backend (if any), and register the file record in
DataRegistryvia DP RPC.
-
POST /data/{scope} Response:
{
"scope": "instagram.profile",
"collectedAt": "2026-01-21T10:00:00Z",
"status": "syncing"
}Authentication Model (Builder Data Requests)
- Builder data requests MUST include
Authorization: Web3Signed ...(see 4.1.9 and 4.1.9.1). - The Web3Signed payload MUST include
grantId(permissionId) when raw data is requested. - Personal Server recovers the signer address from the
Authorizationheader and verifies it matches the on-chain grantee for the providedgrantId.
Authentication Model (Builder Non-Data Requests)
For builder calls that do NOT return raw data (e.g., /data, /data/{scope}/versions), the Personal Server MUST require a signed request but MUST NOT require a grant.
- Builder MUST include
Authorization: Web3Signed ...(see 4.1.9.1). - Personal Server verifies the recovered signer is a registered Builder on-chain.
- Personal Server enforces
iat/exptime bounds (and MAY cache signatures to reduce replay).
Access Control for Data Endpoints
/grantsendpoints are user-initiated actions (Desktop App or Personal Server UI) and MUST NOT be callable by Builders.GET /data/{scope}MAY be user-initiated OR builder-initiated when a valid grant is provided (see 4.1.9).DELETE /data/{scope}is a user-only action for removing local/decrypted data and triggering storage cleanup in the storage backend.GET /dataandGET /data/{scope}/versionsMAY be builder-initiated but requireAuthorization: Web3Signed ...per 4.1.9.1.
The Personal Server is designed to be portable across multiple deployment targets. All implementations MUST expose the same API surface and enforce grants identically.
| Target | Runtime | Activation | Availability | Key Derivation |
|---|---|---|---|---|
| Desktop-Bundled | Embedded in Tauri app | User opens app | While app is running | User signs on app open |
| ODL Cloud | Firecracker MicroVM (Sprites.dev) | HTTP request auto-activates | Always (cold start ~1s) | Delegated signature (never expires) |
| Self-Hosted | Docker container | Always running | User manages | User's choice |
Note: The Personal Server does NOT require the user's wallet private key. It only needs the master-key signature (the signature over "vana-master-key-v1") to derive keys. Implementations MAY supply this at startup via an env var such as VANA_MASTER_KEY_SIGNATURE, or store it encrypted in Sprite storage for ODL Cloud.
ODL Cloud (Sprites.dev) Details:
ODL Cloud uses Sprites.dev to provide stateful, per-user MicroVMs:
┌─────────────────────────────────────────────────────────────────────────────┐
│ ODL CLOUD ARCHITECTURE │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Sprites Orchestrator (managed by Vana) ││
│ │ - Provisions one Sprite per user on-demand ││
│ │ - Auto-scales to 0 when inactive (stops billing) ││
│ │ - Persists data to durable storage between activations ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Per-User Sprite (Firecracker MicroVM) ││
│ │ ┌─────────────────────────────────────────────────────────────────────┐││
│ │ │ Personal Server Process │││
│ │ │ - Decrypts data on activation (delegated signature stored) │││
│ │ │ - Stores decrypted data in ~/.vana/data/{scope}/ │││
│ │ │ - Serves builder data requests │││
│ │ │ - Serves builder requests via HTTP (port 8080) │││
│ │ │ - Serves decrypted data to authorized builders │││
│ │ └─────────────────────────────────────────────────────────────────────┘││
│ │ ││
│ │ Resources: Up to 8 CPUs, 16GB RAM, 100GB+ storage ││
│ │ URL: https://{user-id}.server.vana.com → proxied to Sprite port 8080 ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ COLD START BEHAVIOR: │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ 1. Builder calls: GET https://user-abc.server.vana.com/data ││
│ │ 2. Sprites edge sees Sprite inactive → 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 (for Unattended Access):
When a user enables ODL Cloud, they sign a message that is stored encrypted in the Sprite's persistent storage. This signature NEVER expires and is used to derive the master key on each activation:
┌─────────────────────────────────────────────────────────────────────────────┐
│ DELEGATED SIGNATURE FLOW │
│ │
│ SETUP (one-time, in Desktop App): │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ 1. User clicks "Enable ODL Cloud" in Desktop App ││
│ │ 2. Desktop App prompts: "Sign to authorize your cloud server" ││
│ │ 3. User signs message: "vana-master-key-v1" via EIP-191 personal_sign ││
│ │ 4. Desktop App provisions Sprite via ODL Cloud API ││
│ │ 5. Signature encrypted with Sprite-specific key, stored in Sprite ││
│ │ 6. Sprite URL registered in DataPortabilityServers on-chain ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ ACTIVATION (every cold start): │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ 1. HTTP request activates Sprite ││
│ │ 2. Personal Server reads encrypted signature from persistent storage ││
│ │ 3. Decrypts signature (Sprite has decryption key) ││
│ │ 4. Derives master key: HKDF(signature, "vana", "master") ││
│ │ 5. Derives scope keys, decrypts user data files ││
│ │ 6. Server ready to handle requests ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ SECURITY PROPERTIES: │
│ • Signature never expires (user authorizes once, stored forever) │
│ • Signature encrypted at rest in Sprite storage │
│ • Master key material derived in-memory, never persisted │
│ • User can revoke by deleting Sprite (via Desktop App) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Note: The delegated signature is sufficient for key derivation; the Personal Server never loads or uses a user wallet private key.
Sprite Provisioning (On-Demand):
Sprites are NOT provisioned for all users automatically. They are provisioned only when a user explicitly enables "unattended access" or "ODL Cloud":
| Trigger | Action |
|---|---|
| User creates account | NO Sprite provisioned (Desktop-only) |
| User enables "ODL Cloud" | Sprite provisioned, signature stored |
| User disables "ODL Cloud" | Sprite deleted, data remains in storage backend |
The Personal Server includes a Data Sync component that manages the flow of data between the storage backend and local storage. The storage backend is the source of truth for encrypted data; Personal Servers maintain local decrypted copies for compute and serving. Decryption requires the canonical scope, which is resolved from schemaId via the Gateway schema lookup.
DP RPC deployment constraint: The DP RPC runs on Vercel Serverless and does not support streaming. Personal Servers MUST poll for new file records using a lastProcessedTimestamp cursor (not block-based backfill).
The lastProcessedTimestamp is cached locally by the Personal Server and used when querying DP RPC; if the timestamp is not provided, DP RPC returns all file records for the user.
Storage Backend Selection:
Users select ONE storage backend for all their data (per-user, not per-file). Selection happens during Desktop App sign-in and is persisted in ~/.vana/server.json. Until a storage backend is selected, the Personal Server operates in local-only mode and does NOT write to DataRegistry.
When a storage backend is selected or changed, the Personal Server bulk-uploads existing local data to the new backend and registers corresponding DataRegistry file records on-chain (triggered/configured by the Desktop App).
| Backend | URL Format | Notes |
|---|---|---|
| Vana Storage (default) | https://storage.vana.com/v1/blobs/{owner}/{scope}/{collectedAt} |
Managed by Vana, zero-config |
| Google Drive | gdrive://{fileId} |
User authorizes via OAuth |
| Dropbox | dropbox://{path} |
User authorizes via OAuth |
| IPFS | ipfs://{cid} |
Content-addressed, immutable |
Storage backends in the Data Portability Protocol MUST:
- Accept only encrypted blobs — Storage backends receive pre-encrypted data from Personal Servers. Encryption/decryption is the Personal Server's responsibility.
- Authenticate requests — Verify the requester is authorized to operate on the owner's data. Authorization model is backend-specific but MUST tie operations to the data owner's identity.
- Support hierarchical keys — Vana Storage uses keys in the format
{ownerAddress}/{scope}/{collectedAt}. Other backends map this to their native structure (e.g., GDrive folders, IPFS CIDs). - Return canonical URLs — After upload, return a stable URL that the Personal Server registers with the DataRegistry via Gateway.
Storage backends are NOT responsible for:
- Decrypting or processing data
- Schema validation (done by Personal Server before upload)
- File discovery (handled by Gateway's DataRegistry)
Data Flow (Storage-First Model):
After a storage backend is selected, all new data goes to the storage backend FIRST, then syncs to all Personal Server instances:
┌─────────────────────────────────────────────────────────────────────────────┐
│ DATA SYNC FLOW │
│ │
│ NEW DATA (from Desktop connector): │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ 1. Desktop App collects data via connector (Instagram, etc.) ││
│ │ 2. Desktop App sends raw data to Personal Server ││
│ │ 3. Personal Server stores locally unencrypted: ~/.vana/data/{scope}/ ││
│ │ 4. Personal Server encrypts data with scope key ││
│ │ 5. Encrypted blob uploaded to storage backend (Vana Storage/GDrive/Dropbox/IPFS) ││
│ │ 6. File record registered in DataRegistry via DP RPC (schemaId required)││
│ │ 7. DP RPC records file metadata (schemaId attached) ││
│ │ 8. Other Personal Servers poll DP RPC for new file records since lastProcessedTimestamp ││
│ │ 9. Other Personal Servers download → resolve schemaId → decrypt → store ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ PERSONAL SERVER SETUP (first activation): │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ 1. Personal Server activates (Desktop open OR Sprite cold start) ││
│ │ 2. Query DP RPC for all file records for this user ││
│ │ 3. Query DP RPC for file records since lastProcessedTimestamp ││
│ │ 4. For each file record: ││
│ │ a) Download encrypted blob from storage backend ││
│ │ b) Resolve schemaId → canonical scope (via Gateway /v1/schemas) ││
│ │ c) Decrypt with scope key (derived from master key material) ││
│ │ d) Read scope/collectedAt from payload ││
│ │ e) Store in ~/.vana/data/{scope}/{collectedAt}.json ││
│ │ f) Update local index (fileId → path, scope, collectedAt) ││
│ │ 5. Mark sync complete, server ready to serve requests ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ MULTI-INSTANCE SYNC (Desktop + ODL Cloud): │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Source of Truth: Storage Backend + Data Registry (via DP RPC) ││
│ │ ││
│ │ • Each Personal Server polls DP RPC for new file records ││
│ │ • On new fileId: download → resolve schemaId → decrypt → index → write ││
│ │ • On restart: resume from lastProcessedTimestamp ││
│ │ • Conflict resolution: last-write-wins (collectedAt in payload) ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Before a storage backend is selected, steps 4–9 are skipped: data remains local-only and no DataRegistry entries are created.
When a file record is registered on-chain, the Data Sync component records the fileId in the local index (mapping to the immutable local file). The data file itself is never mutated post-registration.
Local Sync Index (Personal Server):
Personal Servers maintain a local index to avoid duplicate downloads and to support fast hierarchy lookups. Minimum fields:
fileId → pathmappingscope,collectedAtlastProcessedTimestampfor polling cursor
Sync API (Personal Server exposes internally):
POST /v1/sync/trigger
Force sync from storage backend
GET /v1/sync/status
Get sync status (last sync, lastProcessedTimestamp, pending files, errors)
POST /v1/sync/file/{fileId}
Sync specific file from storage backend
Personal Servers store decrypted data in a standardized directory structure. This hierarchy is consistent across all implementation targets.
Directory paths are derived from scope segments (dot-separated). For example, instagram.profile maps to ~/.vana/data/instagram/profile/.
~/.vana/ # Root directory (all Personal Server targets)
├── data/ # User data files (decrypted)
│ ├── instagram/
│ │ ├── profile/
│ │ │ ├── 2026-01-21T10-00-00Z.json # versioned snapshot (UTC, filename-safe)
│ │ │ └── 2026-01-22T10-00-00Z.json
│ │ ├── posts/
│ │ │ ├── 2026-01-21T10-00-00Z.json
│ │ │ └── 2026-01-22T10-00-00Z.json
│ │ ├── likes/
│ │ │ ├── 2026-01-21T10-00-00Z.json
│ │ │ └── 2026-01-22T10-00-00Z.json
│ │ └── followers/
│ │ ├── 2026-01-21T10-00-00Z.json
│ │ └── 2026-01-22T10-00-00Z.json
│ ├── chatgpt/
│ │ └── conversations/
│ │ ├── 2026-01-21T10-00-00Z.json
│ │ └── 2026-01-22T10-00-00Z.json
│ ├── youtube/
│ │ ├── watch_history/
│ │ │ ├── 2026-01-21T10-00-00Z.json
│ │ │ └── 2026-01-22T10-00-00Z.json
│ │ └── subscriptions/
│ │ ├── 2026-01-21T10-00-00Z.json
│ │ └── 2026-01-22T10-00-00Z.json
│ └── gmail/
│ ├── messages/
│ │ ├── 2026-01-21T10-00-00Z.json
│ │ └── 2026-01-22T10-00-00Z.json
│ └── labels/
│ ├── 2026-01-21T10-00-00Z.json
│ └── 2026-01-22T10-00-00Z.json
│
├── logs/ # Access logs (JSON lines, daily rotation)
│ └── access-{YYYY-MM-DD}.log # Daily audit log
│
├── index.db # Local registry index (SQLite)
│
└── server.json # Server configuration (includes storage + OAuth + sync)
Server Configuration (~/.vana/server.json):
Single config file for the Personal Server, including storage backend selection, OAuth tokens, and sync state.
{
"version": "1.0",
"server": {
"address": "0x...",
"url": "https://user-abc.server.vana.com",
"capabilities": {
"mcp": true,
"compute": true
}
},
"storage": {
"backend": "vana", // vana | gdrive | dropbox | ipfs | local
"config": {
// backend-specific settings (e.g., bucket, base path, cid, etc.)
},
"oauth": {
"gdrive": {
"accessToken": "...",
"refreshToken": "...",
"expiresAt": "2026-01-21T10:00:00Z"
},
"dropbox": {
"accessToken": "...",
"refreshToken": "...",
"expiresAt": "2026-01-21T10:00:00Z"
}
}
},
"sync": {
"lastProcessedTimestamp": "2026-01-21T10:00:00Z"
}
}File Naming Convention (Versioned):
~/.vana/data/{scope}/{YYYY-MM-DDTHH-mm-ssZ}.json
Examples:
~/.vana/data/instagram/profile/2026-01-21T10-00-00Z.json
~/.vana/data/chatgpt/conversations/2026-01-21T10-00-00Z.json
~/.vana/data/youtube/watch_history/2026-01-21T10-00-00Z.json
Data File Structure (decrypted, JSON only in v1):
Each data file contains standardized metadata plus the actual data. Data files are immutable after write; fileId is tracked in the Personal Server local index, not written into the file.
{
"$schema": "https://ipfs.io/<cid_for_schema_id>", // must match schemaId registered on-chain (DataRefinerRegistry)
"version": "1.0",
"scope": "instagram.profile",
"collectedAt": "2026-01-21T10:00:00Z",
"data": {
// Source-specific data structure
}
}Personal Servers serve raw data to Builders for scopes explicitly granted by the user. Builders do not access storage backends directly; storage is used only for sync/portability. Compute/LLM operations are out of scope for this spec.
Builder Read Request (HTTP):
GET /data/{scope}
Query params:
?fileId={fileId} # specific version by DataRegistry fileId
?at={ISO8601} # specific version by collectedAt (closest <= at)
(default: latest by collectedAt)
Headers:
Authorization: Web3Signed <base64url(json)>.<signature>
Signature Requirements:
- Builder MUST include an
Authorization: Web3Signed ...header per 4.1.9.1. grantIdin the Web3Signed payload MUST be the on-chainpermissionIdfor the Builder and user.
Grant Enforcement:
grantId(from Web3Signed payload) is the on-chainpermissionIdfromDataPortabilityPermissions- Personal Server recovers the signer address from the
Authorizationheader and verifies it matches the on-chain grantee forgrantId - Requested
scopeMUST be a subset of the granted scopes - Access is logged to the audit trail
Response:
- Returns the decrypted data file JSON for the requested
scope(see 5.2 Data File Format before encryption) - If multiple versions exist, default is the most recent by
collectedAt
All builder-initiated Personal Server requests MUST include a signed authorization header:
Authorization: Web3Signed <base64url(json)>.<signature>
Payload JSON (canonicalized, keys sorted alphabetically):
{
"aud": "https://user-abc.server.vana.com", // target origin
"method": "GET",
"uri": "/data?scopePrefix=instagram&limit=50&offset=0",
"bodyHash": "", // empty string for GET
"iat": 1737500000,
"exp": 1737500300,
"grantId": "0x..." // required only for raw data reads
}Signing Rules:
jsonMUST be canonicalized (keys sorted alphabetically at all levels).base64url(json)is the UTF-8 JSON string encoded with base64url (no padding).signatureis an EIP-191 signature over the ASCII bytes of<base64url(json)>.
Verification Rules (Personal Server):
- Recover signer from
signatureand verify signer is a registered Builder on-chain. audMUST match the Personal Server origin.methodanduriMUST match the actual request.bodyHashMUST match the request body (empty string for GET).iat/expMUST be within an allowed skew window (e.g., 5 minutes).- For raw data reads (
GET /data/{scope}), the Web3Signed payload MUST includegrantId.
The Personal Server includes an MCP (Model Context Protocol) server, enabling AI assistants to access user data with proper authorization.
MCP Resources:
| URI | Description |
|---|---|
vana://files |
List all data files for authenticated user |
vana://file/{scope} |
Get decrypted file content |
vana://file/{scope}/metadata |
Get file metadata only |
vana://grants |
List active grants |
vana://schemas |
List available data schemas |
vana://schema/{schemaId} |
Get schema definition |
MCP Tools:
// List files with optional filtering
async function list_files(params: {
wallet_address: string;
scopePrefix?: string;
limit?: number;
offset?: number;
}): Promise<FileList>;
// Get file content with optional JSONPath filter
async function get_file(params: {
wallet_address: string;
scope: string;
filter?: string; // JSONPath expression
}): Promise<FileContent>;
// Search across files
async function search_files(params: {
wallet_address: string;
query: string;
scopePrefixes?: string[];
}): Promise<SearchResults>;MCP Authentication:
All MCP requests require EIP-191 wallet signature verification:
// MCP client must sign request
const signature = await wallet.signMessage(
JSON.stringify({
action: "mcp_request",
uri: "vana://files",
timestamp: Date.now(),
}),
);
// Personal Server verifies signature and checks grantsDesktop-Bundled Personal Servers need a mechanism to be accessible from the internet for builder requests. This is achieved through tunneling via a Vana-managed FRP (Fast Reverse Proxy) server.
Vana FRP Tunnel
FRP provides stable, long-lived URLs without requiring third-party accounts. Vana operates an FRP server at proxy.server.vana.org with wildcard DNS for *.server.vana.org and TLS termination via Let's Encrypt.
┌─────────────────────────────────────────────────────────────────────────────┐
│ DESKTOP TUNNELING ARCHITECTURE │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ User's Desktop ││
│ │ ┌─────────────────────────────────────────────────────────────────────┐││
│ │ │ Desktop App (Tauri) │││
│ │ │ ├── Personal Server (localhost:8080) │││
│ │ │ └── frpc daemon │││
│ │ │ └── Outbound tunnel to Vana FRP server │││
│ │ └─────────────────────────────────────────────────────────────────────┘││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │ │
│ │ Outbound-only connection │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Vana FRP Server (proxy.server.vana.org) ││
│ │ URL: https://{userId}.server.vana.org ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │ │
│ │ Builder request │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Builder App ││
│ │ GET https://{userId}.server.vana.org/data ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Why FRP over Cloudflare Tunnel:
| Aspect | FRP | Cloudflare Tunnel |
|---|---|---|
| Third-party account | Not required | Required |
| Subdomain control | Vana-managed, tied to on-chain userId | Random or requires CF account |
| Ingress quality | Vana controls | Cloudflare controls |
Tunnel Lifecycle:
| Event | Action |
|---|---|
| Desktop App opens | Start frpc, establish tunnel |
| Tunnel established | Register tunnel URL in DataPortabilityServers (if changed) |
| Desktop App closes | Tunnel terminates, builders get 503 |
| Builder request while offline | DP RPC returns "server unavailable" error |
Fallback Behavior:
When Desktop App is closed and user has NOT enabled ODL Cloud:
- Builder requests to personal server URL fail (503 or timeout)
- DP RPC returns
{ "error": "server_unavailable", "message": "User's personal server is offline" } - Builder MAY prompt user: "Open your Vana app or enable always-on access"
When user HAS enabled ODL Cloud:
- DataPortabilityServers points to Sprite URL (not tunnel URL)
- Sprite auto-activates on request
- Requests always succeed (with ~1-2s cold start latency)
Provides fast API access to protocol operations with eventual chain consistency.
For now, all on-chain operations are submitted through the Gateway as a relayer; clients sign EIP-712 payloads and the Gateway validates the operations and returns immediately, without waiting for on-chain confirmation. The Gateway broadcasts the operations on-chain asynchronously.
Latency of submitting on-chain transactions to Vana L1 has high latency, so it's not suitable for providing real-time UX. Conversely, the Gateway:
- Accepts signed operations immediately
- Returns instant responses
- Syncs to chain asynchronously
- Allows verification against chain for trust
Phase 0: Use Gateway + signature verification
Phase 1: Add on-chain anchoring, the operations can be verified on-chain after on-chain confirmation
┌─────────────────────────────────────────────────────────────────┐
│ TRUST SPECTRUM │
│ │
│ Fast ◀─────────────────────────────────────────────▶ Trustless │
│ │
│ Gateway Gateway + Gateway + Chain │
│ only signature spot-check only │
│ verification chain │
│ │
│ ~50ms ~50ms ~50ms + async ~5-10s │
│ Trust Vana Trust user Trust but Trustless │
│ signed verify │
└─────────────────────────────────────────────────────────────────┘
# Personal Server Operations
POST /v1/servers. Register/update Personal Server URL
GET /v1/servers/{address} Get Personal Server info
GET /v1/servers/{address}/status
Get confirmation status (pending/confirmed)
# Grant Operations
POST /v1/grants Create grant
DELETE /v1/grants/{grantId} Revoke grant
GET /v1/grants/{grantId} Get grant details
GET /v1/grants?user={address} List grants for user
GET /v1/grants?builder={address}
List grants for builder
GET /v1/grants/{grantId}/status Get confirmation status (pending/confirmed)
# File Registry Operations
POST /v1/files Register file record (schemaId required)
GET /v1/files/{fileId} Get file record
GET /v1/files?user={address} List files for user
GET /v1/files?user={address}&since={ISO8601}
List files for user since timestamp
GET /v1/files/{fileId}/status
Get confirmation status (pending/confirmed)
# Schema Registry
GET /v1/schemas/{schemaId} Get schema metadata (canonical scope + schema URL)
GET /v1/schemas?scope={scope} Look up schemaId by canonical scope (reverse lookup)
GET /v1/schemas/{schemaId}/status
Get confirmation status (pending/confirmed)
# Builder Operations
POST /v1/builders. Register builder (public key + app URL)
GET /v1/builders/{address} Get builder info (public key + app URL)
GET /v1/builders/{address}/status
Get confirmation status (pending/confirmed)
# Sync Status
GET /v1/sync/status Get chain sync status
# Nonces
GET /v1/nonces?user={address}&operation={operation}
Get the current and next nonce per user and operation
grantId refers to the on-chain permissionId from DataPortabilityPermissions.
serverAddress and builderAddress are used as their idempotent IDs. grantId and fileId are computed deterministically from their input parameters. Those IDs are consistent between the off-chain gateway and the on-chain smart contracts, so that they can be used to query both on-chain and off-chain with the Gateway.
Request
Each POST request has an Authorization header with the user's EIP-712 signature against the fields in the request's body. The request's body includes the parameters of the corresponding on-chain function with a nonce to prevent replay attacks.
Example:
POST /v1/files
Authorization: Signature 0xabc123...def
Content-Type: application/json
{
"url": "https://storage.vana.com/alice/encrypted/instagram-profile-2025-01-28.enc",
"schemaId": 7,
"nonce": 42
}
| Gateway API | On-chain function | Contract |
|---|---|---|
POST /v1/files |
addFileWithSchema(string url, uint256 schemaId) |
DataRegistry (0x8C8788f98385F6ba1adD4234e551ABba0f82Cb7C) |
POST /v1/servers |
addServer(address serverAddress, string ``publicKey``, string serverUrl) |
DataPortabilityServer (0x1483B1F634DBA75AeaE60da7f01A679aabd5ee2c) |
POST /v1/builders |
registerGrantee(address owner, address granteeAddress, string publicKey) |
DataPortabilityGrantees (0x8325C0A0948483EdA023A1A2Fd895e62C5131234) |
POST /v1/grants |
addPermission(uint256 granteeId, string grant, uint256[] fileIds) |
DataPortabilityPermissions (0xD54523048AdD05b4d734aFaE7C68324Ebb7373eF) |
Response
All responses include verification data:
{
"data": {
"serverUrl": "https://server.alice.com",
"publicKey": "0x..."
},
"proof": {
"userSignature": "0x...", // User's EIP-712 signature
"gatewaySignature": "0x...", // Gateway attestation
"timestamp": 1737500000,
"status": "pending", // Chain sync status, pending or confirmed
"estimatedConfirmation": "30s", // Estimated on-chain confirmation time
"chainBlockHeight": null // Block where confirmed
}
}
Write Operations
Each Gateway's POST API endpoint performs the same verification on the inputs as the corresponding on-chain function against the Gateway's current state. In addition, the Gateway manages the user nonces and verifies the signatures in the Authorization headers.
After all the verification, the Gateway commits the operations to its off-chain DB and marks them as pending for on-chain submission and confirmation.
The Gateway assigns to each operation record (files, grants, builders, and servers) a bytes32 ID, which is deterministically computed from hashing the operation's parameters. The smart contracts uses the same way to compute the IDs of those entities so that the records in the off-chain Gateway's DB and the on-chain entities always have the same IDs.
DOMAIN_TYPE_HASH = keccak256("DataPortabilityDomain(uint256 chainId,address verifyingContract)")
domainSeparator = keccak256(DOMAIN_TYPE_HASH, chainId, verifyingContract)
fileId = keccak256(abi.encode(domainSeparator, ownerAddress, url, schemaId))
To prevent front-running attacks, in which a file url is registered before the Gateway with another ownerAddress or schemaId, the DataRegistry contract MUST use the hash-based fileId for uniqueness. If a fileId is already registered, the contract returns immediately. (Currently, the DataRegistry contract uses only url for uniqueness that prevents the Gateway from registering an url if it is front-runned.)
serverId = keccak256(abi.encode(domainSeparator, serverAddress, publicKey, serverUrl))
builderId = keccak256(abi.encode(domainSeparator, owner, granteeAddress, publicKey))
grantId = keccak256(abi.encode(domainSeparator, granteeId, grant, fileIds))
Note: Don't use abi.encodePacked because it creates collisions in some dynamic types like string or arrays.
The Gateway stores the pending operations in a queue and submit them on-chain periodically. For efficiency, the Gateway broadcasts the operations in batch, via a batch function per operation types or via Account Abstraction bundler-style handleOps function. After those operations are confirmed on-chain, the Gateway updates their chain sync statuses to confirmed.
Read Operations
The Gateway returns the records in its off-chain DB if the chain sync status is pending. Otherwise, it reads the on-chain records and returns. The Gateway also provides a proof, including its attestation on (requestHash, responseHash).
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
contract DataPortabilityServers {
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;
}
struct AddServerInput {
address serverAddress;
string publicKey;
string serverUrl;
}
struct AddServerWithSignatureInput {
uint256 nonce;
address serverAddress;
string publicKey;
string serverUrl;
}
struct TrustServerInput {
uint256 nonce;
uint256 serverId;
}
struct UntrustServerInput {
uint256 nonce;
uint256 serverId;
}
event ServerRegistered(uint256 indexed serverId, address indexed owner, address indexed serverAddress, string publicKey, string url);
event ServerUpdated(uint256 indexed serverId, string url);
event ServerTrusted(address indexed user, uint256 indexed serverId);
event ServerUntrusted(address indexed user, uint256 indexed serverId);
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);
}contract DataPortabilityGrantees {
struct GranteeInfo {
address owner;
address granteeAddress;
string publicKey;
string appUrl; // New field
uint256[] permissionIds;
}
struct GranteeInfoV2 {
address owner;
address granteeAddress;
string publicKey;
string appUrl; // New field
uint256 permissionsCount;
}
event GranteeRegistered(
uint256 indexed granteeId,
address indexed owner,
address indexed granteeAddress,
string publicKey,
string appUrl
);
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 granteesV2(uint256 granteeId) external view returns (GranteeInfoV2 memory);
function granteeByAddress(address granteeAddress) external view returns (GranteeInfo memory);
function granteeByAddressV2(address granteeAddress) external view returns (GranteeInfoV2 memory);
function granteeAddressToId(address granteeAddress) external view returns (uint256);
function granteesCount() external view returns (uint256);
function granteePermissions(uint256 granteeId) external view returns (uint256[] memory);
}contract DataPortabilityPermissions {
struct PermissionInfo {
uint256 id;
address grantor;
uint256 nonce;
uint256 granteeId;
string grant;
uint256 startBlock;
uint256 endBlock;
uint256[] fileIds;
}
struct PermissionInput {
uint256 nonce;
uint256 granteeId;
string grant;
uint256[] fileIds;
}
struct ServerFilesAndPermissionInput {
uint256 nonce;
uint256 granteeId;
string grant;
string[] fileUrls;
uint256[] schemaIds;
address serverAddress;
string serverUrl;
string serverPublicKey;
IDataRegistry.Permission[][] filePermissions;
}
struct RevokePermissionInput {
uint256 nonce;
uint256 permissionId;
}
event PermissionAdded(uint256 indexed permissionId, address indexed user, uint256 indexed granteeId, string grant, uint256[] fileIds);
event PermissionRevoked(uint256 indexed permissionId);
function addPermission(PermissionInput calldata permission, bytes calldata signature) external returns (uint256);
function addServerFilesAndPermissions(ServerFilesAndPermissionInput calldata input, 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);
function filePermissionIds(uint256 fileId) external view returns (uint256[] memory);
function permissionsCount() external view returns (uint256);
function userNonce(address user) external view returns (uint256);
function userPermissionIdsValues(address user) external view returns (uint256[] memory);
function dataRegistry() external view returns (IDataRegistry);
function dataPortabilityServers() external view returns (IDataPortabilityServers);
function dataPortabilityGrantees() external view returns (IDataPortabilityGrantees);
}contract DataRegistry {
struct FileResponse {
uint256 id;
address ownerAddress;
string url;
uint256 schemaId;
uint256 addedAtBlock;
}
struct Permission {
address account;
string key;
}
event FileAdded(uint256 indexed fileId, address indexed ownerAddress, string url);
event FileAddedV2(uint256 indexed fileId, address indexed ownerAddress, string url, uint256 schemaId);
event PermissionGranted(uint256 indexed fileId, address indexed account);
function addFile(string memory url) external returns (uint256);
function addFileWithSchema(string memory url, uint256 schemaId) external returns (uint256);
function addFileWithPermissions(string memory url, address ownerAddress, Permission[] memory permissions) external returns (uint256);
function addFileWithPermissionsAndSchema(string memory url, address ownerAddress, Permission[] memory permissions, uint256 schemaId) external returns (uint256);
function addFilePermissionsAndSchema(uint256 fileId, Permission[] memory permissions, uint256 schemaId) external;
function addFilePermission(uint256 fileId, address account, string memory key) external;
function files(uint256 index) external view returns (FileResponse memory);
function fileIdByUrl(string memory url) external view returns (uint256);
function filePermissions(uint256 fileId, address account) external view returns (string memory);
}Requirement: Clients MUST register files with a schemaId (use addFileWithSchema or addFileWithPermissionsAndSchema). Calls that omit schemaId are invalid for Data Portability Protocol compliance.
A user-facing application that:
- Authenticates user to the protocol
- Runs Data Connectors to collect data
- Configures Personal Server
- Manages grants and permissions
- Provides data browsing and search
The Desktop App is a protocol client, NOT a protocol participant:
- It is NOT registered on-chain
- It CONTROLS the Personal Server (which IS a protocol participant)
- If it bundles a Personal Server, that server still MUST be registered on-chain and is the participant
- Multiple Desktop Apps can exist (different platforms, different UIs)
┌─────────────────────────────────────────────────────────────────┐
│ DATA PORTABILITY CLIENT ROLES │
├─────────────────────────────────────────────────────────────────┤
│ │
│ IDENTITY │
│ ├── Sign in via Passport (wallet abstraction) │
│ └── Manage wallet settings (in Passport) │
│ │
│ DATA COLLECTION (Not part of protocol) │
│ ├── Run Data Connectors (scraping) │
│ ├── Send raw data to Personal Server (local or remote) │
│ └── Personal Server encrypts + uploads to storage backend │
│ │
│ PERSONAL SERVER CONFIGURATION │
│ ├── Register Personal Server on-chain (address + URL) │
│ ├── Configure storage backend (enables sync + on-chain writes)│
│ ├── Configure capabilities │
│ └── Monitor server health │
│ │
│ PERMISSION MANAGEMENT │
│ ├── View incoming grant requests │
│ ├── Approve/deny grants │
│ ├── Revoke existing grants │
│ └── Set auto-approve rules │
│ │
│ DATA MANAGEMENT │
│ ├── Browse connected data │
│ ├── View access logs │
│ ├── Delete data │
│ └── Export data │
│ │
│ TOOLING (Not part of protocol, bundled for convenience) │
│ ├── Local search / vector DB │
│ ├── MCP server for local AI │
│ └── Data visualization │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ DATA PORTABILITY CLIENT │
│ │
│ ┌─────────────────┐ │
│ │ Data Connector │──── Scrape data ────▶ Platform │
│ │ (e.g. Instagram)│ (Instagram) │
│ └────────┬────────┘ │
│ │ Raw data (JSON) │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Personal Server │ │
│ │ (bundled or │ │
│ │ remote) │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Local Storage │ ~/.vana/data/{scope}/... (unencrypted) │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Encrypt + Upload│──────▶ Storage Backend (if configured) │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Until a storage backend is selected, sync targets the local Personal Server only and does not write to the on-chain registry.
Client-side authentication and wallet provisioning. Passport is NOT part of the protocol; each client can choose its own vendor to manage user wallets and map them to on-chain addresses.
- MUST support wallet creation without seed phrase exposure to user
- MUST support social login (Google, Apple, email)
- MUST support wallet recovery via social/email
- SHOULD support existing wallet import for advanced users
Vana uses Privy as the Passport vendor in the Desktop App; other clients may use different providers.
┌─────────┐ ┌─────────────┐ ┌─────────┐
│ User │ │ Passport │ │ Client │
│ │ │ (Vendor) │ │ │
└────┬────┘ └──────┬──────┘ └────┬────┘
│ │ │
│ 1. Click "Sign In" │ │
│────────────────────────────────────────────▶│
│ │ │
│ 2. Redirect to Passport │ │
│◀────────────────────────────────────────────│
│ │ │
│ 3. Authenticate │ │
│─────────────────────▶│ │
│ │ │
│ 4. Create/retrieve │ │
│ wallet │ │
│◀─────────────────────│ │
│ │ │
│ 5. Return JWT + │ │
│ wallet address │ │
│◀────────────────────────────────────────────│
│ │ │
The Session Relay is a standalone service that coordinates "Connect Data" between the builder web popup and the Desktop App. It stores short-lived session state in a Neon PostgreSQL database and is not part of the DP RPC/Gateway.
Session State
pending → claimed → approved
→ `expired` (15 minutes)
→ `denied`
Request Signing
POST /v1/session/initMUST includeAuthorization: Web3Signed <base64url(json)>.<signature>- The Web3Signed payload uses the same format as 4.1.9.1, with:
aud= Session Relay originuri=/v1/session/initbodyHash= SHA-256 of the request body
app_user_idis optional and used only for builder correlation when the grant is returned- Session Relay verifies the signature against
granteeAddress
Session API
POST /v1/session/init
Headers: { Authorization: Web3Signed <base64url(json)>.<signature> }
Input: { granteeAddress, scopes, webhookUrl?, app_user_id? }
Output: { sessionId, deepLinkUrl, expiresAt }
Deep link format: vana://connect?sessionId=xxx&secret=yyy
GET /v1/session/{sessionId}/poll
Input: { }
Output: { status, grant?, reason? }
POST /v1/session/claim
Input: { sessionId, secret }
Output: { sessionId, granteeAddress, scopes, webhookUrl?, app_user_id?, expiresAt }
POST /v1/session/{sessionId}/approve
Input: { secret, grantId, userAddress, scopes }
Output: { status: "success" }
POST /v1/session/{sessionId}/deny
Input: { secret, reason? }
Output: { status: "denied" }
The secret is a cryptographic token generated at session creation. It is included in the deep link URL and required for claim/approve/deny to bind the Desktop App to the session securely.
Grant Payload (returned via poll/webhook):
{
"grantId": "0x...", // on-chain permissionId
"userAddress": "0x...",
"builderAddress": "0x...",
"scopes": ["instagram.profile", "instagram.likes"],
"expiresAt": 0,
"app_user_id": "optional"
}Builders use grantId to fetch full grant details from the Gateway and userAddress to resolve the Personal Server URL.
Scopes follow a hierarchical naming convention:
{source}.{category}[.{subcategory}]
Examples:
instagram.profile
instagram.posts
instagram.likes
instagram.followers
chatgpt.conversations
chatgpt.conversations.shared
youtube.watch_history
youtube.subscriptions
gmail.messages
gmail.labels
The source is derived from the first segment of the scope (e.g., instagram in instagram.posts).
Schema definitions MUST encode the canonical scope for the dataset (e.g., in schema metadata). This provides the pre-decryption reference needed to derive scope keys from schemaId; the file payload remains the source of truth for scope and collectedAt.
In v1, data files are JSON objects before encryption (future versions may support other mime types). The entire plaintext JSON object is encrypted as a single blob; no plaintext metadata is stored alongside the ciphertext. fileId linkage is tracked in the Personal Server local index, not written into the file. Each data file MUST include a $schema URL that points to the IPFS CID for the schemaId registered on-chain (DataRefinerRegistry).
{
"version": "1.0",
"scope": "instagram.profile",
"collectedAt": "2026-01-21T10:00:00Z",
"data": {
"username": "alice",
"displayName": "Alice Smith",
"bio": "...",
"followers": 1234,
"following": 567
}
}
After encryption, the file is stored as an OpenPGP binary message.
const grantTypedData = {
domain: {
name: "Vana Data Portability",
version: "1",
chainId: 14800,
verifyingContract: "0x...", // DataPortabilityPermissions address
},
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", "instagram.likes"],
expiresAt: 0,
nonce: 1,
},
};Grant Identifier:
grantIdin API responses refers to the on-chainpermissionIdreturned byDataPortabilityPermissions.
{
"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"
}Builders MUST publish an app manifest to provide human-readable consent UI metadata. The Builder's appUrl is stored on-chain (DataPortabilityGrantees) and is the canonical origin for discovering the manifest.
Manifest Discovery (Browser-Compatible):
- Clients MUST fetch
https://{appUrl}and resolve the manifest URL from<link rel="manifest" href="..."> - The manifest URL MUST be same-origin with
appUrl
Base Standard: W3C Web App Manifest with a custom top-level vana block for protocol-specific metadata.
Required vana Fields (logical):
vana.appUrl(canonical app origin, must match on-chainappUrl)vana.privacyPolicyUrlvana.termsUrlvana.supportUrlvana.webhookUrlvana.signature(Builder signature over thevanablock)
Scope Labels:
- Consent UI labels for scopes come from Data Connector metadata in the Desktop App, not from the manifest.
Signature:
vana.signatureMUST be an EIP-191 signature by the Builder address over the canonical JSON of thevanablock- Canonicalization: sort keys alphabetically, and exclude the
signaturefield from the signed payload
Verification (Desktop App / Personal Server UI):
- Fetch
https://{appUrl}and resolve the manifest URL from<link rel="manifest"> - Verify
vana.appUrlequals the on-chainappUrl - Recompute the canonical JSON for the
vanablock and verifyvana.signaturerecovers the Builder address - Ensure requested
webhookUrlmatchesvana.webhookUrl
Manifest Failure:
- If manifest discovery or signature verification fails, the Desktop App MUST NOT render the consent screen and MUST fail the session flow.
Example Manifest (abridged):
{
"name": "Flipboard",
"short_name": "Flipboard",
"start_url": "https://flipboard.com/",
"scope": "https://flipboard.com/",
"icons": [
{ "src": "/icons/512.png", "sizes": "512x512", "type": "image/png" }
],
"vana": {
"appUrl": "https://flipboard.com",
"privacyPolicyUrl": "https://flipboard.com/privacy",
"termsUrl": "https://flipboard.com/terms",
"supportUrl": "https://flipboard.com/support",
"webhookUrl": "https://api.flipboard.com/vana/webhook",
"signature": "0x..."
}
}Data Connectors define the canonical human-readable names for scopes. The Desktop App MUST use Data Connector metadata to render consent UI labels.
Connector Definition (logical):
{
"connectorId": "instagram",
"displayName": "Instagram",
"scopes": [
{
"scope": "instagram.profile",
"label": "Your Instagram profile",
"description": "Basic profile info, bio, and counts"
},
{
"scope": "instagram.posts",
"label": "Your Instagram posts",
"description": "Your posts and captions"
}
],
"version": "1.0"
}┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ User │ │ Desktop │ │ Identity│ │ DP RPC │
│ │ │ App │ │Provider │ │ │
└────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘
│ │ │ │
│ 1. Open app │ │ │
│───────────────▶│ │ │
│ │ │ │
│ │ 2. Redirect │ │
│ │───────────────▶│ │
│ │ │ │
│ 3. Authenticate (social/email) │ │
│────────────────────────────────▶│ │
│ │ │ │
│ │ 4. Wallet │ │
│ │ created │ │
│ │◀───────────────│ │
│ │ │ │
│ │ │ │
│ 7. Setup complete │ │
│◀───────────────│ │ │
│ │ │ │
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌────────────┐ ┌─────────┐ ┌─────────┐
│ User │ │ Desktop │ │ Platform│ │ Personal │ │ Storage │ │ DP RPC │
│ │ │ App │ │(Instagram) │ Server │ │ Backend │ │(Gateway)│
└────┬────┘ └────┬────┘ └────┬────┘ └────┬───────┘ └────┬────┘ └────┬────┘
│ │ │ │ │ │
│ 1. Click │ │ │ │ │
│ "Connect │ │ │ │ │
│ Instagram" │ │ │ │ │
│───────────────▶│ │ │ │ │
│ │ │ │ │ │
│ │ 2. Open embedded browser │ │ │
│ │───────────────▶│ │ │ │
│ │ │ │ │ │
│ 3. Log in to Instagram │ │ │ │
│────────────────────────────────▶│ │ │ │
│ │ │ │ │ │
│ │ 4. Scrape data │ │ │ │
│ │ (user's IP) │ │ │ │
│ │◀───────────────│ │ │ │
│ │ │ │ │ │
│ │ 5. Send raw data │ │ │
│ │───────────────────────────────▶│ │ │
│ │ │ │ │ │
│ │ │ │ 6. Store locally (unencrypted) │
│ │ │ │ │ │
│ │ │ │ 7. Upload encrypted blob │
│ │ │ │───────────────────────────────▶│
│ │ │ │ │ │
│ │ │ │ 8. Register file record │
│ │ │ │────────────────────────────────▶│
│ │ │ │ │ │
│ 9. "Instagram connected ✓" │ │ │ │
│◀───────────────│ │ │ │ │
│ │ │ │ │ │
If no storage backend has been selected yet, steps 7–8 are skipped; data remains local-only and is not written on-chain.
The "Connect data" flow allows users to connect their Vana Personal Server to a third-party Builder application. This flow uses a standalone Session Relay service to coordinate between the builder web popup and the Desktop App using deep links (no manual code entry).
- Initiation: User clicks "Connect data" on Builder App.
- Session Creation: Builder backend creates a session (signed) via Session Relay. The frontend opens a popup with a deep link.
- Desktop Pairing: User opens the Desktop App via deep link (Desktop App must be installed).
- Consent: Desktop App fetches session details, resolves Builder metadata by discovering the manifest from
appUrl, verifies signature, and prompts the user to approve. - Completion: Desktop App calls
POST /v1/grantson the Personal Server with{ granteeAddress, scopes }. The Personal Server signs the EIP-712 grant registration and submits it to the Gateway, returning agrantId. The Desktop App then approves the session via Session Relay with{ secret, grantId, userAddress, scopes }. The Builder receives the grant payload via polling or webhook and completes the login.
Grant payload includes grantId (on-chain permissionId) and the user's wallet address.
┌─────────┐ ┌─────────┐ ┌────────────┐ ┌─────────┐
│ User │ │ Builder │ │ Session │ │ Desktop │
│ │ │ (Browser│ │ Relay │ │ App │
│ │ │ Popup) │ │ (Service) │ │ │
└────┬────┘ └────┬────┘ └────┬───────┘ └────┬────┘
│ │ │ │
│ 1. Click "Connect │ │ │
│ data" │ │ │
│───────────────▶│ │ │
│ │ │ │
│ │ 2. Open Popup │
│ │ (sessionId) │
│ │───────────────▶│ │
│ │ │ │
│ │ │ 3. Session already created
│ │ │ Return session_id
│ │ │────────────────│
│ │ │ │
│ │ │ 4. Display Deep Link
│ │ │◀───────────────│
│ │ │ │
│ 5. Install │ │ │
│ Desktop App │ │ │
│───────────────▶│ │ │
│ │ │ │
│ 6. Open App, │ │ │
│ Sign In, │ │ │
│ Open Deep │ │ │
│ Link │ │ │
│─────────────────────────────────────────────────▶│
│ │ │ │
│ │ │ 7. Claim Session (Deep Link)
│ │ │◀───────────────│
│ │ │ │
│ │ │ 8. Return Session Details
│ │ │ (Builder address, Scopes)
│ │ │───────────────▶│
│ │ │ │
│ 9. Approve │ │ │
│ Grant │ │ │
│─────────────────────────────────────────────────▶│
│ │ │ │
│ │ │ │
│ │ │ │ ┌──────────────┐
│ │ │ │ │ Personal │
│ │ │ │ │ Server │
│ │ │ │ └──────┬───────┘
│ │ │ │ │
│ │ │ 10. POST /v1/grants │
│ │ │ { granteeAddress, │
│ │ │ scopes } │
│ │ │───────────────▶│ │
│ │ │ │ │
│ │ │ │ 10a. Sign EIP-712 +
│ │ │ │ Submit to Gateway
│ │ │ │───────▶│
│ │ │ │◀───────│
│ │ │ │ │
│ │ │ 10b. { grantId } │
│ │ │◀───────────────│ │
│ │ │ │ │
│ │ │ 11. Approve Session │
│ │ │ { secret, grantId, │
│ │ │ userAddress, │
│ │ │ scopes } │
│ │ │◀───────────────│
│ │ │ │
│ │ 12. Poll/webhook for Grant │
│ │◀───────────────│ │
│ │ │ │
│ │ 13. Fetch Data (using Grant) │
│ │────────────────────────────────▶│
│ │ │
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ User │ │ Desktop │ │ Gateway │ │Personal │
│ │ │ App │ │ │ │ Server │
└────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘
│ │ │ │
│ 1. Click │ │ │
│ "Revoke" for │ │ │
│ Builder X │ │ │
│───────────────▶│ │ │
│ │ │ │
│ │ 2. Sign revocation │
│ │────────────────│ │
│ │ │ │
│ │ 3. Submit to Gateway │
│ │───────────────▶│ │
│ │ │ │
│ │ 4. Instant "Revoked" │
│ │◀───────────────│ │
│ │ │ │
│ │ │ 5. Async: record on-chain
│ │ │────────────────▶
│ │ │ │
│ │ 6. Notify Personal Server │
│ │───────────────────────────────▶│
│ │ │ │
│ │ │ │ 7. Block
│ │ │ │ future
│ │ │ │ requests
│ │ │ │
│ 8. "Grant │ │ │
│ revoked ✓" │ │ │
│◀───────────────│ │ │
│ │ │ │
Principle: DataRegistry file entries are immutable; deletion is implemented as storage cleanup plus permission revocation and a tombstone that signals the file is no longer available.
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ User │ │ Desktop │ │Personal │ │ Storage │ │ Gateway │
│ │ │ App │ │ Server │ │ Backend │ │ │
└────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘
│ │ │ │ │
│ 1. Click │ │ │ │
│ "Delete data" │ │ │ │
│───────────────▶│ │ │ │
│ │ 2. Request delete │ │
│ │───────────────────────────────▶│ │
│ │ │ │ │
│ │ │ 3. Delete encrypted blob │
│ │ │───────────────────────────────▶│
│ │ │ │ │
│ │ │ 4. Remove local decrypted copy │
│ │ │ │ │
│ │ 5. Write tombstone / registry update │
│ │────────────────────────────────────────────────▶│
│ │ │ │ │
│ 6. "Deleted ✓" │ │ │ │
│◀───────────────│ │ │ │
Notes:
- Deletion is user-initiated (Desktop App or Personal Server UI) and is not exposed to builders.
- Gateway or chain layer records a delete marker (tombstone) so downstream systems treat the file as unavailable.
- Personal Servers MUST treat tombstoned file records as non-existent and return
410or404for read attempts.
The builder-facing SDK provides both server-side and client-side tools for integrating the "Connect data" flow and accessing user data. Published as @opendatalabs/connect on NPM.
Package Structure (Multi-Entrypoint)
@opendatalabs/connect/server — Session Relay client, request signer, data client
@opendatalabs/connect/react — Polling hook, Connect button component
@opendatalabs/connect/core — Shared types, errors
Prerequisite: The Builder must be registered on-chain (via the Desktop App + Gateway relayer) with a valid appUrl and public key.
Builder Registration (Desktop App) — Minimal Steps
- Open Desktop App -> Builder Registration
- Enter
appUrl(canonical app origin) - Desktop App generates (or imports) a Builder wallet and keypair
- Desktop App displays:
- Builder address (granteeAddress)
- Builder public key
- Builder private key (download/store securely)
- Desktop App submits
registerGranteevia Gateway relayer - Builder config:
- Set
VANA_APP_PRIVATE_KEYin server env (used to signAuthorization: Web3Signed ...) - Use
granteeAddressin@opendatalabs/connectinit - Publish manifest at
appUrlwith validvanablock
- Set
Server-Side SDK (@opendatalabs/connect/server)
import {
createSessionRelay,
createDataClient,
createRequestSigner,
} from "@opendatalabs/connect/server";
// Session Relay — create sessions and poll for completion
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 Client — fetch user data with grants
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 SDK (@opendatalabs/connect/react)
import { useVanaConnect, ConnectButton } from "@opendatalabs/connect/react";
// Hook-based (custom UI)
const { connect, status, grant, deepLinkUrl, reset } = useVanaConnect({
sessionRelayUrl: "https://session-relay.vana.org",
});
await connect({ sessionId: "..." }); // sessionId from server
// Component-based (pre-built UX)
<ConnectButton
sessionId={sessionId}
sessionRelayUrl="https://session-relay.vana.org"
onComplete={(grant) => { /* handle grant */ }}
/>Responsibilities
- Server: Create signed Session Relay sessions, poll for completion, generate Web3Signed headers, fetch data from Personal Servers
- React: Poll session status from browser (no auth needed), display deep link, manage connection lifecycle
- Enforce that the Builder has an on-chain
appUrland a valid manifest discoverable via<link rel="manifest">
Signing Helpers
@opendatalabs/connect/server provides createRequestSigner() to canonicalize JSON (sort keys alphabetically) and generate Authorization: Web3Signed ... headers using the builder's private key.
- All user data MUST be encrypted with OpenPGP password-based encryption before being written to a storage backend. The password is
hex(scope_key)where the scope key is derived per §2.3. - Personal Servers serve decrypted data to authorized builders over TLS
- Vana (or any intermediary) MUST NOT have access to plaintext data
- On-chain protocol operations MUST be signed by the user's wallet using EIP-712 typed data.
- Personal Server data requests from Builders MUST include
Authorization: Web3Signed ...using ECDSA (EIP-191) per 4.1.9.1. - Nonces MUST be used to prevent replay attacks for on-chain operations.
- Personal Server MUST verify grant validity before serving data.
- Grant verification MUST check:
- Signature is valid
- Grant is not revoked
- Grant has not expired
- Requested scope is within granted scopes
Authorizationsigner matches the on-chain grantee for the grant
- All HTTP endpoints MUST use TLS 1.3
- Personal Servers SHOULD implement certificate pinning
- Gateway SHOULD implement rate limiting
| Threat | Mitigation |
|---|---|
| Vana sees user data | Data encrypted before upload, Vana has no key |
| Builder exceeds granted scope | Personal Server validates scope on each request |
| Grant replay | Nonces and timestamps in grant signature |
| Malicious builder | User must explicitly approve, can revoke anytime |
| Gateway lies about grants | Grants include user signature, verifiable on chain |
| Personal Server compromised | User data on that server may be exposed; storage backend remains encrypted |
Following SMTP convention, DP uses a 3-digit error code system:
First digit:
2xx - Success
3xx - Intermediate (more input needed)
4xx - Temporary failure (retry may succeed)
5xx - Permanent failure (do not retry)
Second digit:
x0x - Syntax/format
x1x - Authentication/authorization
x2x - Data/storage
x3x - Grant/permission
x4x - Protocol/network
x5x - Rate limiting
| Code | Description |
|---|---|
| 200 | Success |
| 201 | Created |
| 301 | Redirect to personal server |
| 400 | Bad request (syntax error) |
| 401 | Unauthorized (invalid signature) |
| 403 | Forbidden (valid auth but not permitted) |
| 404 | Not found |
| 410 | Grant revoked |
| 411 | Grant expired |
| 412 | Scope not granted |
| 420 | Data not found at registry entry |
| 421 | Storage backend unavailable |
| 429 | Rate limited |
| 440 | Chain sync pending |
| 500 | Internal server error |
| 503 | Service unavailable |
{
"error": {
"code": 412,
"message": "Scope not granted",
"details": {
"requestedScope": "instagram.messages",
"grantedScopes": ["instagram.profile", "instagram.likes"]
}
}
}New data sources can be added by:
- Creating a Data Connector (implementation-specific, not protocol)
- Defining scope taxonomy for the dataset
- Implementing data format schema
Data Connectors are NOT part of the protocol. They are implementation details of specific clients.
Storage backends implement a standard interface:
interface StorageBackend {
upload(key: string, data: Uint8Array): Promise<string>; // Returns canonical URL
download(url: string): Promise<Uint8Array>;
delete(url: string): Promise<boolean>;
exists(url: string): Promise<boolean>;
// Optional: bulk operations (not all backends support)
deleteScope?(scope: string): Promise<number>; // Returns count deleted
deleteAll?(): Promise<number>; // Returns count deleted
}Personal Servers declare capabilities in their registration:
capabilities: ["storage", "compute", "mcp"]
New capabilities can be defined without protocol changes.
Based on team discussions, the following clarifications resolve potential misalignments:
| Aspect | Desktop App | Personal Server |
|---|---|---|
| Protocol participant? | NO | YES |
| Registered on-chain? | NO | YES |
| Can receive requests? | NO | YES |
| Can act unattended? | NO | YES |
| Multiple instances? | YES (per user) | YES (per user) |
| Relationship | Controls | Is controlled |
Resolution: Desktop App is a CLIENT that CONFIGURES the Personal Server. Like how an email client configures SMTP/IMAP servers. If the Desktop App bundles a Personal Server, the embedded server is still the protocol participant and must be registered on-chain; the app remains a client.
Data Connectors (web scrapers) are NOT part of the protocol. They are:
- Implementation-specific to the Desktop App
- Can vary between clients
- Follow protocol standard for data output format
Resolution: The protocol defines data FORMAT, not data COLLECTION.
Search, vector DB, MCP server are NOT part of the protocol. They are:
- Convenience features bundled with Desktop App
- Can be omitted in alternative clients
- Advanced queries belong in third-party apps (like Vana Trace)
Resolution: Protocol is minimal. Desktop App bundles extras for UX.
Discussion: Anna noted the Desktop App (via its bundled Personal Server) already acts like a locally hosted personal server (decrypting and re-encrypting data). She proposed bundling the Personal Server into the Desktop App for local compute.
Maciej's synthesis: The protocol itself is NOT opinionated about whether the Personal Server stores encrypted or unencrypted data. This is an implementation decision:
| Implementation | Encrypted at Rest? | Rationale |
|---|---|---|
| Vana-Hosted | MUST encrypt | Vana ≠ data controller; legal "blind infrastructure" |
| Desktop-Bundled | MAY store unencrypted | User's security zone; enables local compute |
| Self-Hosted | User's choice | User controls their server |
Resolution:
- Protocol requires encryption in transit and when sharing with builders
- At-rest encryption is implementation-specific
- Desktop-Bundled Personal Server CAN access unencrypted data (user's security zone)
- See Section 4.1.4 for full specification
- "Rich email client" vs "light app" — The spec describes full functionality. UX may prioritize specific flows initially.
- Desktop-Bundled availability — When Desktop App is closed, the bundled Personal Server is unavailable. Builders SHOULD fall back to ODL Cloud when enabled; otherwise requests return
server unavailable. - Delegated signature lifetime — Delegated signature for ODL Cloud NEVER expires; users revoke by disabling ODL Cloud and deleting the Sprite.
- V1 chain anchoring — Notes indicate V1 may not anchor to chain. Gateway operates as authoritative source initially.
User: [Opens Vana Desktop App for first time]
App: "Welcome to Vana. Sign in to get started."
[Sign in with Google] [Sign in with Email]
User: [Clicks "Sign in with Google"]
App: [Passport popup opens]
User: [Authenticates with Google]
App: "Creating your secure identity..."
[Background: Passport creates embedded wallet 0xABC...]
[Background: App registers personal server at server.vana.com/u/abc123]
[Background: Gateway records personal server registration]
App: "You're all set! Your Vana address is 0xABC..."
"Connect your first data source:"
[Instagram] [ChatGPT] [YouTube] [Gmail]
User: [Clicks "Instagram"]
App: [Opens embedded browser to instagram.com]
User: [Logs into Instagram with their credentials]
App: [Scrapes profile, posts, likes - using user's session, user's IP]
"Collecting your Instagram data..."
[Background: Personal Server stores data at ~/.vana/data/instagram/]
[Background: Personal Server encrypts data before upload to storage backend]
[Background: Personal Server uploads encrypted blob to storage backend]
[Background: Personal Server registers file record in DataRegistry via Gateway]
App: "Instagram connected ✓"
"You have 1,234 posts, 567 likes, 89 followers"
[View Data] [Connect Another Source]
Builder: [Registers as Builder via Desktop App; appUrl + public key on-chain]
[Hosts manifest and links it via <link rel="manifest"> on https://myapp.com]
Builder: [Implements "Connect data" button using @opendatalabs/connect]
User: [Visits myapp.com]
[Clicks "Connect data"]
Builder backend: [POST /v1/session/init to Session Relay (signed)]
Builder popup: [Displays deep link]
User: [Opens Desktop App via deep link]
Desktop App: [Fetches session details, verifies manifest, prompts consent]
User: [Clicks "Allow"]
Desktop App: [Creates grant:]
- Signs EIP-712 grant
- Submits to Gateway (grant → on-chain permissionId)
- Submits signed grant + permissionId to Session Relay
Builder: [Receives grant payload via poll/webhook]
[grantId = permissionId, user address]
Builder: [Calls Gateway: GET /v1/grants/{grantId}]
[Receives: grant details]
Builder: [Calls Gateway: GET /v1/servers/{userAddress}]
[Receives: Personal Server URL]
Builder: [Calls Personal Server: GET /data/instagram/profile
with Authorization: Web3Signed ... (payload includes grantId)]
[Receives: decrypted JSON]
[Now has user's Instagram profile data]
Builder: [Shows personalized experience to user]
| SMTP | DP | Notes |
|---|---|---|
| RFC 5321 | This spec | Protocol definition |
| Mail User Agent (MUA) | Data Portability Client | User-facing software |
| Mail Transfer Agent (MTA) | Personal Server | Protocol participant that routes/stores |
| Mail Delivery Agent (MDA) | Storage Backend | Final storage location |
| SMTP Server | Data Portability RPC | Service endpoint |
| Email address (user@domain) | Wallet address + Personal Server URL | User identifier + routing |
| Email message | Data File | Unit of data |
| SMTP EHLO | Personal Server registration | Establish identity |
| SMTP MAIL FROM | Grant creation | Authorize data flow |
| SMTP RCPT TO | Builder address | Recipient |
| SMTP DATA | Data file upload | Content transfer |
| SMTP QUIT | Grant revocation | Terminate authorization |
| Mailbox | Scope | Data category |
| Spam filter | Grant approval | User control over access |
| Bounce message | Error response | Failure notification |
ODL Cloud uses Sprites.dev (by Fly.io) for per-user stateful MicroVMs. Key characteristics:
| Feature | Benefit for Personal Server |
|---|---|
| Firecracker MicroVMs | Hardware-level isolation per user |
| Stateful storage | User data persists between activations |
| Checkpoint/restore | Fast cold starts (~300ms VM boot) |
| HTTP auto-activation | Requests wake sleeping VMs automatically |
| Pay-per-use billing | Cost scales with actual usage |
| Up to 8 CPU, 16GB RAM | Sufficient for local LLM inference |
Based on sprites.dev pricing (as of 2026):
| Component | Price | Typical Usage |
|---|---|---|
| CPU Time | $0.07/CPU-hour | ~$0.02-0.10/user/month |
| Memory | $0.04375/GB-hour | ~$0.05-0.20/user/month |
| Hot Storage | $0.000683/GB-hour | ~$0.01/user/month |
| Cold Storage | $0.000027/GB-hour | ~$0.02/user/month |
Estimated monthly cost per user:
| Usage Pattern | Monthly Cost |
|---|---|
| Light (few builder requests/month) | ~$0.10-0.50 |
| Medium (daily builder access) | ~$0.50-2.00 |
| Heavy (continuous access) | ~$2.00-10.00 |
USER DESKTOP APP ODL CLOUD API SPRITES
│ │ │ │
│ 1. Enable │ │ │
│ "ODL Cloud" │ │ │
│─────────────────────────▶│ │ │
│ │ │ │
│ 2. Sign master │ │ │
│ key message │ │ │
│◀─────────────────────────│ │ │
│ │ │ │
│ 3. Signature │ │ │
│─────────────────────────▶│ │ │
│ │ │ │
│ │ 4. Create Sprite │ │
│ │ request │ │
│ │──────────────────────▶│ │
│ │ │ │
│ │ │ 5. Provision │
│ │ │ Sprite │
│ │ │──────────────────────▶│
│ │ │ │
│ │ │ 6. Sprite URL │
│ │ │◀──────────────────────│
│ │ │ │
│ │ 7. Store encrypted │ │
│ │ signature in │ │
│ │ Sprite storage │ │
│ │──────────────────────▶│ │
│ │ │ │
│ │ 8. Sync all files │ │
│ │ to Sprite │ │
│ │──────────────────────▶│ │
│ │ │ │
│ │ 9. Register Sprite │ │
│ │ URL in │ │
│ │ DataPortabilityServers │ │
│ │──────────────────────▶│ │
│ │ │ │
│ 10. "ODL Cloud │ │ │
│ enabled!" │ │ │
│◀─────────────────────────│ │ │
│ │ │ │
BUILDER CLOUDFLARE SPRITES PERSONAL SERVER
│ │ │ │
│ 1. GET /data │ │ │
│──────────────────────────▶ │ │
│ │ │ │
│ │ 2. Route to Sprite │ │
│ │──────────────────────▶│ │
│ │ │ │
│ │ │ 3. Sprite inactive │
│ │ │ → assign compute │
│ │ │ (~300ms) │
│ │ │ │
│ │ │ 4. Boot VM with │
│ │ │ persisted storage │
│ │ │ │
│ │ │ 5. Start Personal │
│ │ │ Server process │
│ │ │──────────────────────▶│
│ │ │ │
│ │ │ │ 6. Read encrypted
│ │ │ │ signature
│ │ │ │
│ │ │ │ 7. Derive master
│ │ │ │ key
│ │ │ │
│ │ │ │ 8. Decrypt data
│ │ │ │ files
│ │ │ │
│ │ │ 9. Proxy request │
│ │ │ to port 8080 │
│ │ │──────────────────────▶│
│ │ │ │
│ │ │ │ 10. Validate grant
│ │ │ │
│ │ │ │ 11. Serve response
│ │ │◀──────────────────────│
│ │ │ │
│ 12. Response │ │ │
│◀─────────────────────────────────────────────────│ │
│ │ │ │
│ │ │ 13. Idle timeout │
│ │ │ → sleep Sprite │
│ │ │ (billing stops) │
│ │ │ │
Total cold start latency: ~1-2 seconds (acceptable for API calls)
| Concern | Mitigation |
|---|---|
| Vana sees user data | Sprites isolated per-user; Vana cannot access Sprite internals |
| Signature stored in Sprite | Encrypted with Sprite-specific key; only decryptable within Sprite |
| Sprite compromise | Hardware isolation (Firecracker); data encrypted at rest in storage backend |
| Sprite deletion | Data remains in storage backend; user can re-provision anytime |
| Version | Date | Changes |
|---|---|---|
| 0.5.0-draft | 2026-02-09 | Added POST /v1/grants endpoint, added secret parameter to Session Relay claim/approve/deny, updated connect flow to show Personal Server grant signing, renamed @vana/connect to @opendatalabs/connect with multi-entrypoint package design |
| 0.4.0-draft | 2026-01-27 | Renaming "Sign in with Vana" to "Connect data" and "Vana Cloud" to "ODL Cloud" (temporary name) |
| 0.3.0-draft | 2026-01-26 | Standardized Session Relay flow, builder reads from Personal Server (decrypted), added versioned /data reads, clarified storage backend bulk sync, and documented builder registration steps |
| 0.2.0-draft | 2026-01-22 | Added Personal Server deployment details, sync model, MCP integration, and ODL Cloud reference architecture |
| 0.1.0-draft | 2026-01-21 | Initial draft |
End of Specification