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

# Next.js

> Mount Connect.js in a Client Component and exchange auth codes in a server-only route.

Connect.js mounts an iframe and reads browser globals. Initialize the widget in a Client Component, then exchange the auth code in server-only code.

## Client component

```jsx theme={null}
"use client"

import { useEffect, useRef, useState } from "react"

export function LinkMonoAccount({ customerId }) {
  const connectRef = useRef(null)
  const [ready, setReady] = useState(false)

  useEffect(() => {
    let active = true

    async function mountConnect() {
      const { default: Connect } = await import("@mono.co/connect.js")

      if (!active) return

      const connect = new Connect({
        key: process.env.NEXT_PUBLIC_MONO_PUBLIC_KEY,
        scope: "auth",
        data: {
          customer: { id: customerId }
        },
        onLoad: () => setReady(true),
        onSuccess: async ({ code }) => {
          const response = await fetch("/api/mono/exchange", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ code })
          })

          if (!response.ok) {
            throw new Error("Could not complete Mono account linking")
          }
        }
      })

      connect.setup()
      connectRef.current = connect
    }

    mountConnect()

    return () => {
      active = false
      connectRef.current?.close()
      connectRef.current = null
    }
  }, [customerId])

  return (
    <button
      type="button"
      disabled={!ready}
      onClick={() => connectRef.current?.open()}
    >
      {ready ? "Link a financial account" : "Loading account linking…"}
    </button>
  )
}
```

The dynamic import keeps package loading inside the browser lifecycle. `NEXT_PUBLIC_MONO_PUBLIC_KEY` is intentionally public.

## Route handler

The Route Handler exchanges the short-lived code with the secret key. It must also authenticate the application user and store the returned account ID against that user.

```javascript theme={null}
// app/api/mono/exchange/route.js
import { NextResponse } from "next/server"

export async function POST(request) {
  const { code } = await request.json()

  if (typeof code !== "string" || code.length === 0) {
    return NextResponse.json(
      { error: "Mono auth code is required" },
      { status: 400 }
    )
  }

  const monoResponse = await fetch(
    "https://api.withmono.com/v2/accounts/auth",
    {
      method: "POST",
      headers: {
        accept: "application/json",
        "Content-Type": "application/json",
        "mono-sec-key": process.env.MONO_SECRET_KEY
      },
      body: JSON.stringify({ code }),
      cache: "no-store"
    }
  )

  const result = await monoResponse.json()

  if (!monoResponse.ok) {
    return NextResponse.json(
      { error: "Mono token exchange failed" },
      { status: monoResponse.status }
    )
  }

  // Persist result.id for the authenticated application user.
  return NextResponse.json({ id: result.id })
}
```

<Warning>
  Never name the secret `NEXT_PUBLIC_MONO_SECRET_KEY`. Variables with the `NEXT_PUBLIC_` prefix are included in client bundles.
</Warning>

## Reauthorization

Inside the Client Component, initialize with the existing account ID instead of calling `setup()`.

```javascript theme={null}
connect.reauthorise(accountId)
connectRef.current = connect
```

See [Reauthorization](/content/sdk/connect-js/usage/reauthorization) for the complete flow.

## Next.js-specific checks

* The component containing Connect.js begins with `"use client"`.
* Widget creation runs inside `useEffect`.
* The public key uses `NEXT_PUBLIC_`; the secret key never does.
* The Route Handler authenticates the application user before storing an account ID.
* Server errors returned to the browser do not contain the secret key or raw upstream response.
