---
title: Next.js
description: Components, server helpers and route protection.
---

import Callout from '../../../components/Callout.astro';


```bash
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.

```ts
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

```tsx
// 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

```ts
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`:

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

### Checking permissions

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

<Callout type="info">
Offline verification cannot know a session was revoked. That is the trade a
stateless token makes. Access tokens are short-lived, so the window is small.
If you need to react to a sign-out immediately, call `/v1/me`, which checks the
session.
</Callout>

## Middleware

```ts
// 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.

<Callout type="info">
Protect-by-default sounds safer and is not: it protects the sign-in page too,
and the callback it redirects to, so the app redirect-loops. Listing what to
protect is explicit, and the mistake it allows, forgetting a route, is caught
by the server check you need anyway.
</Callout>

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

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