@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
The redirect is a navigation and correlation signal, not data delivery. The
browser entry point has no API key. Keep session status and data reads behind
your backend routes.
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));
}
Use ready_for_read as the read gate, not completed. The backend
readAllWhenReady() helper reads every requested scope and completes only after
the final scope drives the Vana acknowledgement. One successful scope read is
not complete. If acknowledgement fails, it raises a retryable typed
ConnectError with kind vana_ack_failed instead of reporting partial success.
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.
- Use
ready_for_read as the read gate. completed follows the reads and
acknowledgement.