ZevAuth Docs
Sign up

Building with the SDK

Next.js

Components, server helpers and route protection.

npm install @zevauth/nextjs

The package has two entry points, and the split matters: everything that renders is a client component, and everything that verifies is server-only.

import { SignIn, useUser } from '@zevauth/nextjs';        // client
import { getAuth } from '@zevauth/nextjs/server';          // server

The client entry is marked 'use client', so importing <SignIn> into a server component works instead of failing with a hooks error.

Provider

// app/layout.tsx
import { ZevAuthProvider } from '@zevauth/nextjs';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ZevAuthProvider publishableKey={process.env.NEXT_PUBLIC_ZEVAUTH_KEY!}>
          {children}
        </ZevAuthProvider>
      </body>
    </html>
  );
}

Reading auth on the server

import { getAuth } from '@zevauth/nextjs/server';

export async function GET(request: Request) {
  const auth = await getAuth(request, {
    environmentId: process.env.ZEVAUTH_ENVIRONMENT_ID!,
  });

  if (!auth.isSignedIn) return new Response('Unauthorized', { status: 401 });

  return Response.json({ userId: auth.userId });
}

This verifies offline, against your environment’s published keys. It does not call ZevAuth on every request, so our availability is not in the path of every page you serve.

An expired, tampered or missing token all return isSignedIn: false rather than throwing. To refuse instead, use requireAuth, which throws a Response:

const auth = await requireAuth(request, { environmentId });

Checking permissions

if (!auth.has({ permission: 'org:members:manage' })) {
  return new Response('Forbidden', { status: 403 });
}

Middleware

// middleware.ts
import { createZevAuthMiddleware } from '@zevauth/nextjs/server';

export default createZevAuthMiddleware({
  environmentId: process.env.ZEVAUTH_ENVIRONMENT_ID!,
  protected: ['/dashboard', '/settings'],
});

export const config = { matcher: ['/((?!_next|.*\\..*).*)'] };

Protection is opt-in, path by path. A signed-out visitor to a protected path is redirected to /sign-in with redirect_url set to where they were going.

A string rule covers everything beneath it, so /dashboard also protects /dashboard/billing. Regular expressions work too, and public wins over protected:

createZevAuthMiddleware({
  environmentId,
  protected: [/^\/app/],
  public: ['/app/health'],
});

Updated at, Friday, August 28, 2026