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:
- A user-recognizable application name.
- Authorization Code with PKCE for browser login.
- The exact callback, logout, web origin, and CORS origin values above.
- 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¶
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¶
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/projectsreturns 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.