FaceGateBack to home

Next.js SDK

@facegate/nextjs wires face authentication into the Next.js App Router. It gives you a client provider, route-protecting middleware, server-side helpers (currentUser(), auth()), and a route handler that mints and verifies sessions. Sessions are stored in httpOnly cookies as HS256 JWTs.

The package ships two entry points:

ImportRuns inExports
@facegate/nextjsClient ('use client')FaceGateProvider, FaceGateSignIn, UserButton, useUser, useFaceGate, theme helpers
@facegate/nextjs/serverServer (middleware, route handlers, server components)faceGateMiddleware, currentUser, auth, faceGateRouteHandler

Install

npm install @facegate/nextjs

Peer dependencies: next >= 14, react >= 18, react-dom >= 18. Built and tested against Next 15 and React 19.

Initialization

Server-side helpers read three values: your API key, the FaceGate server URL, and the JWT signing secret. Set them with initFaceGate() or via environment variables.

import { initFaceGate } from '@facegate/nextjs/server';
 
initFaceGate(
  process.env.FACEGATE_API_KEY!,     // apiKey
  process.env.FACEGATE_SERVER_URL,   // serverUrl (optional)
  process.env.FACEGATE_JWT_SECRET,   // jwtSecret (optional arg, required in practice — see callout)
);
ArgumentEnv var fallbackDefaultRequired
apiKeyFACEGATE_API_KEYYes
serverUrlFACEGATE_SERVER_URLhttps://api.facegate.aiNo
jwtSecretFACEGATE_JWT_SECRETYes, in practice — see below

Most setups skip initFaceGate() entirely and just set env vars, since faceGateMiddleware and faceGateRouteHandler call initFaceGate() for you with the API key you pass them. The server helpers (currentUser, auth) read the values back through the env-var fallbacks.

# .env.local
FACEGATE_API_KEY=fg_test_your_key_here
FACEGATE_JWT_SECRET=your_signing_secret
# FACEGATE_SERVER_URL=https://api.facegate.ai   # optional

You must set the JWT secret

This is the one configuration mistake that fails silently. Read it before you ship.

FaceGate's server signs session tokens with HS256 (HMAC-SHA256) using its global JWT signing secret — not your API key. The SDK must verify tokens with that same secret.

You provide it via the jwtSecret argument to initFaceGate() or the FACEGATE_JWT_SECRET environment variable.

If you don't set it, the SDK falls back to using your apiKey as the verification key. Real server-issued tokens were signed with the JWT secret, so HMAC verification fails and decodeAndValidateJWT returns null. The result: every legitimate session is rejected, the middleware redirects authenticated users back to sign-in, and currentUser() always returns null. The only signal is a one-line console warning:

[facegate] Using apiKey for JWT verification. Set jwtSecret via initFaceGate() for correct signature verification.

There is no thrown error and no failed build. Set FACEGATE_JWT_SECRET (or pass jwtSecret) and the warning disappears along with the silent rejections.

A few related guarantees the verifier enforces:

  • Only HS256 is accepted. The token's alg header is checked before the signature. none, asymmetric algorithms, and anything else are rejected outright — this blocks alg: none and key-confusion attacks rather than relying on the HMAC verify to incidentally fail.
  • An exp claim is required. Tokens with no expiry, or expired tokens, are rejected.

Wrap your app in the provider

FaceGateProvider hydrates the current user on mount (it calls GET /api/facegate/session) and exposes user state and sign-out to client components. Put it at the root of your App Router tree.

// app/layout.tsx
import { FaceGateProvider } from '@facegate/nextjs';
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <FaceGateProvider apiKey={process.env.NEXT_PUBLIC_FACEGATE_API_KEY!}>
          {children}
        </FaceGateProvider>
      </body>
    </html>
  );
}
PropTypeDefaultNotes
apiKeystringYour publishable API key.
serverUrlstringhttps://api.facegate.aiOverride for self-hosted servers.
childrenReact.ReactNodeYour app.

Protect routes with middleware

faceGateMiddleware checks the session cookie on incoming requests, redirects unauthenticated users to your sign-in page, and refreshes tokens that are near expiry.

// middleware.ts
import { faceGateMiddleware } from '@facegate/nextjs/server';
 
export default faceGateMiddleware({
  protectedRoutes: ['/dashboard', '/settings'],
  signInUrl: '/login',
  apiKey: process.env.FACEGATE_API_KEY!,
});
 
export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
OptionTypeNotes
signInUrlstringWhere to redirect unauthenticated requests. Required.
protectedRoutesstring[]Exact paths to guard. Everything else is public.
publicRoutesstring[]Inverse mode: everything is protected except these paths.
apiKeystringPassed to initFaceGate() so verification has a key.
serverUrlstringOverride for self-hosted servers.

Use either protectedRoutes (allow-list of guarded paths) or publicRoutes (allow-list of open paths). Routes are matched as exact, anchored patterns — /dashboard does not cover /dashboard/settings, so list nested paths explicitly. If you set neither, nothing is treated as protected.

When a request to a protected route has no valid session, the middleware redirects to signInUrl and appends the original path as a redirect_url query param so you can send the user back after they authenticate. If the access token is within five minutes of expiry and a refresh cookie is present, the middleware calls POST /v1/session/refresh on the FaceGate server and rotates both cookies in the response.

Read the user server-side

In server components, route handlers, and server actions, currentUser() returns the verified user (or null). It reads the session cookie and validates the JWT using the configured secret.

// app/dashboard/page.tsx
import { currentUser } from '@facegate/nextjs/server';
import { redirect } from 'next/navigation';
 
export default async function Dashboard() {
  const user = await currentUser();
  if (!user) redirect('/login');
 
  return <h1>Welcome, {user.name}</h1>;
}

currentUser() resolves to a FaceGateUser or null:

interface FaceGateUser {
  id: string;
  name: string;
  role: string | null;
  metadata: Record<string, unknown>;
  sessionId: string;
  expiresAt: number; // unix seconds
}

auth() is a lighter helper when you only need identifiers:

import { auth } from '@facegate/nextjs/server';
 
const { userId, sessionId } = await auth();
// both are string | null

auth() is built on currentUser() — it returns { userId: null, sessionId: null } whenever there is no valid session.

Set up the route handler

faceGateRouteHandler exposes the /api/facegate/* endpoints the provider and sign-in component call. Mount it at app/api/facegate/[...facegate]/route.ts.

// app/api/facegate/[...facegate]/route.ts
import { faceGateRouteHandler } from '@facegate/nextjs/server';
 
const handler = faceGateRouteHandler({
  apiKey: process.env.FACEGATE_API_KEY!,
  // serverUrl: 'https://api.facegate.ai', // optional
});
 
export const { GET, POST, DELETE } = handler;

The handler implements:

MethodPathPurpose
GET/api/facegate/sessionReturn the current user for client hydration. Returns { user: null } when unauthenticated.
POST/api/facegate/sessionExchange a verification token for a session. Calls POST /v1/session, then sets the session and refresh cookies.
POST/api/facegate/enrollProxy a face enrollment to the FaceGate server. Requires a valid session.
DELETE/api/facegate/sessionSign out — clears both cookies.

Both POST and DELETE validate the Origin header against the request host and return 403 on mismatch, which blocks CSRF. The enroll proxy additionally requires an authenticated session and returns 401 otherwise. Session cookies are httpOnly, sameSite: 'lax', and secure on HTTPS.

Client helpers

Once the provider is mounted, these run in client components.

useUser()

'use client';
import { useUser } from '@facegate/nextjs';
 
export function Greeting() {
  const { user, isLoaded } = useUser();
  if (!isLoaded) return <span>Loading…</span>;
  if (!user) return <a href="/login">Sign in</a>;
  return <span>Hi, {user.name}</span>;
}

Returns { user: FaceGateUser | null, isLoaded: boolean }. isLoaded is false until the hydration fetch resolves.

useFaceGate()

'use client';
import { useFaceGate } from '@facegate/nextjs';
 
export function SignOutButton() {
  const { signOut, reauthenticate } = useFaceGate();
  return <button onClick={() => signOut()}>Sign out</button>;
}

Returns { signOut, reauthenticate }. signOut() calls DELETE /api/facegate/session and clears local user state. reauthenticate() navigates the browser to /login.

<UserButton />

A drop-in avatar with a sign-out menu. Renders null until a user is loaded, so it is safe to place in a nav bar.

import { UserButton } from '@facegate/nextjs';
 
export function Nav() {
  return (
    <nav>
      <UserButton />
    </nav>
  );
}

It shows the user's initials and a dropdown with their name and a Sign Out action. It takes no props.

<FaceGateSignIn />

The camera + liveness sign-in surface, pre-wired to the provider's API key, server URL, and session strategy. It accepts the same props as the @facegate/react component, minus apiKey, serverUrl, and the internal session strategy (the provider supplies those).

'use client';
import { FaceGateSignIn } from '@facegate/nextjs';
 
export function Login() {
  return (
    <FaceGateSignIn
      onAuthenticated={(session) => {
        window.location.href = '/dashboard';
      }}
    />
  );
}

On a successful scan it exchanges the verification token through the route handler, which sets the session cookies — so server components and middleware see the user on the next request.

Theme

The provider re-exports the theming primitives from @facegate/react:

import { resolveTheme, DARK_THEME, LIGHT_THEME } from '@facegate/nextjs';
import type { FaceGateTheme } from '@facegate/nextjs';

See the React SDK for the full theming surface.

Next steps

  • React SDK — every <FaceGateSignIn /> prop and theming option.
  • REST API — the /v1/session, /v1/session/refresh, and /v1/enroll endpoints this SDK calls.
  • Security — liveness, anti-spoofing, and how face data is stored.