> ## Documentation Index
> Fetch the complete documentation index at: https://docs.concatflow.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a per-customer assistant with Next.js

> A Next.js app where each logged-in customer asks questions about their own data, and never sees another customer's rows. About 20 minutes.

In this tutorial, you build a small Next.js app with a login page, a dashboard and an assistant. Each customer who logs in gets answers on their own rows only. You will:

* open a session on your server with `@concatflow/node`,
* show the assistant with `<ConcatflowAssistant>`,
* renew the session when it expires,
* check that two customers never see each other's data.

The finished app adds seven files to a new Next.js project.

## Before you start

You need Node 20.9 or later, for Next.js 16. In the [Concatflow dashboard](https://app.concatflow.com), open your assistant and check four things:

* **Isolation** is signed on `customer_id`. If your isolation uses another key, use that key wherever this tutorial says `customer_id`.
* In **Doors**, the **per-user door** is on.
* In **Doors → Allowed origins**, `http://localhost:3000` is listed.
* The assistant is **published**.

You also need your [API key and assistant ID](https://docs.concatflow.com/identifiers.md), and two customer IDs from your database whose data differs.

## 1. Create the app

```bash theme={"system"}
npx create-next-app@latest concatflow-next --yes
cd concatflow-next
npm install @concatflow/node @concatflow/embed server-only
```

`--yes` picks the defaults: TypeScript, the App Router and the `@/*` import alias. This tutorial relies on all three.

## 2. Add your keys

Create `.env.local` at the root of the project:

```bash .env.local theme={"system"}
CONCATFLOW_API_KEY=lyra_sk_YOUR_API_KEY
CONCATFLOW_ASSISTANT_ID=YOUR_ASSISTANT_ID
```

Neither name starts with `NEXT_PUBLIC_`, so Next.js never sends them to the browser. The project's `.gitignore` already leaves `.env*` files out of git.

## 3. Add a stand-in login

The assistant needs to know who is logged in. Your app already has a login. For this tutorial, a cookie holding the customer ID stands in for it.

```ts lib/auth.ts theme={"system"}
import "server-only";
import { cookies } from "next/headers";

// Stand-in for your login. Replace it with your own: Auth.js, Clerk, your sessions.
export async function auth(): Promise<{ customerId: string } | null> {
  const customerId = (await cookies()).get("customer_id")?.value;
  return customerId ? { customerId } : null;
}
```

```ts app/login/actions.ts theme={"system"}
"use server";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

export async function logIn(form: FormData) {
  (await cookies()).set("customer_id", String(form.get("customerId")), { httpOnly: true, sameSite: "lax" });
  redirect("/dashboard");
}

export async function logOut() {
  (await cookies()).delete("customer_id");
  redirect("/login");
}
```

```tsx app/login/page.tsx theme={"system"}
import { logIn } from "./actions";

export default function LoginPage() {
  return (
    <main className="mx-auto w-full max-w-sm p-8">
      <h1 className="mb-4 text-xl font-semibold">Log in</h1>
      <form action={logIn} className="flex gap-2">
        <input name="customerId" placeholder="Customer ID" required className="flex-1 rounded border px-2 py-1" />
        <button className="rounded bg-black px-3 py-1 text-white">Log in</button>
      </form>
    </main>
  );
}
```

> [!WARNING]
>
> Anyone can type any customer ID into this page. That is fine on your laptop, never in production. What matters is the rule the rest of the tutorial follows: the customer comes from `auth()`, on the server, never from the request body.

## 4. Open a session on the server

One function opens a Concatflow session for one customer. `server-only` makes the build fail if a client component ever imports it, so your API key cannot reach the browser.

```ts lib/concatflow.ts theme={"system"}
import "server-only";
import { Concatflow } from "@concatflow/node";

const concatflow = new Concatflow({ apiKey: process.env.CONCATFLOW_API_KEY! });

export function openSession(customerId: string) {
  return concatflow.sessions.create({
    assistantId: process.env.CONCATFLOW_ASSISTANT_ID!,
    partitions: { customer_id: customerId },
  });
}
```

It resolves to `{ token, expiresAt }`. The token is opaque and lasts 15 minutes. The keys of `partitions` must be exactly the keys of the isolation: a missing or extra key fails with `403 partition_keys_mismatch`.

## 5. Add the renewal route

When a token expires, the assistant asks your page for a new one. Your page asks this route.

```ts app/api/concatflow/session/route.ts theme={"system"}
import { ConcatflowError } from "@concatflow/node";
import { auth } from "@/lib/auth";
import { openSession } from "@/lib/concatflow";

export async function POST() {
  const user = await auth();
  if (!user) return Response.json({ error: "logged_out" }, { status: 401 });

  try {
    return Response.json(await openSession(user.customerId));
  } catch (err) {
    if (!(err instanceof ConcatflowError)) throw err;
    console.error("Concatflow:", err.status, err.code);
    return Response.json({ error: err.code ?? "concatflow_error" }, { status: 502 });
  }
}
```

The route answers `401` to a visitor who is not logged in. It never passes Concatflow's error body to the browser: it logs the status and only returns the code.

## 6. Show the assistant

The dashboard is a server component. It opens the first session while it renders, so the assistant gets its token without a round trip.

```tsx app/dashboard/page.tsx theme={"system"}
import { Suspense } from "react";
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
import { openSession } from "@/lib/concatflow";
import { logOut } from "../login/actions";
import { Assistant } from "./assistant";

export default function DashboardPage() {
  return (
    <main className="mx-auto w-full max-w-2xl p-8">
      <Suspense fallback={<p>Loading…</p>}>
        <Dashboard />
      </Suspense>
    </main>
  );
}

async function Dashboard() {
  const user = await auth();
  if (!user) redirect("/login");
  const { token } = await openSession(user.customerId);

  return (
    <>
      <div className="mb-6 flex items-center justify-between">
        <h1 className="text-xl font-semibold">Customer {user.customerId}</h1>
        <form action={logOut}>
          <button className="rounded border px-3 py-1">Log out</button>
        </form>
      </div>
      <Assistant customerId={user.customerId} initialToken={token} />
    </>
  );
}
```

`create-next-app` turns on Cache Components. With them, code that reads the request, such as `cookies()` in `auth()`, must render inside `<Suspense>`. Without it, `next build` fails on this page.

The assistant itself is a client component. It renews its token through the route from step 5.

```tsx app/dashboard/assistant.tsx theme={"system"}
"use client";
import { ConcatflowAssistant } from "@concatflow/embed/react";

async function renew(): Promise<string> {
  const res = await fetch("/api/concatflow/session", { method: "POST" });
  if (!res.ok) throw new Error("Could not open a Concatflow session");
  return (await res.json()).token;
}

export function Assistant({ customerId, initialToken }: { customerId: string; initialToken: string }) {
  return (
    <ConcatflowAssistant
      key={customerId}
      session={initialToken}
      launcher="bar"
      onSessionExpired={renew}
      onError={(err) => console.warn("Concatflow:", err.message)}
    />
  );
}
```

`key={customerId}` gives each customer a fresh assistant. When the customer logs out, the component unmounts and the assistant is destroyed.

## 7. Run it

```bash theme={"system"}
npm run dev
```

Open [http://localhost:3000/login](http://localhost:3000/login) and log in with your first customer ID.

![A login form with a customer ID field and a Log in button](https://mintcdn.com/concatflow/xCmphOUeinyoVnFX/images/tutorials/nextjs-per-customer/login.png?fit=max\&auto=format\&n=xCmphOUeinyoVnFX\&q=85\&s=9a079675422e186d6bab501740f6f95d)

The stand-in login.

The search bar appears under the heading. Ask a question that only this customer's data can answer, such as "What are my account balances?".

![The dashboard of customer 4712 with an answer listing two accounts and their balances](https://mintcdn.com/concatflow/xCmphOUeinyoVnFX/images/tutorials/nextjs-per-customer/customer-a.png?fit=max\&auto=format\&n=xCmphOUeinyoVnFX\&q=85\&s=5e9b76e8e1e76c73b617787a3e03b391)

Customer 4712 sees their two accounts.

> [!TIP]
>
> No bar? Open the browser console. `Concatflow: …` names the problem. A `502` from `/api/concatflow/session` means Concatflow refused the session: the server log shows the status and code. See [Errors](https://docs.concatflow.com/reference/node/errors.md).

## 8. Check the renewal

You do not have to wait 15 minutes. In `app/dashboard/assistant.tsx`, hand the assistant a token Concatflow will refuse:

```diff app/dashboard/assistant.tsx theme={"system"}
-      session={initialToken}
+      session="expired"
```

Reload the dashboard and ask a question. In the **Network** tab of your browser's developer tools, you see one `POST /api/concatflow/session`, and the assistant answers. That is what happens 15 minutes after each page load. Put `session={initialToken}` back.

## 9. Check the isolation

1. Note the answer the first customer got.
2. Click **Log out**, then log in with the second customer ID.
3. Ask the same question.

![The dashboard of customer 4713 with an answer listing one account](https://mintcdn.com/concatflow/xCmphOUeinyoVnFX/images/tutorials/nextjs-per-customer/customer-b.png?fit=max\&auto=format\&n=xCmphOUeinyoVnFX\&q=85\&s=981ddb50cd0026a1f2bf0d021e85faad)

Customer 4713 asks the same question and sees only their own account.

The second customer must see neither the first customer's rows nor their questions. Log in with the first customer ID again: their conversation is still there. Concatflow keeps one conversation per assistant and per customer.

## What you built

| File | Runs on | Does |
| - | - | - |
| `lib/auth.ts` | server | Says who is logged in. Replace it with your login. |
| `app/login/actions.ts`, `app/login/page.tsx` | server | The stand-in login and logout. |
| `lib/concatflow.ts` | server | Opens a session for one customer, with your API key. |
| `app/api/concatflow/session/route.ts` | server | Renews the token for the logged-in customer. |
| `app/dashboard/page.tsx` | server | Opens the first session and renders the dashboard. |
| `app/dashboard/assistant.tsx` | browser | Shows the assistant and renews its token. |

The browser only ever holds the token. Your API key, and the customer ID Concatflow filters on, stay on your server.

## Next steps

> **[Go live](https://docs.concatflow.com/guides/go-live.md)**
>
> Production origins, caps and the last checks.

> **[Erase a customer's conversations](https://docs.concatflow.com/guides/conversations.md)**
>
> Call `conversations.erase` when a customer deletes their account.

> **[Appearance](https://docs.concatflow.com/guides/appearance.md)**
>
> Accent colour, theme and placeholder.

> **[Bar or bubble](https://docs.concatflow.com/guides/bar-or-bubble.md)**
>
> Choose where the assistant lives on the page.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.