Skip to content

Next.js Quickstart

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

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.

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

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

Add Environment Variables

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

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!;
  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:

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

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

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

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

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

Add Sign-In, Callback, And Logout

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, 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

Create src/app/protected/page.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

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

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

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 or Run The Example App.