Getting started

Quickstart

Add a Connect flow to a Next.js app in about ten minutes. By the end, a button in your app opens the hosted consent flow, your user approves, and your original tab shows their data as JSON.

Signed in? Your test key and app ID are already filled into every snippet on this page. Test mode never bills and never expires.

1. Prerequisites

Create an account and an app in the dashboard. Add http://localhost:3000 to the app's embed origins, then note its API key and public app ID. Scaffold a Next.js app with TypeScript and the App Router, then install the SDK.

$ npm install @opendatalabs/connect-js

2. Environment variables

Put these in .env.local. The API key stays on the server. It is never sent to the browser.

.env.local
ODL_API_BASE_URL=https://api.opendatalabs.com/api/v1
ODL_API_KEY=uu_test_sk_4Kq9…
ODL_APP_ID=odl_app_123
APP_URL=http://localhost:3000
NEXT_PUBLIC_VANA_CONNECT_OPENING_URL=https://app.vana.org/connect/opening

3. Create the Connect controller

Server side. Choose the source and scopes here, not in the browser. Use stable source IDs such as strava or icloud_notes, never display names.

lib/odl.ts
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!,
  source: "strava",
  scopes: ["read:activities"],
});

Strava is rolling out; use oura or instagram to test today.

4. Add three server routes

One starts a session, one checks status, one reads the data once it is ready.

app/api/connect/session/route.ts
import { odl } from "@/lib/odl";

export async function POST() {
  return Response.json(
    await odl.createConnectSession({
      redirectUrl: process.env.APP_URL!,
    })
  );
}
app/api/connect/status/route.ts
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));
}
app/api/connect/data/route.ts
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));
}

5. Add the Connect button

The hook opens a holding page in a second tab during the click, which keeps popup blockers quiet, then sends that tab to the approval page once your server has created the session.

app/connect-button.tsx
import { useTwoTabConnect } from "@opendatalabs/connect-js/react";

export function ConnectButton() {
  const connect = useTwoTabConnect({
    openingUrl: process.env.NEXT_PUBLIC_VANA_CONNECT_OPENING_URL,
    createSession: async () => (await fetch("/api/connect/session", { method: "POST" })).json(),
    getStatus: async (id) => (await fetch(`/api/connect/status?connectionId=${id}`)).json(),
    readResult: async (id) => (await fetch(`/api/connect/data?connectionId=${id}`)).json(),
  });

  return (
    <button disabled={connect.state.type !== "idle"} onClick={() => connect.start()} type="button">
      {connect.state.type === "idle" ? "Connect data" : "Connecting..."}
    </button>
  );
}

6. Run it

Start the dev server, open localhost:3000, click Connect, approve in the second tab, and come back. You are done when the original tab shows JSON for the approved scopes.

// what comes back for strava.activities
[{ "id": "a_1180234", "activity_type": "Run", "start_date": "2026-10-25",
   "distance_m": 8042, "moving_time_s": 2411, "average_heartrate": 152,
   "calories_kcal": 611, "freshness": "export" }]

Next