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 { initTRPC } from '@trpc/server'
import { createPermix } from 'permix/trpc'
interface Post {
id: string
authorId: string
title: string
content: string
}
interface Context {
user: {
id: string
role: string
}
}
// Initialize tRPC
const t = initTRPC.context<Context>().create()
// Create your Permix instance
const permix = createPermix<{
post: {
dataType: Post
action: 'create' | 'read' | 'update' | 'delete'
}
}>()
// Create a protected procedure with Permix
const protectedProcedure = t.procedure.use(({ ctx, next }) => {
// You can access ctx.user or other properties to determine permissions
const isAdmin = ctx.user.role === 'admin'
const p = permix.setup({
post: {
create: true,
read: true,
update: isAdmin,
delete: isAdmin
}
})
return next({
ctx: {
permix: p,
},
})
})
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 one method:
check
: Synchronously 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(({ ctx, next }) => {
const p = permix.setup(
ctx.user.role === 'admin'
? adminTemplate
: userTemplate
)
return next({
ctx: {
permix: p,
},
})
})
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 objectentity
: The entity that was checkedactions
: 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(async ({ ctx, next }) => {
// Fetch user permissions from database
const userPermissions = await getUserPermissions(ctx.user.id)
const p = permix.setup({
post: {
create: userPermissions.canCreatePosts,
read: userPermissions.canReadPosts,
update: userPermissions.canUpdatePosts,
delete: userPermissions.canDeletePosts
}
})
return next({
ctx: {
permix: p,
},
})
})
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'
})
})
})