Permix

oRPC

Learn how to use Permix with oRPC

Overview

Permix provides a middleware for oRPC that allows you to easily check permissions in your middlewares. The middleware can be created using the createPermix function.

Before getting started with oRPC integration, make sure you've completed the initial setup steps in the Quick Start guide.

Setup

Here's a basic example of how to use the Permix middleware with oRPC:

import { os } from '@orpc/server'
import { createPermix } from 'permix/orpc'

interface Post {
  id: string
  title: string
}

interface Context {
  user: {
    id: string
    role: string
  }
}

const orpcPermix = os.$context<Context>()

// Create your Permix instance with a custom context key
const permix = createPermix<{
  post: ['create', 'read', 'update']
  user: ['delete']
}>().contextKey('permissions')

// Create a protected middleware with Permix
const protectedMiddleware = orpcPermix.use(({ context, next }) => {
  const isAdmin = context.user.role === 'admin'

  return next({
    context: permix.setupContext({
      post: {
        create: true,
        read: true,
        update: isAdmin
      },
      user: {
        delete: isAdmin
      }
    })
  })
})

Call .contextKey('name') on the returned builder to set a custom context key — its literal type is inferred automatically. Omit it to use the default key 'permix'. Pass onForbidden as an option to createPermix itself.

Checking Permissions

Use the checkMiddleware function in your oRPC middlewares to check permissions:

const router = orpcPermix.router({
  createPost: protectedMiddleware
    .use(permix.checkMiddleware('post.create'))
    .handler(({ context }) => {
      // Create post logic here
      return { success: true }
    }),

  updatePost: protectedMiddleware
    .use(permix.checkMiddleware(c => c('post.read') && c('post.update')))
    .handler(({ context }) => {
      // Update post logic here
      return { success: true }
    }),

  deleteUser: protectedMiddleware
    .use(permix.checkMiddleware('user.delete'))
    .handler(({ context }) => {
      // Delete user logic here
      return { success: true }
    })
})

Accessing Permix in Middlewares

Permix is automatically added to your oRPC context under the key you specified, so you can access it directly:

const router = orpcPermix.router({
  getPosts: protectedMiddleware
    .handler(({ context }) => {
      // Check permissions manually
      if (context.permissions.check('post.read')) {
        return getAllPosts()
      }

      throw new ORPCError('FORBIDDEN', {
        message: 'You do not have permission to read posts'
      })
    })
})

The context.permissions object provides:

  • check: Synchronously check a permission
  • dehydrate: Serialize the current rules for client hydration
  • template: Create reusable permission templates

Using Templates

Permix provides a template helper to create reusable permission rule sets:

// Create a template for admin permissions
const adminTemplate = permix.template({
  post: {
    create: true,
    read: true,
    update: true
  },
  user: {
    delete: true
  }
})

// Create a template for regular user permissions
const userTemplate = permix.template({
  post: {
    create: true,
    read: true,
    update: false
  },
  user: {
    delete: false
  }
})

// Use templates in your middleware
const protectedMiddleware = orpcPermix.use(({ context, next }) => {
  return next({
    context: permix.setupContext(
      context.user.role === 'admin'
        ? adminTemplate()
        : userTemplate()
    ),
  })
})

Custom Error Handling

By default, the middleware throws an ORPCError with code FORBIDDEN. You can customize this behavior with the onForbidden option, which is a terminal handler — it receives the middleware opts (including next) plus the check context, and controls the outcome:

Throw a Custom Error

const permix = createPermix<Definition>({
  onForbidden: ({ path, context }) => {
    throw new ORPCError('FORBIDDEN', {
      message: `User ${context.user.id} doesn't have permission for ${path}`
    })
  }
})

Allow Through

You can also let denied requests through by calling next():

const permix = createPermix<Definition>({
  onForbidden: ({ path, next }) => {
    console.warn(`Permission denied for ${path}, allowing through`)
    return next()
  }
})

The onForbidden handler receives:

  • path: The permission path that was checked (e.g. 'post.create')
  • data: Optional data passed to the check
  • context: Your oRPC context object
  • next: The middleware next function — call it to allow the request through

Advanced Usage

Async Permission Rules

You can use async functions in your permission setup:

const protectedMiddleware = orpcPermix.use(async ({ context, next }) => {
  const userPermissions = await getUserPermissions(context.user.id)

  return next({
    context: permix.setupContext({
      post: {
        create: userPermissions.canCreatePosts,
        read: userPermissions.canReadPosts,
        update: userPermissions.canUpdatePosts
      },
      user: {
        delete: userPermissions.canDeleteUsers
      }
    })
  })
})

Hooks

You can register hooks at the factory level to listen for events across all requests:

permix.hook('check', ({ path, data }) => {
  console.log(`Permission checked: ${path}`, data)
})

On this page