---
summary: Step-by-step Next.js App Router quickstart for adding SigID hosted login, callback handling, protected UI, and a protected API route.
tags:
  - developers
  - quickstart
  - nextjs
  - app-router
categories:
  - For Developers
---

# Next.js Quickstart

<!-- agent:page
You are a coding agent integrating SigID hosted login into a Next.js App Router app by following this guide end to end.
First collect from the user or workspace admin, all from the same environment: issuer URL, client ID, app URL, allowed callback/logout/web-origin/CORS values, scopes (e.g. openid profile email projects:read), API audience, and tenant ID. The Dashboard application must use Authorization Code with PKCE and the exact callback URL <app URL>/auth/callback.
Implementation order:
- npm install @sigid/client @sigid/react @sigid/next
- create .env.local with NEXT_PUBLIC_SIGID_ISSUER_URL, NEXT_PUBLIC_SIGID_CLIENT_ID, NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_SIGID_SCOPES, SIGID_API_AUDIENCE, SIGID_API_SCOPE, SIGID_TENANT_ID (never put a client secret in NEXT_PUBLIC_*)
- create the src/lib/sigid-browser.ts and src/lib/sigid-server.ts clients, mount SigIdProvider via src/app/providers.tsx in the root layout
- add src/app/api/auth/[...auth]/route.ts with createAuthHandlers, sign-in/sign-out UI on the home page, src/app/auth/callback/page.tsx, a protected page, and a protected src/app/api/projects/route.ts using requireAccessToken
Adapt paths to the project's existing layout and do not overwrite existing auth code blindly.
Verify with npm run dev: sign-in redirects to the configured issuer, the callback returns to /auth/callback, the protected page renders after sign-in, /api/projects returns JSON only with a valid access token, and sign-out clears the session.
Most common failures: invalid_redirect_uri means the callback URL does not exactly match Dashboard (scheme, host, port, path, trailing slash); 403/insufficient_scope on the API means the token lacks SIGID_API_SCOPE or uses the wrong tenant or audience.
-->

Use this guide when you want a Next.js App Router app to sign users in with
SigID, handle the callback, show a protected page, and protect one API route.

Time: 20-30 minutes after you have a SigID application configuration.

## What You Will Build

- a public PKCE browser login flow
- a SigID provider in your App Router layout
- a sign-in button, callback page, and logout action
- a protected page backed by the browser session
- a protected API route that validates SigID access tokens

## Before You Start

<!-- agent:action Collect the application configuration
Ask the user or workspace admin for every value in this table: issuer URL, client ID, app URL, allowed callback/logout/web-origin/CORS URLs, scopes, API audience, and tenant ID, all from one environment.
If the user owns the workspace, have them create the application in Dashboard (choose tenant, open Applications, Create Application) with Authorization Code with PKCE and the exact callback, logout, web origin, and CORS values.
Do not proceed with placeholders; mismatched callback values cause invalid_redirect_uri failures later.
-->

Ask the workspace owner for one application from the same environment:

| Value | Local example |
|---|---|
| Issuer URL | `http://auth.sigid.localhost:3000` |
| Client ID | `sigid-next-local` |
| App URL | `http://localhost:3000` |
| Allowed Callback URL | `http://localhost:3000/auth/callback` |
| Allowed Logout URL | `http://localhost:3000` |
| Allowed Web Origin | `http://localhost:3000` |
| Allowed Origins (CORS) | `http://localhost:3000` |
| Scopes | `openid profile email projects:read` |
| API audience | `https://api.example.local/projects` |
| Tenant ID or slug | the tenant that owns the app |

If you own the workspace, create it in Dashboard first: choose the tenant,
open **Applications**, click **Create Application**, then fill the create form.
If you do not own the workspace, ask the admin for the handoff packet from
[Workspace Admin Quickstart](../business/workspace-admin-quickstart.md#5-hand-off-to-developers).

In the Dashboard application, set:

1. A user-recognizable application name.
2. Authorization Code with PKCE for browser login.
3. The exact callback, logout, web origin, and CORS origin values above.
4. The scopes and API audience your backend will require.

The field labels in the current Dashboard application form include **Allowed
Callback URLs**, **Allowed Logout URLs**, **Allowed Web Origins**, and
**Allowed Origins (CORS)**.

## Install

<!-- agent:action Install the SDK packages
Run: npm install @sigid/client @sigid/react @sigid/next
Run it at the Next.js app root (where package.json lives), using the project's existing package manager if it is not npm.
Verify all three packages appear in package.json dependencies before continuing.
-->

```bash
npm install @sigid/client @sigid/react @sigid/next
```

## Add Environment Variables

<!-- agent:action Add environment variables
Create .env.local at the app root with NEXT_PUBLIC_SIGID_ISSUER_URL, NEXT_PUBLIC_SIGID_CLIENT_ID, NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_SIGID_SCOPES, plus server-only SIGID_API_AUDIENCE, SIGID_API_SCOPE, and SIGID_TENANT_ID.
Fill in the values collected from the workspace admin; ask the user for any value you do not have.
Never place a client secret in a NEXT_PUBLIC_* variable; browser PKCE clients must be public clients.
-->

Create `.env.local`:

```bash
NEXT_PUBLIC_SIGID_ISSUER_URL=http://auth.sigid.localhost:3000
NEXT_PUBLIC_SIGID_CLIENT_ID=sigid-next-local
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_SIGID_SCOPES="openid profile email projects:read"

SIGID_API_AUDIENCE=https://api.example.local/projects
SIGID_API_SCOPE=projects:read
SIGID_TENANT_ID=tenant-id-required
```

Do not put a client secret in `NEXT_PUBLIC_*` variables. Browser PKCE clients
must be public clients.

## Create The SigID Clients

<!-- agent:action Create the SigID clients
Create src/lib/sigid-browser.ts exporting getBrowserSigIdClient(), which lazily builds a client with createSigIdClient from @sigid/client using NEXT_PUBLIC_SIGID_ISSUER_URL as baseURL, NEXT_PUBLIC_SIGID_CLIENT_ID, redirectUri NEXT_PUBLIC_APP_URL + "/auth/callback", and NEXT_PUBLIC_SIGID_SCOPES split on whitespace.
Create src/lib/sigid-server.ts exporting createServerSigIdClient() with the same oauth config plus basePath "/auth".
If the project has no src/ directory, place these under lib/ and adjust imports accordingly.
-->

Create `src/lib/sigid-browser.ts`:

```typescript
"use client";

import { createSigIdClient, type SigIdClient } from "@sigid/client";

let browserClient: SigIdClient | null = null;

export function getBrowserSigIdClient(): SigIdClient {
  if (browserClient) return browserClient;

  const appUrl = process.env.NEXT_PUBLIC_APP_URL!;
  browserClient = createSigIdClient({
    baseURL: process.env.NEXT_PUBLIC_SIGID_ISSUER_URL!,
    oauth: {
      clientId: process.env.NEXT_PUBLIC_SIGID_CLIENT_ID!,
      redirectUri: `${appUrl}/auth/callback`,
      scopes: process.env.NEXT_PUBLIC_SIGID_SCOPES!.split(/\s+/),
    },
  });

  return browserClient;
}
```

Create `src/lib/sigid-server.ts`:

```typescript
import { createSigIdClient } from "@sigid/client";

export function createServerSigIdClient() {
  const appUrl = process.env.NEXT_PUBLIC_APP_URL!;
  return createSigIdClient({
    baseURL: process.env.NEXT_PUBLIC_SIGID_ISSUER_URL!,
    basePath: "/auth",
    oauth: {
      clientId: process.env.NEXT_PUBLIC_SIGID_CLIENT_ID!,
      redirectUri: `${appUrl}/auth/callback`,
      scopes: process.env.NEXT_PUBLIC_SIGID_SCOPES!.split(/\s+/),
    },
  });
}
```

## Mount The Provider

<!-- agent:action Mount the provider
Create src/app/providers.tsx as a "use client" component that wraps children in SigIdProvider from @sigid/react, passing the client from getBrowserSigIdClient().
Import Providers in src/app/layout.tsx and wrap the body's children with it.
If the layout already has a providers wrapper, nest SigIdProvider inside the existing one instead of replacing it.
-->

Create `src/app/providers.tsx`:

```tsx
"use client";

import { SigIdProvider } from "@sigid/react";
import { getBrowserSigIdClient } from "@/lib/sigid-browser";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <SigIdProvider client={getBrowserSigIdClient()}>
      {children}
    </SigIdProvider>
  );
}
```

Use it in `src/app/layout.tsx`:

```tsx
import { Providers } from "./providers";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}
```

## Add Auth Routes

<!-- agent:action Add the auth route handlers
Create src/app/api/auth/[...auth]/route.ts exporting { GET, POST } from createAuthHandlers(createServerSigIdClient()), with createAuthHandlers imported from @sigid/next and createServerSigIdClient from src/lib/sigid-server.ts.
This single catch-all route serves the server side of the auth flow; do not add other route files under /api/auth.
-->

Create `src/app/api/auth/[...auth]/route.ts`:

```typescript
import { createAuthHandlers } from "@sigid/next";
import { createServerSigIdClient } from "@/lib/sigid-server";

export const { GET, POST } = createAuthHandlers(createServerSigIdClient());
```

## Add Sign-In, Callback, And Logout

<!-- agent:action Add sign-in, callback, and logout
On the home page (src/app/page.tsx), render SignInButton inside SignedOut and the user's email plus SignOutButton inside SignedIn, using useUser from @sigid/react; pass returnTo="/protected" to SignInButton.
Create src/app/auth/callback/page.tsx as a "use client" page that calls handleCallback() from useSigId() in a useEffect and shows progress or a readable error message.
The callback path must match the Allowed Callback URL registered in Dashboard exactly.
-->

Use a sign-in button on `src/app/page.tsx`:

```tsx
"use client";

import { SignedIn, SignedOut, SignInButton, SignOutButton, useUser } from "@sigid/react";

export default function HomePage() {
  const { user } = useUser();

  return (
    <main>
      <SignedOut>
        <SignInButton returnTo="/protected">Sign in with SigID</SignInButton>
      </SignedOut>
      <SignedIn>
        <p>Signed in as {user?.email ?? user?.id}</p>
        <SignOutButton>Sign out</SignOutButton>
      </SignedIn>
    </main>
  );
}
```

Create `src/app/auth/callback/page.tsx`:

```tsx
"use client";

import { useEffect, useState } from "react";
import { useSigId } from "@sigid/react";

export default function CallbackPage() {
  const { handleCallback } = useSigId();
  const [message, setMessage] = useState("Completing sign-in...");

  useEffect(() => {
    handleCallback()
      .then(() => setMessage("Sign-in complete. You can return to the app."))
      .catch((error) => setMessage(error instanceof Error ? error.message : String(error)));
  }, [handleCallback]);

  return <main>{message}</main>;
}
```

## Protect A Page

<!-- agent:action Protect a page
Create src/app/protected/page.tsx as a "use client" page wrapping its content in the Protected component from @sigid/react, with the signedOut prop rendering a SignInButton with returnTo="/protected".
Use useSession() to display session metadata; never render raw tokens.
-->

Create `src/app/protected/page.tsx`:

```tsx
"use client";

import { Protected, SignInButton, useSession } from "@sigid/react";

export default function ProtectedPage() {
  const { session } = useSession();

  return (
    <Protected signedOut={<SignInButton returnTo="/protected">Sign in</SignInButton>}>
      <main>
        <h1>Protected page</h1>
        <p>Session ID: {session?.session.id}</p>
      </main>
    </Protected>
  );
}
```

## Protect An API Route

<!-- agent:action Protect an API route
Create src/app/api/projects/route.ts. In GET, call requireAccessToken(request, ...) from @sigid/next with issuer NEXT_PUBLIC_SIGID_ISSUER_URL, audience SIGID_API_AUDIENCE, tenantId SIGID_TENANT_ID, scopes [SIGID_API_SCOPE], and allowedSubjectTypes ["human"].
Return JSON with the validated claims.subject on success; on error return accessTokenErrorResponse(error).
The route must fail closed: a missing or invalid bearer token gets an error response, never data.
-->

Create `src/app/api/projects/route.ts`:

```typescript
import { accessTokenErrorResponse, requireAccessToken } from "@sigid/next";

export async function GET(request: Request) {
  try {
    const claims = await requireAccessToken(request, {
      issuer: process.env.NEXT_PUBLIC_SIGID_ISSUER_URL!,
      audience: process.env.SIGID_API_AUDIENCE!,
      tenantId: process.env.SIGID_TENANT_ID!,
      scopes: [process.env.SIGID_API_SCOPE ?? "projects:read"],
      allowedSubjectTypes: ["human"],
    });

    return Response.json({ ok: true, subject: claims.subject });
  } catch (error) {
    return accessTokenErrorResponse(error);
  }
}
```

## Run And Verify

<!-- agent:action Run and verify
Start the app with npm run dev and open http://localhost:3000.
Confirm: the home page shows the sign-in button, sign-in redirects to the configured issuer, the callback returns to /auth/callback, the protected page renders after sign-in, /api/projects returns JSON only with a valid access token, and sign-out returns to the signed-out state.
If you see invalid_redirect_uri, compare the registered callback URL with the app's value exactly, including scheme, host, port, path, and trailing slash.
-->

```bash
npm run dev
```

Open `http://localhost:3000`.

You are successful when:

- the home page shows a SigID sign-in button
- sign-in sends the browser to the configured SigID issuer
- the callback returns to `/auth/callback`
- the protected page is visible after sign-in
- `/api/projects` returns JSON only when called with a valid access token
- sign-out clears the local session and returns to the signed-out state

## If Something Fails

| Symptom | Check |
|---|---|
| `invalid_redirect_uri` | Callback URL exactly matches Dashboard, including scheme, host, port, path, and trailing slash. |
| Callback state error | The callback tab kept SDK state from the same login attempt. Restart login from the app. |
| Protected API returns `401` | The request did not send a valid bearer token. |
| Protected API returns `403` or `insufficient_scope` | The token lacks `SIGID_API_SCOPE` or uses the wrong tenant/audience. |
| Login works locally but fails in production | Production issuer, app URL, callback URL, origins, and tenant values are from the same environment. |

Next, read [Backend API Quickstart](quickstart-backend-api.md) or
[Run The Example App](run-example-app.md).
