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

# Next.js

> The three Payman routes on the App Router.

The [quickstart](/quickstart) is written for Express. On Next.js only route registration differs, and `@paymanai/connect-next` does it.

<Steps>
  <Step title="Install">
    ```bash Shell theme={null}
    npm install @paymanai/connect @paymanai/connect-next
    npm install @paymanai/connect-react   # the buttons and hooks, if you want them
    ```
  </Step>

  <Step title="Create the client and routes once">
    ```ts lib/payman.ts theme={null}
    import { getPaymanConnect, paymanRoutes } from "@paymanai/connect-next";
    import { currentUserId } from "./session";

    export const payman = getPaymanConnect();   // reads PAYMAN_APP_KEY

    export const routes = paymanRoutes(payman, {
      currentUser: () => currentUserId(),
    });
    ```

    Use `getPaymanConnect()`, never module-scope `new PaymanConnect()`. Next compiles every route handler into its own bundle, so a module-level `new` makes one client per route instead of one per process. The symptoms: "not connected" right after a successful consent, and a consent return that hangs.

    `currentUser` receives the inbound `Request`; in App Router you will usually ignore it and call your own session helper, as above.
  </Step>

  <Step title="Re-export GET from three route files">
    ```ts app/payman/callback/route.ts theme={null}
    import { routes } from "@/lib/payman";

    export const runtime = "nodejs";

    export const { GET } = routes.callback;
    ```

    ```ts app/payman/approvals/[ref]/stream/route.ts theme={null}
    import { routes } from "@/lib/payman";

    export const runtime = "nodejs";
    export const dynamic = "force-dynamic";

    export const { GET } = routes.approval;
    ```

    ```ts app/payman/resume/stream/route.ts theme={null}
    import { routes } from "@/lib/payman";

    export const runtime = "nodejs";
    export const dynamic = "force-dynamic";

    export const { GET } = routes.resume;
    ```

    That is the whole server side. Register `http://localhost:3000/payman/callback` as a redirect URI in the developer console, matched byte for byte, port included; `payman.callbackPath()` returns what the SDK expects.

    The startup notice prints once per route file. It counts mounts, not clients, so three notices are correct.
  </Step>
</Steps>

## Write the two settings yourself

Next reads route segment config by static analysis at build time, so a re-exported constant silently does nothing. Write both in each route file:

| Export                      | Where        | Why                                                                                         |
| --------------------------- | ------------ | ------------------------------------------------------------------------------------------- |
| `runtime = "nodejs"`        | all three    | The SDK uses node crypto and a node store. It cannot start on the edge runtime.             |
| `dynamic = "force-dynamic"` | both streams | A cached or statically evaluated SSE route delivers nothing, or everything at the very end. |

## The app key at build time

`next build` evaluates route modules, so the client is constructed during the build and throws without `PAYMAN_APP_KEY`. Give the build step a key, or construct the client lazily. For a CI build with no secrets, a syntactically valid placeholder is enough; it authenticates nothing.

## The client component

```tsx app/Chat.tsx theme={null}
"use client";
import { ConnectButton, ApprovalButton } from "@paymanai/connect-react";
```

```tsx app/layout.tsx theme={null}
import "@paymanai/connect-react/styles.css";
```

`"use client"` is required: these mount listeners and open windows. The components ship no CSS; without the stylesheet the connect control renders as a bare hyperlink. See [styling the components](/build/approvals#styling).

<Note>
  `openConnect()` and `openApproval()` must run synchronously inside the click handler. Any `await` before `window.open` gets the popup blocked and resolves `{ state: "popup_blocked" }`.
</Note>

## Deploying

* **Serverless means many instances.** The default store is in-process, so a connection saved by one instance is missing from the next. Move to `sqlStore()` (`@paymanai/connect/stores/sql`) or `redisStore()` (`@paymanai/connect/stores/redis`) before you ship.
* **Session cookie must be `SameSite=Lax`.** `Strict` works on localhost and drops the cookie on the production consent return; `mount()` prints a reminder, and nothing can check it for you.
* **A pending approval can hold a stream open for up to 16 minutes**, longer than most serverless function limits. The browser reconnects on its own, but on per-invocation billing that is many invocations per approval.

## Writing the routes by hand

`@paymanai/connect-next` needs Next 15. On Next 14, or to skip the dependency, `@paymanai/connect/fetch` inside the core package has the same three handlers; see [any other framework](/frameworks). Keep one process-wide client even here.

```ts lib/payman-handlers.ts theme={null}
import { createPaymanHandlers } from "@paymanai/connect/fetch";
import { payman } from "./payman";
import { currentUserId } from "./session";

export const handlers = createPaymanHandlers(payman, {
  currentUser: () => currentUserId(),
});
```

The callback and resume handlers re-export directly (`export const GET = handlers.callback`). The approval handler does not: wrap it, and type the context exactly.

```ts app/payman/approvals/[ref]/stream/route.ts theme={null}
import { handlers } from "@/lib/payman-handlers";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export function GET(req: Request, ctx: { params: Promise<{ ref: string }> }) {
  return handlers.approval(req, ctx);
}
```

<Note>
  `next build` validates each handler against its generated `RouteContext`. An optional context argument fails with `Expected "RouteContext", got "undefined"`, and a union type is rejected too. Neither check runs under `next dev`; encoding this is half of what `@paymanai/connect-next` exists for.
</Note>
