Permix

Better Auth

Learn how to use Permix with Better Auth

Overview

Permix provides a Better Auth plugin that exposes a GET /permix/get-permissions endpoint, returning the current user's dehydrated permissions for easy server-client sync.

Make sure you've completed the Quick Start guide first.

Server Setup

Add permixPlugin<Definition>() to your auth config and call createPermix() to define your rules.

auth.ts
import {  } from 'better-auth'
import { ,  } from 'permix/better-auth'
import type {  } from 'permix'

interface Post {
  : string
  : string
  : string
  : string
}

export type  = <{
  : {
    : Post
    : 'create' | 'read' | 'update' | 'delete'
  }
}>

export const  = ({
  // ...your config
  : {
    : {
      : { : 'string', : 'user' },
    },
  },
  : [<>()],
})

// Optional but recommended: pass session type for typed fields like `role`
type  = typeof ..

export const  = <, >({
  : ({  }) => ({
    : {
      : . === 'admin',
      : true,
      : . === 'admin',
      : . === 'admin',
    },
  }),
})

The second generic Session defaults to Better Auth's base session type. Passing typeof auth.$Infer.Session is optional but recommended when using additionalFields or plugins that extend the session (e.g. admin, organization).

Passing Definition to permixPlugin() and permixClient() ensures auth.api.getPermissions() and authClient.permix.getPermissions() return typed PermixStateJSON<Definition>.

Client Setup

Add permixClient() to your auth client for typed access to the permissions endpoint:

auth-client.ts
import { createAuthClient } from 'better-auth/client'
import { permixClient } from 'permix/better-auth'

const authClient = createAuthClient({
  plugins: [permixClient<Definition>()],
})

const { data: permissions, error } = await authClient.permix.getPermissions()

Hydrating on the Client

Hydrate the dehydrated permissions into a client-side Permix instance. Works with React, Vue, and Solid:

permissions.ts
import { createPermix } from 'permix'
import type { Definition } from './permix'

const permix = createPermix<Definition>()

const { data } = await authClient.permix.getPermissions()

if (data) {
  permix.hydrate(data)
}

See the Hydration guide for SSR details.

Using Templates

Use standalone createTemplate() for reusable rule sets. Unlike permix.template(), it avoids circular reference issues with createPermix():

auth.ts
import { createTemplate } from 'permix'
import { createPermix } from 'permix/better-auth'

const adminTemplate = createTemplate<Definition>({
  post: {
    create: true,
    read: true,
    update: true,
    delete: true,
  },
})

const userTemplate = createTemplate<Definition>({
  post: {
    create: false,
    read: true,
    update: false,
    delete: false,
  },
})

type Session = typeof auth.$Infer.Session

const permix = createPermix<Definition, Session>({
  rules: ({ user }) => {
    if (user.role === 'admin') {
      return adminTemplate()
    }

    return userTemplate()
  },
})

Reusing Rules with Framework Middleware

The rules function from createPermix() can be reused in framework middleware to protect routes beyond Better Auth endpoints:

app.ts
import { Hono } from 'hono'
import { createPermix as createHonoPermix } from 'permix/hono'
import { auth, permix } from './auth'

const honoPermix = createHonoPermix<Definition>()

const app = new Hono()

app.use(honoPermix.setupMiddleware(async ({ c }) => {
  const session = await auth.api.getSession({
    headers: c.req.raw.headers,
  })

  if (!session) {
    return {
      post: { create: false, read: false, update: false, delete: false },
    }
  }

  return permix.rules(session)
}))

app.post('/posts', honoPermix.checkMiddleware('post', 'create'), (c) => {
  return c.json({ success: true })
})

Advanced Usage

Async Rules

The rules callback supports async functions for permissions that depend on external data:

const permix = createPermix<Definition, Session>({
  rules: async ({ user }) => {
    const teamPermissions = await getTeamPermissions(user.id)

    return {
      post: {
        create: teamPermissions.canCreatePosts,
        read: true,
        update: teamPermissions.canUpdatePosts,
        delete: teamPermissions.canDeletePosts,
      },
    }
  },
})

Admin Plugin

With Better Auth's admin plugin, use typeof auth.$Infer.Session to get the typed role field:

import { betterAuth } from 'better-auth'
import { admin } from 'better-auth/plugins'
import { permixPlugin, createPermix } from 'permix/better-auth'

export const auth = betterAuth({
  plugins: [admin(), permixPlugin<Definition>()],
})

type Session = typeof auth.$Infer.Session

const permix = createPermix<Definition, Session>({
  rules: ({ user }) => ({
    post: {
      create: user.role === 'admin' || user.role === 'editor',
      read: true,
      update: user.role === 'admin' || user.role === 'editor',
      delete: user.role === 'admin',
    },
  }),
})

Example

For a full working example, see the Next.js + Better Auth example.