name: vana-data-app description: > Create and customize Vana data apps that access users' portable personal data via the Vana Connect SDK. Use when asked to "create a data app", "new vana app", "build an app with user data", "customize the starter", "add a scope", "change data source", or when working with @opendatalabs/connect or the vana-connect-starter repo.
Vana Data App Builder
Build Next.js applications that access users' portable personal data (ChatGPT conversations,
Instagram posts, Spotify history, etc.) via the Vana Connect protocol. Apps use the
@opendatalabs/connect SDK for session creation, grant polling, and data retrieval.
Core Architecture
The data flow has three steps, all handled by the SDK:
- Create session (server) —
connect(config)returns aconnectUrldeep link +sessionId - Poll for approval (client) —
useVanaData()hook polls until the user approves in DataConnect - Fetch data (server) —
getData({ privateKey, grant, environment })retrieves user data
The grant flow: Builder backend creates session via Session Relay, user approves in DataConnect desktop app, DataConnect registers an on-chain grant (EIP-712 signed), builder fetches data from the user's Personal Server using the grant.
File Roles — What to Change vs. Leave Alone
CUSTOMIZE these files:
| File | What to change |
|---|---|
.env.local |
Set VANA_SCOPES (comma-separated scope keys) |
src/app/manifest.json/route.ts |
App name, short_name, privacy/terms/support URLs |
src/components/ConnectFlow.tsx |
UI — redesign data display, add visualizations, features |
src/app/page.tsx |
Homepage — title, description, branding, layout |
src/app/globals.css |
Styling — colors, typography, components |
src/app/layout.tsx |
Metadata — page title, description |
src/app/icon.svg |
App icon (DataConnect resolves /icon.svg → /icon.png → /favicon.ico) |
DO NOT modify these files:
| File | Why |
|---|---|
src/app/api/connect/route.ts |
Thin SDK wrapper — works as-is |
src/app/api/data/route.ts |
Thin SDK wrapper — works as-is |
src/app/api/webhook/route.ts |
Stub — extend for production but don't break the interface |
Workflow
Step 1 — Define the app concept
Ask the user (if not clear):
- What does the app do with the data? (analyze, visualize, export, transform)
- Which data sources? (chatgpt, instagram, spotify, gmail, etc.)
- What scopes are needed? (e.g.
chatgpt.conversations,instagram.posts)
Available scope schemas: https://github.com/vana-com/data-connectors/tree/main/schemas
Scope key rule: use the schema filename without .json (for example spotify.savedTracks.json -> spotify.savedTracks).
Step 2 — Scaffold or clone the starter
If starting fresh:
git clone https://github.com/vana-com/vana-connect-starter.git my-data-app
cd my-data-app
pnpm install
cp .env.local.example .env.local
Required environment variables:
VANA_PRIVATE_KEY— App private key (get it from https://account.vana.org/admin)APP_URL—http://localhost:3001for dev, HTTPS domain for production (no trailing slash)VANA_SCOPES— Comma-separated scope keys, e.g.chatgpt.conversations,instagram.posts
Step 3 — Configure scopes
Set scopes in .env.local:
VANA_SCOPES=chatgpt.conversations
# or
VANA_SCOPES=chatgpt.conversations,instagram.posts
src/config.ts reads VANA_SCOPES and validates required env vars (VANA_PRIVATE_KEY, APP_URL, VANA_SCOPES) at startup:
import { createVanaConfig } from "@opendatalabs/connect/server";
const scopes = (process.env.VANA_SCOPES ?? "")
.split(",")
.map((scope) => scope.trim())
.filter(Boolean);
const appUrl = (process.env.APP_URL ?? "").trim().replace(/\/+$/, "");
const privateKey = (process.env.VANA_PRIVATE_KEY ?? "").trim();
if (scopes.length === 0) {
throw new Error("Missing VANA_SCOPES");
}
if (!appUrl) {
throw new Error("Missing APP_URL");
}
if (!privateKey || !/^0x[0-9a-fA-F]{64}$/.test(privateKey)) {
throw new Error("Invalid VANA_PRIVATE_KEY");
}
export const config = createVanaConfig({
privateKey: privateKey as `0x${string}`,
scopes,
appUrl,
});
Step 4 — Update app identity
Edit src/app/manifest.json/route.ts — change the manifest object:
const manifest = {
name: "Your App Name",
short_name: "YourApp",
start_url: "/",
display: "standalone",
background_color: "#09090b",
theme_color: "#09090b",
icons: [{ src: "/icon.svg", sizes: "any", type: "image/svg+xml" }],
vana: vanaBlock,
};
Update the URLs passed to signVanaManifest():
privacyPolicyUrltermsUrlsupportUrlwebhookUrl
Step 5 — Build the UI
The ConnectFlow component uses useVanaData() from @opendatalabs/connect/react:
const {
status, // "idle" | "connecting" | "waiting" | "approved" | "denied" | "expired" | "error"
grant, // { grantId, userAddress, builderAddress, scopes, serverAddress? }
data, // Fetched user data (Record<string, unknown>)
error, // Error message string
connectUrl, // Deep link URL to DataConnect
initConnect, // Start session
fetchData, // Retrieve data using grant
isLoading, // Loading state boolean
} = useVanaData();
Status flow: idle → connecting → waiting → approved → (fetch data)
The component must:
- Call
initConnect()once on mount (useuseRefguard in StrictMode) - Show "Connect with Vana" link to
connectUrlwhen status iswaiting - Call
fetchData()after approval to get user data - Display the data in your app-specific way
IMPORTANT — Actual response shape from useVanaData().data:
The /api/data route returns { data: ... } and the hook exposes the full response body.
Each scope value is NOT a bare array — it's an envelope with schema metadata and a nested data object:
{
"data": { // from /api/data response
"chatgpt.conversations": { // scope key
"$schema": "https://...schema.json",
"version": "1.0",
"scope": "chatgpt.conversations",
"collectedAt": "2026-03-01T20:26:46Z",
"data": { // actual payload
"conversations": [...]
}
}
}
}
To extract conversations: data.data["chatgpt.conversations"].data.conversations
Timestamps like create_time may be ISO 8601 strings (e.g. "2025-12-31T07:59:14.849720Z")
rather than Unix timestamps. Always handle both formats when parsing dates.
Step 6 — Test locally
pnpm dev # Opens on http://localhost:3001
E2E flow:
- Open http://localhost:3001
- Click "Connect with Vana"
- Approve in DataConnect desktop app
- Click "Fetch Data"
SDK Reference
Server imports (@opendatalabs/connect/server)
import { createVanaConfig, connect, getData, signVanaManifest } from "@opendatalabs/connect/server";
createVanaConfig({ privateKey, scopes, appUrl })— Creates config objectconnect(config)— Creates session, returns{ sessionId, connectUrl, expiresAt }getData({ privateKey, grant, environment })— Fetches data from Personal ServersignVanaManifest({ privateKey, appUrl, ... })— Signs manifest for identity verification
Client imports (@opendatalabs/connect/react)
import { useVanaData } from "@opendatalabs/connect/react";
Core imports (@opendatalabs/connect/core)
import { ConnectError, isValidGrant } from "@opendatalabs/connect/core";
import type { ConnectionStatus } from "@opendatalabs/connect/core";
Tech Stack
- Framework: Next.js 15 (App Router, React 19)
- Package manager: pnpm (required, not npm)
- TypeScript: ~5.7, strict mode, ES2022 target
- SDK:
@opendatalabs/connect^0.8.1 - Crypto:
viem^2.0.0 (Ethereum utilities) - Path alias:
@/*→./src/* - Dev server port: 3001
Common Gotchas
.env.localoverrides.env— Next.js loads.env.localwith higher priority. If.env.localhas placeholder values likeVANA_PRIVATE_KEY=0x..., they override real values in.env. Use only one env file, or ensure.env.localhas the real key.API errors are swallowed — The catch blocks in API routes return generic messages. Always include
console.errorlogging to see the actual error in the terminal.Data is deeply nested — The response from
getData()wraps each scope in an envelope with$schema,version,collectedAt, and a nesteddataobject containing the actual payload. Don't assumescope_key → array— it'sscope_key → envelope → data → array.Timestamps are ISO strings — Connector schemas return
create_timeas ISO 8601 strings (e.g."2025-12-31T07:59:14Z"), not Unix timestamps. Parse withnew Date(value), notparseFloat(value).
What NOT to Do
- Do NOT modify the API route handlers — they are thin SDK wrappers that work correctly as-is
- Do NOT use npm — this project requires pnpm
- Do NOT expose
VANA_PRIVATE_KEYto the client — all signing happens server-side - Do NOT hardcode environment URLs — the SDK resolves environments automatically
- Do NOT skip the manifest — DataConnect uses it to verify your app's identity
- Do NOT add authentication to the connect/data API routes — the SDK handles auth via EIP-191/712
- Do NOT call
initConnect()without auseRefguard — React StrictMode will double-fire it