A Personal Server is the user-controlled environment that stores personal data, responds to authorized data requests from builders, and maintains access logs. It is the only protocol component that sees plaintext data.
A Personal Server is a protocol participant — it registers onchain and can act unattended. The Data Portability Client (e.g. Data Connect desktop app) is a client that configures and controls the Personal Server, but is not itself a protocol participant. Think of it like email: the desktop app is your mail client, the Personal Server is your mail server.
Role in the protocol
The Personal Server is responsible for:
- Storing user data in plaintext, organized by scope
- Serving data to builders who hold a valid grant
- Encrypting data before uploading to storage backends
- Syncing data across instances (e.g. desktop + cloud)
- Logging access for auditability
A user can have multiple Personal Server instances (desktop-bundled + cloud) that stay in sync through the storage backend and Data Registry.
Registration
Every Personal Server must be registered onchain via the DataPortabilityServers contract. Registration records the server’s address, public key, and URL. Users then trust a server to act on their behalf.
The Gateway provides signature-based registration so the Desktop App can register a server without the user sending an onchain transaction directly:
See Identity for the full registration flow and the DataPortabilityServers contract interface.
API
The Personal Server exposes a versioned REST API under /v1.
Data endpoints
| Method | Path | Description |
|---|
POST | /v1/data/{scope} | Create a new data document for a scope |
GET | /v1/data | List available scopes and latest version metadata |
GET | /v1/data/{scope} | Read data for a scope (latest or specific version) |
GET | /v1/data/{scope}/versions | List available versions (metadata only) |
DELETE | /v1/data/{scope} | Delete data for a scope (user-only) |
Grant endpoints
| Method | Path | Description |
|---|
GET | /v1/grants | List all grants for this user |
POST | /v1/grants | Create a new grant (user-only) |
POST | /v1/grants/verify | Verify a grant signature |
Other endpoints
| Method | Path | Description |
|---|
GET | /v1/access-logs | Get access log history |
GET | /health | Health check (unversioned) |
Query parameters for GET /v1/data
| Parameter | Description |
|---|
scopePrefix | Filter by scope prefix (e.g. instagram) |
limit | Maximum number of results |
offset | Pagination offset |
Query parameters for GET /v1/data/{scope}
| Parameter | Description |
|---|
fileId | Return a specific version by DataRegistry fileId |
at | Return the version closest to this ISO 8601 timestamp (closest ≤ at) |
If neither parameter is provided, the latest version by collectedAt is returned.
Write flow (POST /v1/data/{scope})
When a data document is written:
- Look up
schemaId for the scope via the Gateway
- Validate the request body against the schema
- Generate a
collectedAt timestamp (UTC)
- Construct the data file envelope with schema URL, version, scope, and timestamp
- Store locally in
~/.vana/data/{scope}/{collectedAt}.json
- Return
201 Created immediately
- Asynchronously: encrypt with the scope key, upload to the storage backend, and register in the DataRegistry
Builder data access
Builders read data from the Personal Server using signed HTTP requests. Every builder request must include an Authorization: Web3Signed header.
Web3Signed authorization
Authorization: Web3Signed <base64url(json)>.<signature>
The payload is a JSON object with keys sorted alphabetically:
{
"aud": "https://user-abc.server.vana.com",
"bodyHash": "",
"exp": 1737500300,
"grantId": "0x...",
"iat": 1737500000,
"method": "GET",
"uri": "/data/instagram.profile"
}
| Field | Description |
|---|
aud | Personal Server origin (must match) |
method | HTTP method of the request |
uri | Request path and query string |
bodyHash | Hash of request body (empty string for GET) |
iat | Issued-at timestamp (Unix seconds) |
exp | Expiration timestamp (Unix seconds) |
grantId | Onchain permissionId — required for raw data reads |
Signing: The JSON is canonicalized (keys sorted alphabetically), then base64url-encoded (no padding). The signature is an EIP-191 signature over the ASCII bytes of the base64url string.
Verification
When the Personal Server receives a builder request, it:
- Recovers the signer address from the
Authorization header
- Verifies the signer is a registered builder onchain
- Checks
aud matches the server’s own origin
- Checks
method and uri match the actual request
- Validates
iat/exp are within the allowed skew window (e.g. 5 minutes)
- For data reads: verifies
grantId exists onchain and the signer matches the grantee
- Confirms the requested scope is within the granted scopes
- Logs the access
Access control summary
| Endpoint | Who can call |
|---|
POST /v1/data/{scope} | User only |
GET /v1/data, GET /v1/data/{scope} | User or builder with valid grant |
GET /v1/data/{scope}/versions | User or builder with valid grant |
DELETE /v1/data/{scope} | User only |
POST /v1/grants, GET /v1/grants | User only |
GET /v1/access-logs | User only |
Hosting options
Personal Servers can be hosted in three ways. All are protocol-equivalent — builders interact with the same API regardless of where the server runs.
| Option | URL format | Operator | Availability | Cold start |
|---|
| Desktop-bundled | Local (tunneled via {userId}.server.vana.org) | User’s device | While app is open | None |
| ODL Cloud | https://server.vana.com/u/{userId} | Vana (Sprites.dev) | Always on (auto-wakes) | ~1-2s |
| Self-hosted | https://server.alice.com | User | User manages | User manages |
Desktop-bundled
The Personal Server runs as an embedded process inside the Data Connect desktop app (Tauri). Data stays on the user’s machine. When the app is closed, the server is unavailable — builder requests return 503.
To make a desktop-bundled server reachable from the internet, Vana provides FRP tunneling (Fast Reverse Proxy). The desktop app connects to proxy.server.vana.org and receives a public URL at {userId}.server.vana.org with automatic TLS.
ODL Cloud
ODL Cloud runs Personal Servers on per-user Firecracker MicroVMs via Sprites.dev. Each user gets an isolated VM that:
- Wakes automatically on incoming requests (~1-2 second cold start)
- Sleeps when idle (billing stops)
- Persists data between activations
Delegated signature. When a user enables ODL Cloud, they sign the master key message once. The signature is encrypted and stored in the Sprite. On activation, the Sprite derives the master key in-memory, decrypts data files, and serves requests — no user interaction required.
The delegated signature never expires. Users revoke it by disabling ODL Cloud, which deletes the Sprite. Data remains in the storage backend and can be re-provisioned.
Self-hosted
Advanced users can run the Personal Server as a Docker container on their own infrastructure. They control the URL, uptime, and security configuration.
Fallback behavior
If both a desktop-bundled and ODL Cloud server are configured, the onchain registration points to the ODL Cloud URL so builders always have a reachable endpoint. When the desktop app is open, it syncs data to the cloud server through the storage backend.
Sync
When a user has multiple Personal Server instances (e.g. desktop + ODL Cloud), they stay synchronized through the storage backend and Data Registry. See Storage & Encryption — Data sync for the full sync model.
Local data hierarchy
All Personal Server implementations use the same directory layout:
~/.vana/
├── data/ # Decrypted user data
│ ├── instagram/
│ │ ├── profile/
│ │ │ └── 2026-01-21T10-00-00Z.json
│ │ ├── posts/
│ │ └── likes/
│ ├── chatgpt/
│ │ └── conversations/
│ └── youtube/
│ ├── watch_history/
│ └── subscriptions/
├── logs/ # Access logs (JSON lines, daily rotation)
│ └── access-2026-01-21.log
├── index.db # Local registry index (SQLite)
└── server.json # Server configuration
Files are named {YYYY-MM-DDTHH-mm-ssZ}.json using the collectedAt timestamp.