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

# Sign in with Fanvue with the SDK (off-platform)

> Add Sign in with Fanvue to a Next.js off-platform app with SDK route handlers, a cookie session and an API client that refreshes its own tokens.

By the end of this page your Next.js app has Sign in with Fanvue: login, callback and logout routes, a signed cookie session, and an API client that refreshes its tokens when they expire. Everything imports from `@fanvue/builder-sdk/nextjs/off-platform`.

You need:

* an off-platform app in the Developer Area, with its Client ID and Client Secret
* a Next.js App Router project with `@fanvue/builder-sdk` and `next` 14 or later [installed](/docs/app-store/sdk/overview#install)
* an `https` origin for your dev server, unless it runs on `localhost`

## Run over HTTPS locally

A redirect URI uses `https://` on any host, or `http://` only on `localhost`, `127.0.0.1` or `[::1]`. The examples in this guide use `my-fanvue-app.dev`, which is neither, so give your dev server an `https` origin with portless, or with mkcert and local-ssl-proxy. [Set up local HTTPS proxy](/docs/get-started/sign-in-with-fanvue#2-set-up-local-https-proxy) walks through both.

Use the resulting URL, for example `https://my-fanvue-app.dev:3001/api/auth/callback`, as the redirect URI in your app registration and in `OAUTH_REDIRECT_URI`.

If you use an App Manifest, it can't live on the local hostname, because Fanvue reads it from a public domain. Serve it from your production or staging domain and list the local redirect URI in `oauth.redirectUris` alongside the production one.

## Configure the client

`createConfig(opts?)` resolves your OAuth client from options or environment variables and throws when a required value is missing. `createConfigSafe(opts?)` returns `{ ok: false, missing }` instead, naming the unset variables, so a route can answer `503` on a misconfigured deployment. Both read the same variables, listed in full under [Environment variables](/docs/app-store/sdk/overview#environment-variables).

The SDK's default callback path is `/api/auth/callback`, which differs from the `/api/oauth/callback` used by the [App Starter](/docs/get-started/sign-in-with-fanvue). Register whichever your routes serve.

```bash .env.local theme={null}
OAUTH_CLIENT_ID=your-client-id
OAUTH_CLIENT_SECRET=your-client-secret
OAUTH_REDIRECT_URI=https://my-fanvue-app.dev:3001/api/auth/callback
OAUTH_SCOPES=read:self
SESSION_SECRET=replace-with-a-random-string-of-at-least-32-characters
# Optional. Defaults shown.
OAUTH_ISSUER_BASE_URL=https://auth.fanvue.com
API_BASE_URL=https://api.fanvue.com
SESSION_COOKIE_NAME=fanvue_session
```

`OAUTH_SCOPES` adds to the default `openid offline_access offline`, so list only the Fanvue scopes your app needs. Every scope must be registered on your app's **Authentication** tab or in its App Manifest.

## Add the route handlers

Each handler factory takes an `OffPlatformOptions` object, which is the resolved config plus `afterLoginPath` and `afterLogoutPath`. Both paths are required, and both accept `null`, which means `/`.

| Route | Export | Behaviour |
| - | - | - |
| `GET /api/auth/login` | `createLoginHandler(opts)` returns `{ GET }` | Builds the authorisation URL with PKCE and `state`, then redirects the browser to Fanvue. |
| `GET` and `POST /api/auth/callback` | `createCallbackHandler(opts)` returns `{ GET, POST }` | Exchanges the code for tokens, signs the session JWT, sets the cookie and redirects to `afterLoginPath`. `POST` serves `OAUTH_RESPONSE_MODE=form_post`. |
| `POST /api/auth/logout` | `createLogoutHandler(opts)` returns `{ POST }` | Deletes the cookie and redirects to `afterLogoutPath`. |

Build one options object and share it across the three routes.

```typescript lib/fanvue.ts theme={null}
import { createConfig } from "@fanvue/builder-sdk/nextjs/off-platform";

export const fanvue = {
  ...createConfig(),
  afterLoginPath: "/home",
  afterLogoutPath: null,
};
```

```typescript app/api/auth/login/route.ts theme={null}
import { createLoginHandler } from "@fanvue/builder-sdk/nextjs/off-platform";
import { fanvue } from "@/lib/fanvue";

export const { GET } = createLoginHandler(fanvue);
```

```typescript app/api/auth/callback/route.ts theme={null}
import { createCallbackHandler } from "@fanvue/builder-sdk/nextjs/off-platform";
import { fanvue } from "@/lib/fanvue";

export const { GET, POST } = createCallbackHandler(fanvue);
```

```typescript app/api/auth/logout/route.ts theme={null}
import { createLogoutHandler } from "@fanvue/builder-sdk/nextjs/off-platform";
import { fanvue } from "@/lib/fanvue";

export const { POST } = createLogoutHandler(fanvue);
```

Register the callback route's full URL, including scheme and port, as the redirect URI. Any mismatch fails the token exchange.

## Read the session

After sign-in, the callback stores a signed JWT in an `httpOnly` cookie. The JWT holds the access token, refresh token and expiry, plus the creator's `uuid`, `handle`, `displayName`, `isCreator` and `avatarUrl`.

| Cookie fact | Value |
| - | - |
| Name | `fanvue_session`, or `SESSION_COOKIE_NAME` |
| Lifetime | 30 days (`SESSION_COOKIE_MAX_AGE_SECONDS`) |
| `SameSite` | `Lax` |
| `Secure` | Set when the callback request arrived over `https`; set in production on refresh |

`getSession(secret, cookieName?)` verifies the cookie and returns the `SessionPayload`, or `null` when the cookie is missing or fails verification.

`getAuthenticatedClient({ sessionSecret, sessionCookieName, config })` returns a `FanvueClient` bound to the session's access token, or `null` when there is no valid session. When the access token is within 30 seconds of expiry and a refresh token exists, it refreshes first and rewrites the cookie with the new tokens. A failed refresh also returns `null`, so treat `null` as a signal to send the creator to `/api/auth/login`.

```tsx app/home/page.tsx theme={null}
import { redirect } from "next/navigation";
import { getAuthenticatedClient } from "@fanvue/builder-sdk/nextjs/off-platform";
import { fanvue } from "@/lib/fanvue";

export default async function HomePage() {
  const client = await getAuthenticatedClient({
    sessionSecret: fanvue.sessionSecret,
    sessionCookieName: fanvue.sessionCookieName,
    config: fanvue,
  });
  if (!client) redirect("/api/auth/login");

  const me = await client.getCurrentUser();
  if (me.isErr()) redirect("/api/auth/login");

  return <h1>Signed in as @{me.value.handle}</h1>;
}
```

A creator who signs in through this flow grants offline access, so the refresh token in the cookie keeps the session alive for the full 30 days without a new login. For what the client can do, see [Call the API with the SDK client](/docs/app-store/sdk/api-client).

## Receive webhooks

Webhook subscriptions are created with the creator's OAuth token, which this entrypoint produces, so it re-exports `createWebhookReceiverHandler` and its port types. [Receive webhooks with the SDK](/docs/app-store/sdk/webhooks) covers the setup.

## Troubleshoot

| Symptom | Cause | Fix |
| - | - | - |
| `Missing required OAuth configuration` thrown at startup | A required `OAUTH_*` variable or `SESSION_SECRET` is unset | Set the variable, or switch to `createConfigSafe` and return `503` while unconfigured. |
| `Invalid apiBaseUrl` thrown at startup | `API_BASE_URL` is not a `fanvue.com` host | Remove the variable or set it to `https://api.fanvue.com`. |
| Redirect URI error on the Fanvue consent screen | `OAUTH_REDIRECT_URI` differs from every registered redirect URI | Match scheme, host, port and path exactly. If a manifest sets the redirect URIs, check the **Versions** tab shows **Up to date**. |
| Scope error on the consent screen | A scope in `OAUTH_SCOPES` is not registered on the app | Add it on the **Authentication** tab or in the manifest. |
| `getAuthenticatedClient` returns `null` after login | The refresh token was retired, or the cookie did not survive a scheme change | Send the creator through `/api/auth/login` again. |

## See also

* [Authentication implementation guide](/docs/authentication/implementation-guide)


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