Permix

tRPC

Learn how to use Permix with tRPC

Overview

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

Before getting started with tRPC 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 tRPC:

import {  } from '@trpc/server'
import {  } from 'permix/trpc'
 
interface Post {
  : string
  : string
  : string
  : string
}
 
interface Context {
  : {
    : string
    : string
  }
}
 
// Initialize tRPC
const  = .<Context>().()
 
// Create your Permix instance
const  = <{
  : {
    : Post
    : 'create' | 'read' | 'update' | 'delete'
  }
}>()
 
// Create a protected procedure with Permix
const  = ..(.<Context>(({  }) => {
  // You can access ctx.user or other properties to determine permissions
  const  = .. === 'admin'
 
  return {
    : {
      : true,
      : true,
      : ,
      : 
    }
  }
}))

The middleware preserves the context and input types from your tRPC procedures, ensuring end-to-end type safety in your API.

Checking Permissions

Use the checkMiddleware function in your tRPC procedures to check permissions:

const router = t.router({
  createPost: protectedProcedure
    .use(permix.checkMiddleware('post', 'create'))
    .mutation(({ input }) => {
      // Create post logic here
      return { success: true }
    }),
 
  updatePost: protectedProcedure
    .use(permix.checkMiddleware('post', ['read', 'update']))
    .mutation(({ input }) => {
      // Update post logic here
      return { success: true }
    }),
 
  deletePost: protectedProcedure
    .use(permix.checkMiddleware('post', 'delete'))
    .mutation(({ input }) => {
      // Delete post logic here
      return { success: true }
    }),
 
  adminAction: protectedProcedure
    .use(permix.checkMiddleware('post', 'all'))
    .query(() => {
      // Admin-only action
      return { success: true }
    })
})

Accessing Permix in Procedures

Permix is automatically added to your tRPC context, so you can access it directly:

const router = t.router({
  getPosts: protectedProcedure
    .query(({ ctx }) => {
      // Check permissions manually
      if (ctx.permix.check('post', 'read')) {
        // User has permission to read posts
        return getAllPosts()
      }
 
      // If not explicitly blocked by middleware, you can handle permission failures here
      throw new TRPCError({
        code: 'FORBIDDEN',
        message: 'You do not have permission to read posts'
      })
    })
})

The ctx.permix object contains two methods:

  • check: Synchronously check a permission
  • checkAsync: Asynchronously check a permission

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,
    delete: true
  }
})
 
// Create a template for regular user permissions
const userTemplate = permix.template({
  post: {
    create: true,
    read: true,
    update: false,
    delete: false
  }
})
 
// Use templates in your middleware
const protectedProcedure = t.procedure.use(permix.setupMiddleware(({ ctx }) => {
  if (ctx.user.role === 'admin') {
    return adminTemplate
  }
 
  return userTemplate
}))

Custom Error Handling

By default, the middleware throws a TRPCError with code FORBIDDEN. You can customize this behavior by providing a forbiddenError option:

Static Error

const permix = createPermix({
  forbiddenError: new TRPCError({
    code: 'FORBIDDEN',
    message: 'Custom forbidden message',
  })
})

Dynamic Error

You can also provide a function that returns different errors based on the entity and actions:

const permix = createPermix<Definition>({
  forbiddenError: ({ entity, actions, ctx }) => {
    if (entity === 'post' && actions.includes('create')) {
      return new TRPCError({
        code: 'FORBIDDEN',
        message: `User ${ctx.user.id} doesn't have permission to ${actions.join('/')} a ${entity}`,
      })
    }
 
    return new TRPCError({
      code: 'FORBIDDEN',
      message: 'You do not have permission to perform this action',
    })
  },
})

The forbiddenError handler receives:

  • ctx: Your tRPC context object
  • entity: The entity that was checked
  • actions: Array of actions that were checked

Advanced Usage

Async Permission Rules

You can use async functions in your permission setup:

const protectedProcedure = t.procedure.use(permix.setupMiddleware(async ({ ctx }) => {
  // Fetch user permissions from database
  const userPermissions = await getUserPermissions(ctx.user.id)
 
  return {
    post: {
      create: userPermissions.canCreatePosts,
      read: userPermissions.canReadPosts,
      update: userPermissions.canUpdatePosts,
      delete: userPermissions.canDeletePosts
    }
  }
}))

Dynamic Data-Based Permissions

You can check permissions based on the specific data being accessed:

const router = t.router({
  updatePost: protectedProcedure
    .input(z.object({ postId: z.string() }))
    .mutation(async ({ input, ctx }) => {
      const post = await getPostById(input.postId)
 
      // Check if user can update this specific post
      if (ctx.permix.check('post', 'update', post)) {
        // Update post logic
        return { success: true }
      }
 
      throw new TRPCError({
        code: 'FORBIDDEN',
        message: 'You cannot update this specific post'
      })
    })
})

On this page