@opendatalabs/connect-js
JavaScript SDK for launching Open Data Labs Connect and reading the selected
data from the user's Personal Server.
The v2 flow uses the same trust split as Plaid Link or Stripe Connect:
- your backend holds the Open Data Labs API key
- your backend chooses the source and scopes
- your frontend opens Vana in a second tab and keeps your app tab alive
- after the user approves, your backend reads the approved scopes from the
user's Personal Server
Installation
npm install @opendatalabs/connect-js
Backend
import { createConnectController } from "@opendatalabs/connect-js/server";
export const odl = createConnectController({
apiBaseUrl: process.env.ODL_API_BASE_URL!,
apiKey: process.env.ODL_API_KEY!,
appId: process.env.ODL_APP_ID!,
defaultOrigin: process.env.APP_URL ?? "https://your-app.com",
source: "instagram",
scopes: ["read:profile", "read:posts"],
});
import { odl } from "@/lib/odl";
export async function POST() {
return Response.json(
await odl.createConnectSession({
redirectUrl: `${process.env.APP_URL ?? "https://your-app.com"}/connect/return`,
})
);
}
import { odl } from "@/lib/odl";
export async function GET(request: Request) {
const connectionId = new URL(request.url).searchParams.get("connectionId");
if (!connectionId) {
return Response.json({ error: "Missing connectionId" }, { status: 400 });
}
return Response.json(await odl.getStatus(connectionId));
}
import { odl } from "@/lib/odl";
export async function GET(request: Request) {
const connectionId = new URL(request.url).searchParams.get("connectionId");
if (!connectionId) {
return Response.json({ error: "Missing connectionId" }, { status: 400 });
}
return Response.json(await odl.readAllWhenReady(connectionId));
}
React
import { useTwoTabConnect } from "@opendatalabs/connect-js/react";
export function ConnectButton() {
const connect = useTwoTabConnect({
openingUrl: process.env.NEXT_PUBLIC_VANA_CONNECT_OPENING_URL,
createSession: async () => {
const res = await fetch("/api/connect/session", { method: "POST" });
if (!res.ok) throw new Error("Failed to create Connect session");
return res.json();
},
getStatus: async (connectionId) => {
const res = await fetch(
`/api/connect/status?connectionId=${encodeURIComponent(connectionId)}`
);
if (!res.ok) throw new Error("Failed to fetch Connect status");
return res.json();
},
readResult: async (connectionId) => {
const res = await fetch(
`/api/connect/data?connectionId=${encodeURIComponent(connectionId)}`
);
if (!res.ok) throw new Error("Failed to read Connect data");
return res.json();
},
});
return (
<button disabled={connect.state.type !== "idle"} onClick={() => connect.start()} type="button">
{connect.state.type === "idle" ? "Connect data" : "Connecting..."}
</button>
);
}
useTwoTabConnect opens a Vana-hosted holding page synchronously inside the
click event, then redirects that tab to the Connect approval page after your
backend creates the session. This avoids popup blockers without showing users a
blank about:blank tab.
The default holding page is https://app.vana.org/connect/opening. For dev or
staging, set NEXT_PUBLIC_VANA_CONNECT_OPENING_URL to the matching Vana app,
for example https://app-dev.vana.org/connect/opening.
Source IDs
Use stable source IDs such as instagram or icloud_notes, not display names
such as "Instagram" or "iCloud Notes".
Requirements
- Keep your API key on the server.
- Choose source and scopes on the server.
- Add each app domain in the dashboard before creating sessions for it.
- Keep the Vana tab open while your app reads from the user's Personal Server.