Skip to content

Next.js Quickstart

Ask about this page: Claude ChatGPT Grok

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.

This guide uses a browser OAuth session and bearer-authenticated API routes. The protected page is client UI; server data is protected by the API guard. For login backed by your own server cookie, use Backend SDK.

Use an existing App Router project with versions supported by the SDK packages (current peer requirements: Next.js 16.3.2+, React 19.2.8+, TypeScript 6.0.3+). Examples use src/ and the @/* import alias; adapt both to your project.

What You Will Build

Ask:
  • 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

New Evaluation: Provision A Sandbox

Ask:

A coding agent can create its own sandbox organization, tenant, application, and ordinary fixture user without a dashboard session. Use the agent quickstart and run:

npx @sigid/cli setup --name my-evaluation --framework nextjs \
  --redirect-uri http://localhost:3000/auth/callback

Map the returned tenant_issuer, client_id, and tenant_id using Integration Settings. The generic env_block is not a complete framework environment file; configure the API audience and scopes separately before testing the protected API. Setup derives allowed browser origins from the exact redirect URI; use your actual preview URL if it differs from the local example. Keep bootstrap management tokens, client secrets and fixture passwords in the private credential file. Configure the app with the public issuer/client ID; let the SDK manage its OAuth session. For unattended execution, supply the keystore passphrase through the CLI's --passphrase-file or --passphrase-stdin option.

Use the returned fixture human for real hosted sign-in, callback, signed-in UI, and logout. The CLI saves its once-only credentials in the owner-only, gitignored .dev/sandbox-fixture.env; keep that file private. Successful metadata probes or expected error responses do not prove hosted login works. Invite or hand off to the human operator after the integration works; activation of the sandbox for production still requires a human owner at AAL2.

For an existing managed workspace, follow the admin configuration path below. init requires an existing tenant and dynamic client registration may require an initial access token; neither replaces sandbox setup. For bootstrap failures, use the agent quickstart troubleshooting with the failing phase, CLI/server versions and sanitized problem/request ID.

Before You Start

Ask:

For an existing managed workspace, obtain one application from its authorized owner:

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 (UUID) tenant_id of 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.

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

Ask:
npm install @sigid/client @sigid/react @sigid/next

Add Environment Variables

Ask:

Create .env.local:

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

Ask:

Create src/lib/sigid-browser.ts:

"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?.trim();
  const issuer = process.env.NEXT_PUBLIC_SIGID_ISSUER_URL?.trim();
  const clientId = process.env.NEXT_PUBLIC_SIGID_CLIENT_ID?.trim();
  const scopes = process.env.NEXT_PUBLIC_SIGID_SCOPES?.trim();
  if (!appUrl || !issuer || !clientId || !scopes) {
    throw new Error("Configure the public SigID settings in .env.local and restart the dev server.");
  }
  browserClient = createSigIdClient({
    baseURL: issuer,
    oauth: {
      clientId,
      redirectUri: `${appUrl}/auth/callback`,
      scopes: scopes.split(/\s+/),
    },
  });

  return browserClient;
}

Mount The Provider

Ask:

Create src/app/providers.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:

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

Ask:

The browser SDK sends authorization and token requests to the configured SigID issuer. Add the callback page below at /auth/callback; it completes the SDK flow in the same browser. This quickstart needs no /api/auth catch-all route. For a server that owns the OAuth exchange and session cookie, follow Backend SDK instead.

Add Sign-In, Callback, And Logout

Ask:

Use a sign-in button on src/app/page.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:

"use client";

import { useEffect, useRef, useState } from "react";
import { useSigId } from "@sigid/react";
import { useRouter } from "next/navigation";

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

  const started = useRef(false);

  useEffect(() => {
    if (started.current) return;
    started.current = true;
    handleCallback()
      .then(() => router.replace("/protected"))
      .catch((error) => setMessage(error instanceof Error ? error.message : String(error)));
  }, [handleCallback, router]);

  return <main><p>{message}</p><a href="/">Return to sign-in</a></main>;
}

Protect A Page

Ask:

Create src/app/protected/page.tsx:

"use client";

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

export default function ProtectedPage() {
  const { session } = useSession();
  const { fetchWithAuth } = useSigId();
  const [result, setResult] = useState("");

  async function callApi() {
    setResult("Loading...");
    try {
      const response = await fetchWithAuth("/api/projects");
      const body = await response.json();
      setResult(response.ok ? `API accepted subject: ${body.subject}` : `API denied: ${body.error}`);
    } catch {
      setResult("Could not reach the API. Try again.");
    }
  }

  return (
    <Protected signedOut={<SignInButton returnTo="/protected">Sign in</SignInButton>}>
      <main>
        <h1>Protected page</h1>
        <p>Session ID: {session?.session.id}</p>
        <button onClick={() => void callApi()}>Call protected API</button>
        <p role="status">{result}</p>
      </main>
    </Protected>
  );
}

Protect An API Route

Ask:

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

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

// Validate required settings when this server module loads.
const issuer = process.env.NEXT_PUBLIC_SIGID_ISSUER_URL?.trim();
const audience = process.env.SIGID_API_AUDIENCE?.trim();
const tenantId = process.env.SIGID_TENANT_ID?.trim();
const requiredScope = process.env.SIGID_API_SCOPE?.trim();
if (!issuer || !audience || !tenantId || !requiredScope) {
  throw new Error("Configure the SigID issuer, API audience, tenant UUID, and API scope.");
}
const tokenOptions = {
  issuer,
  audience,
  tenantId,
  scopes: [requiredScope],
  allowedSubjectTypes: ["human"],
};

export async function GET(request: Request) {
  try {
    const claims = await requireAccessToken(request, tokenOptions);

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

This example returns only the validated subject. Add tenant-scoped resource lookups and ownership checks before returning application data; see Protect Backend APIs. The Protected React component controls UI visibility; the server guard protects the data.

Run And Verify

Ask:
npm run dev

Open http://localhost:3000. After login, the callback navigates to /protected; select Call protected API to send the access token with the SDK. Opening /api/projects directly in the address bar does not attach that bearer token and should return 401.

Check the unauthenticated route separately:

curl -i http://localhost:3000/api/projects

Expect 401, a WWW-Authenticate header, and a JSON missing_bearer_token error.

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

Ask:
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 401 insufficient_scope, wrong_tenant, or wrong_audience Match the registered application, requested scopes, tenant UUID, and backend settings. Sign in again after changing grants.
Protected API returns 403 Your application accepted the token but denied access to the resource.
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 or Run The Example App.