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 with a custom context key
const permix = createPermix<{
post: ['create', 'read', 'update', 'delete']
}>().contextKey('permissions')
// Create a protected procedure with Permix
const protectedProcedure = t.procedure.use(({ ctx, next }) => {
const isAdmin = ctx.user.role === 'admin'
return next({
ctx: permix.setupContext({
post: {
create: true,
read: true,
update: isAdmin,
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 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(c => c('post.read') && c('post.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 }
}),
})Accessing Permix in Procedures
Permix is automatically added to your tRPC context under the key you specified, so you can access it directly:
const router = t.router({
getPosts: protectedProcedure
.query(({ ctx }) => {
// Check permissions manually
if (ctx.permissions.check('post.read')) {
return getAllPosts()
}
throw new TRPCError({
code: 'FORBIDDEN',
message: 'You do not have permission to read posts'
})
})
})The ctx.permissions object provides:
check: Synchronously check a permissiondehydrate: Serialize the current rules for client hydrationtemplate: 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,
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 }) => {
return next({
ctx: permix.setupContext(
ctx.user.role === 'admin'
? adminTemplate()
: userTemplate()
),
})
})Custom Error Handling
By default, the middleware throws a TRPCError 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, ctx }) => {
throw new TRPCError({
code: 'FORBIDDEN',
message: `User ${ctx.user.id} doesn't have permission for ${path}`,
})
},
}).contextKey('permissions')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()
},
}).contextKey('permissions')The onForbidden handler receives:
path: The permission path that was checked (e.g.'post.create')data: Optional data passed to the checkctx: Your tRPC context objectnext: The middlewarenextfunction — call it to allow the request through
Advanced Usage
Async Permission Rules
You can use async functions in your permission setup:
const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
const userPermissions = await getUserPermissions(ctx.user.id)
return next({
ctx: permix.setupContext({
post: {
create: userPermissions.canCreatePosts,
read: userPermissions.canReadPosts,
update: userPermissions.canUpdatePosts,
delete: userPermissions.canDeletePosts
}
})
})
})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)
})