Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/nextjs-starter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Next.js starter

on:
push:
branches: [main, develop]
paths:
- "examples/nextjs-starter/**"
- "test/examples/nextjs-starter.test.ts"
- "vitest.config.ts"
- "src/server/**"
- "src/core/**"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "tsconfig*.json"
- ".github/workflows/nextjs-starter.yml"
pull_request:
branches: [main, develop]
paths:
- "examples/nextjs-starter/**"
- "test/examples/nextjs-starter.test.ts"
- "vitest.config.ts"
- "src/server/**"
- "src/core/**"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "tsconfig*.json"
- ".github/workflows/nextjs-starter.yml"

concurrency:
group: nextjs-starter-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
APP_URL: http://localhost:3001
VANA_ENV: dev
# Public Hardhat fixture identity. No hosted requests run during build.
VANA_PRIVATE_KEY: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test test/examples/nextjs-starter.test.ts
- run: pnpm --filter nextjs-starter build
286 changes: 63 additions & 223 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ their Personal Server, and it drives the builder side of the protocol so an
app or an agent can ask for that data, read it, and pay for it from a
terminal.

This repository also ships the JavaScript SDK the CLI is built on, which is
documented further down.
This repository includes a Next.js starter that consumes the canonical
`@opendatalabs/vana-sdk` Direct integration. Legacy JavaScript connection
exports remain available for compatibility.

## Install

Expand Down Expand Up @@ -116,252 +117,91 @@ vana mcp approve <connection-id> [--scopes a,b] # share all or some of it
vana mcp deny <connection-id>
```

## The SDK
## Web app integration

The same package is also a JavaScript SDK for apps that ask their users for
data from a web page. Which one to use:
For a web app whose users approve data access in Vana, use the Direct integration from `@opendatalabs/vana-sdk`. For terminal scripts and agents, use `vana app request` and `vana app read` above.

- **A web app** whose users approve access in the browser: use the SDK below.
- **A script, a backend job or an agent** working from a terminal: use
`vana app request` and `vana app read` above. They cover the same consent
and add escrow payment, derived answers and exit codes an agent can
branch on.
The [Next.js starter](./examples/nextjs-starter/README.md) includes the complete browser and server flow with SDK 4.3.1. It replaces the starter's session-only Account link, which current Account rejects with `client_id: Required`.

### What problem this solves
### Server

Your users already have rich personal data (ChatGPT conversations, Instagram activity, Gmail, purchase history), but it's locked inside the platforms that collected it. As a builder, you can't easily use that data to personalize onboarding, tailor recommendations, or skip lengthy signup forms.

**Data portability** means users can export their data from these platforms and grant your app scoped access to it, with their explicit consent, cryptographic verification, and full control over what's shared and when to revoke it.

Today, getting access to user data means asking for manual file uploads (high friction), scraping on their behalf (fragile and legally risky), or negotiating enterprise API deals (slow and expensive). This SDK gives you a standardized way to request and receive personal data through Vana's [Data Portability Protocol](https://docs.vana.org/), handling session creation, grant verification, and data fetching in three function calls.

### How it works

```
Your App Vana Protocol
------------------------------ ------------------------------

1. connect({ scopes })
creates a session
returns a connect URL --> 2. User opens the Vana app,
reviews scopes, exports data,
approves the grant

3. Poll resolves with grant <-- Grant signed and registered

4. getData({ grant }) --> 5. Personal Server returns
structured JSON user data over TLS
```

The [Data Portability Protocol](https://docs.vana.org/) defines how users collect data from platforms, store it under their control (on-device or hosted), and grant third-party apps scoped access. This SDK handles session creation, cryptographic request signing, polling, and data fetching. You write three function calls; the protocol handles the rest.

### Try the example app

`examples/nextjs-starter` is a complete working app wired to the development
environment:

```bash
git clone https://github.com/vana-com/vana-cli.git
cd vana-cli/examples/nextjs-starter
cp .env.local.example .env.local
```

Use the pre-registered dev key in .env.local. Note that this private key is ONLY for testing and works only with a testing Vana environment.

```
VANA_PRIVATE_KEY=0x3c05ac1a00546bc0b1b8d3a11fb908409005fac3f26d25f70711e4f632e720d3
APP_URL=http://localhost:3001
```

Install and run:

```bash
pnpm install
pnpm dev
```

The example's own [README](./examples/nextjs-starter/README.md) walks
through approving a request as the end user.

### Add it to your app

#### Installation

```bash
npm install vana-cli
```

#### Prerequisites

First, register your app in the [Developer Portal](https://vana-developers.replit.app/). You will need to provide the URL where your app will be deployed, and then be given a private key after registration.

#### 1. Create a session (server)
Install the SDK with `pnpm add @opendatalabs/vana-sdk@4.3.1`. Keep its controller in a server-only module.

```typescript
import { connect } from "vana-cli/server";

const session = await connect({
privateKey: process.env.VANA_APP_PRIVATE_KEY as `0x${string}`,
import {
createDirectDataController,
PaymentRequiredError,
} from "@opendatalabs/vana-sdk/server";

const vana = createDirectDataController({
appPrivateKey: process.env.VANA_APP_PRIVATE_KEY!,
app: { id: "your-app", name: "Your App", homepageUrl: "https://yourapp.com" },
source: "chatgpt",
scopes: ["chatgpt.conversations"],
webhookUrl: "https://yourapp.com/api/webhook", // optional, data can be pushed to a web hook after a grant is approved
appUserId: "yourapp-user-42", // optional: correlate your app user with the data they provided
env: "production",
personalServerFetch: async (url, init) => {
const response = await fetch(url, init);
if (response.status === 402) {
throw new PaymentRequiredError(
"Payment required. No payment authorized.",
);
}
return response;
},
personalServerTransportRetry: { attempts: 1 },
});

// Return to your frontend:
// session.sessionId - used for polling
// session.connectUrl - where the user reviews and approves the request
// session.expiresAt - ISO 8601 expiration
```

#### 2. Poll for user approval (client)

```tsx
import { useVanaConnect } from "vana-cli/react";

function ConnectData({ sessionId }: { sessionId: string }) {
const { connect, status, grant, connectUrl } = useVanaConnect();

useEffect(() => {
connect({ sessionId });
}, [sessionId]);

if (status === "waiting" && connectUrl) {
return <a href={connectUrl}>Connect your data</a>;
}
if (status === "approved" && grant) {
// grant.grantId, grant.userAddress, grant.scopes are available
return <p>Connected.</p>;
}
return <p>{status}</p>;
}
```

Or use the pre-built button:

```tsx
import { ConnectButton } from "vana-cli/react";

<ConnectButton
sessionId={sessionId}
onComplete={(grant) => saveGrant(grant)}
onError={(err) => console.error(err)}
/>;
```

#### 3. Fetch user data (server)

```typescript
import { getData } from "vana-cli/server";

const data = await getData({
privateKey: process.env.VANA_APP_PRIVATE_KEY as `0x${string}`,
grant, // GrantPayload from step 2
// Expose these through your own authenticated server routes.
const request = await vana.createAccessRequest({
returnUrl: "https://yourapp.com",
});

// Record<string, unknown> keyed by scope
const conversations = data["chatgpt.conversations"];
```

#### Web App Manifest

The Vana app verifies your identity by fetching your manifest. Use `signVanaManifest()` to generate it:

```typescript
import { signVanaManifest } from "vana-cli/server";

// In your manifest route handler (e.g. Next.js /manifest.json/route.ts):
const vanaBlock = await signVanaManifest({
privateKey: process.env.VANA_APP_PRIVATE_KEY as `0x${string}`,
appUrl: "https://yourapp.com",
privacyPolicyUrl: "https://yourapp.com/privacy",
termsUrl: "https://yourapp.com/terms",
supportUrl: "https://yourapp.com/support",
webhookUrl: "https://yourapp.com/api/webhook",
const status = await vana.getAccessRequestStatus(request.requestId);
const result = await vana.readApprovedData({
requestId: request.requestId,
scope: "chatgpt.conversations",
});

const manifest = {
name: "Your App",
short_name: "YourApp",
start_url: "/",
display: "standalone",
vana: vanaBlock, // signed identity block
};
```

Make sure your HTML includes `<link rel="manifest" href="/manifest.json">`.

### API Reference

#### Entrypoints

| Import | Environment | Exports |
| --------------------- | ----------- | ------------------------------------------------------------------- |
| `vana-cli/server` | Node.js | `connect()`, `getData()`, `signVanaManifest()`, low-level clients |
| `vana-cli/react` | Browser | `useVanaConnect()`, `useVanaData()`, `ConnectButton` |
| `vana-cli/core` | Universal | Types, `ConnectError`, constants |
| `vana-cli/runtime` | Node.js | `ManagedPlaywrightRuntime`, the browser runtime `vana connect` uses |
| `vana-cli/connectors` | Node.js | `listAvailableSources()` and the connector catalog |
| `vana-cli/cli` | Node.js | `runCli()`, to run the CLI in-process |

`runtime` and `connectors` are for app surfaces such as the Vana desktop
app that collect data themselves; prefer them over shelling out to the CLI.

#### `connect(config): Promise<SessionInitResult>`

Creates a session on the Session Relay. Returns `sessionId`, `connectUrl`, and `expiresAt`.

| Param | Type | Required | Description |
| ------------ | ------------------- | -------- | ---------------------------------------------------------------------- |
| `privateKey` | `` `0x${string}` `` | Yes | Builder private key |
| `scopes` | `string[]` | Yes | Data scopes to request |
| `webhookUrl` | `string` | No | Public HTTPS URL for grant event notifications (localhost is rejected) |
| `appUserId` | `string` | No | Your app's user ID for correlation |

#### `getData(config): Promise<Record<string, unknown>>`
Register the app identity on the selected network before requesting data. Keep app keys and scope selection on the server. The payment policy above stops every 402 before the SDK can sign a payment challenge. The starter applies this policy too.

Fetches user data from their Personal Server using a signed grant.
### Browser

| Param | Type | Required | Description |
| ------------ | ------------------- | -------- | ---------------------------- |
| `privateKey` | `` `0x${string}` `` | Yes | Builder private key |
| `grant` | `GrantPayload` | Yes | Grant from the approval step |

#### `useVanaConnect(config?): UseVanaConnectResult`
```tsx
import { useDirectVanaConnect } from "@opendatalabs/vana-sdk/react";

React hook that polls the Session Relay and manages connection state.
// Implement these transports with checked responses from your own routes.
const { state, start, retryRead, reset } = useDirectVanaConnect({
createRequest,
getStatus,
readResult,
});

```typescript
const { connect, status, grant, error, connectUrl, reset } = useVanaConnect();
// Call start directly from a user click so the SDK can open the approval tab.
<button onClick={start}>Connect with Vana</button>;
```

`status` transitions: `idle`, then `connecting`, then `waiting`, then one of `approved`, `denied`, `expired` or `error`.
Render `state.type` and the SDK's request/result values. Handle blocked popups with `request.approvalUrl`, mobile `ready_to_open` with `mobileContinuationUrl`, and errors with `retryRead` or `reset`. After approval, the hook automatically reads data. See the starter for the complete state rendering and HTTP handling.

#### `GrantPayload`
SDK 4.3.1's controller reads Personal Server delivery and attempts consumer acknowledgement after the read. Acknowledgement is best effort. A completed request is terminal. The starter documents these limits and never treats a browser-supplied grant or server URL as read authority.

Returned when a user approves access:
### Legacy library exports

```typescript
interface GrantPayload {
grantId: string; // on-chain permission ID
userAddress: string; // user's wallet address
builderAddress: string; // your registered address
scopes: string[]; // approved data scopes
serverAddress?: string; // user's Personal Server
appUserId?: string; // your app's user ID (if provided)
}
```
The existing `vana-cli/server`, `vana-cli/react`, and `vana-cli/core` exports remain available for compatibility. `connect()`, `useVanaConnect()`, `useVanaData()`, and `ConnectButton` use Session Relay. Their session-only Account URLs do not satisfy current Account's OAuth client requirements. Migrate browser approval integrations to the SDK's Direct controller and React hook above. This change does not remove those public exports or alter the terminal commands.

#### Low-level clients
`getData()` retains its grant-based server contract. New Direct integrations use `readApprovedData({ requestId, scope })` so the server rechecks the approved Direct request before reading. `signVanaManifest()` retains the signed manifest contract used by the starter. The starter's webhook remains a stub.

For full control over individual protocol interactions:
| Import | Environment | Purpose |
| ------------------------------- | ----------- | ---------------------------------------------------------- |
| `@opendatalabs/vana-sdk/server` | Node.js | Direct controller, request/status/result types, and errors |
| `@opendatalabs/vana-sdk/react` | Browser | `useDirectVanaConnect()` and Direct flow states |
| `vana-cli/server` | Node.js | Legacy Session Relay, grant reads, and signed manifests |
| `vana-cli/react` | Browser | Legacy Session Relay hooks and `ConnectButton` |
| `vana-cli/core` | Universal | Legacy connection types, errors, and constants |
| `vana-cli/runtime` | Node.js | `ManagedPlaywrightRuntime`, used by `vana connect` |
| `vana-cli/connectors` | Node.js | Connector catalog |
| `vana-cli/cli` | Node.js | In-process CLI entry point |

```typescript
import {
createRequestSigner, // Web3Signed header generation
createSessionRelay, // Session Relay HTTP client
createDataClient, // Data Gateway HTTP client
} from "vana-cli/server";
```
`runtime` and `connectors` are for apps that collect data themselves, including Vana Desktop.

## Connectors

Expand Down
10 changes: 6 additions & 4 deletions examples/nextjs-starter/.env.local.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
VANA_PRIVATE_KEY=0x... # Builder key, must be registered on-chain
APP_URL=http://localhost:3001 # Public URL of your deployed app
VANA_SCOPES=chatgpt.conversations # Comma-separated scopes
# Server-only app key. Register the app identity on the selected network.
VANA_PRIVATE_KEY=0x...
# VANA_APP_PRIVATE_KEY is also accepted when VANA_PRIVATE_KEY is unset.
APP_URL=http://localhost:3001
# dev uses Moksha and app-dev.vana.org. prod uses mainnet and app.vana.org.
VANA_ENV=dev
NEXT_PUBLIC_VANA_ENV=dev
# Source and scopes are configured in src/config.ts.
Loading
Loading