Permix

Next.js

Learn how to use Permix with Next.js App Router

Overview

Permix provides a dedicated integration for the Next.js App Router through permix/next. It exposes a createPermix factory that returns a per-request Permix instance backed by React's cache(), so you can setup() the rules once and check() them anywhere on the server — layouts, pages, route handlers, and server actions — without threading the instance through props.

The client side reuses the existing React integration (permix/react): the server dehydrate()s its state and the client hydrates it into its own singleton via PermixProvider + PermixHydrate.

Before getting started with the Next.js integration, make sure you've completed the initial setup steps in the Quick Start guide. Familiarity with the Hydration guide helps too.

This integration is designed for the App Router. It relies on react's request-scoped cache(), which is available in server components, route handlers, and server actions within a single request.

Define your permissions

Create a Permix instance once in a shared module so it can be imported anywhere on the server:

lib/permix.ts
import { createPermix } from 'permix/next'

interface Post {
  id: string
  authorId: string
}

export const permix = createPermix<{
  post: [
    { name: 'create', type: Post },
    { name: 'read', type: Post },
    { name: 'update', type: Post },
    { name: 'delete', type: Post },
  ]
}>()

The returned helper does not hold any permission state at module scope — every request gets its own isolated instance.

Setup per request

Call setup() early in the request lifecycle. A common place is the root layout (or any server component that runs before the ones doing checks). Resolve any async data (session, headers, cookies, DB lookups) first, then pass plain rules to setup:

app/layout.tsx
import { permix } from '@/lib/permix'
import { getSession } from '@/lib/auth'

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const session = await getSession()

  permix.setup({
    post: {
      create: !!session,
      read: true,
      update: post => post?.authorId === session?.userId,
      delete: session?.role === 'admin',
    },
  })

  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

Because setup() is scoped to the current request, calling it again from a nested server component or route handler in the same request simply replaces the rules for that request. Other requests are unaffected.

Check on the server

Anywhere a server component, route handler, or server action runs in that request, you can use check():

app/posts/[id]/page.tsx
import { notFound } from 'next/navigation'
import { permix } from '@/lib/permix'
import { getPost } from '@/lib/posts'

export default async function PostPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const post = await getPost(id)

  if (!permix.check('post.read', post)) {
    notFound()
  }

  return <article>{/* ... */}</article>
}
app/api/posts/route.ts
import { permix } from '@/lib/permix'

export async function POST(req: Request) {
  if (!permix.check('post.create')) {
    return Response.json({ error: 'Forbidden' }, { status: 403 })
  }

  // create the post...
  return Response.json({ ok: true })
}

You can also reach the underlying core instance through permix.get() if you need methods like isReady() or getRules().

Send permissions to the client

Use dehydrate() to serialize the request's permissions and hand them to a client provider. The server cannot send the Permix instance itself across the boundary — only the JSON state.

app/providers.tsx
'use client'

import { createPermix } from 'permix'
import { PermixHydrate, PermixProvider } from 'permix/react'
import type { DehydratedState } from 'permix'

// One singleton per browser tab. The same type definition as on the server.
const permix = createPermix<{
  post: [
    { name: 'create', type: { id: string, authorId: string } },
    { name: 'read', type: { id: string, authorId: string } },
    { name: 'update', type: { id: string, authorId: string } },
    { name: 'delete', type: { id: string, authorId: string } },
  ]
}>()

export function Providers({
  state,
  children,
}: {
  state: DehydratedState<any>
  children: React.ReactNode
}) {
  return (
    <PermixProvider permix={permix}>
      <PermixHydrate state={state}>{children}</PermixHydrate>
    </PermixProvider>
  )
}

export { permix }

Then wire it up in your root layout right after setup():

app/layout.tsx
import { permix } from '@/lib/permix'
import { getSession } from '@/lib/auth'
import { Providers } from './providers'

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const session = await getSession()

  permix.setup({
    /* ...rules derived from session... */
  })

  return (
    <html lang="en">
      <body>
        <Providers state={permix.dehydrate()}>{children}</Providers>
      </body>
    </html>
  )
}

hydrate() restores the boolean state but does not flip isReady on its own — function-based rules are lost during serialization. If you need isReady on the client (e.g. to gate UI on usePermix(...).isReady), call permix.setup(...) on the client too with the same shape (using booleans and any function rules you want active client-side). See the Hydration guide for details.

Use on the client

From any client component, import the singleton from app/providers.tsx and the hooks/components from permix/react:

app/posts/[id]/edit-button.tsx
'use client'

import { usePermix } from 'permix/react'
import { permix } from '@/app/providers'

export function EditButton({ post }: { post: { id: string, authorId: string } }) {
  const { check } = usePermix(permix)

  if (!check('post.update', post)) {
    return null
  }

  return <button>Edit post</button>
}

If you prefer the component API, create checkers with createComponents from permix/react and use them in your client components — see the React integration for details.

Templates

createPermix exposes the same template() helper as the core API for reusing rule sets:

lib/permix.ts
import { createPermix } from 'permix/next'

export const permix = createPermix<{
  post: ['create', 'read', 'update', 'delete']
}>()

export const adminTemplate = permix.template({
  post: { create: true, read: true, update: true, delete: true },
})

export const guestTemplate = permix.template({
  post: { create: false, read: true, update: false, delete: false },
})
app/layout.tsx
import { permix, adminTemplate, guestTemplate } from '@/lib/permix'
import { getSession } from '@/lib/auth'

const session = await getSession()

permix.setup(session?.role === 'admin' ? adminTemplate() : guestTemplate())

Example

You can find a runnable example of the Next.js integration here.

How per-request isolation works

createPermix from permix/next wraps a single core instance per request using React's cache(). Inside one Next.js request:

  • The first call to setup/check/get/dehydrate creates (or reuses) one instance.
  • All subsequent calls in the same request — across server components, route handlers, and server actions — share that instance.

Across concurrent requests, each request gets its own instance. State never leaks between users.

Do not store the result of permix.get() (or any rule data) in module-level variables. That would defeat per-request isolation. Always go through permix.check() / permix.get() so the request-scoped cache is consulted.

API

createPermix<D>()

Returns an object with the following methods:

MethodDescription
setup(rules)Set the per-request permission rules. Resolve any async data (session, etc.) before calling.
check(...args)Check a permission against the current request's rules. Same signature as the core check.
get()Return the underlying Permix<D> instance for the current request.
getRules()Return the current rules object for the request-scoped instance, or null.
dehydrate()Serialize the current request's rules to JSON (for <PermixHydrate> on the client).
template(rules)Create a reusable rule set. Same as the core template.

TanStack Start and other frameworks

The client layer (permix/react) is framework-agnostic. If you're using TanStack Start, Remix, or a custom React SSR setup, you can still use permix/react on the client. For the server side, either:

On this page