Permix

Elysia

Learn how to use Permix with Elysia

Overview

Permix provides integration for Elysia that allows you to easily check permissions in your routes. The integration can be created using the createPermix function.

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

import { Elysia } from 'elysia'
import { createPermix } from 'permix/elysia'

interface Post {
  id: string
  authorId: string
  title: string
  content: string
}

// Create your Permix instance
const permix = createPermix<{
  post: [
    { name: 'create', type: Post },
    { name: 'read', type: Post },
    { name: 'update', type: Post },
    { name: 'delete', type: Post },
  ]
}>()

// Initialize Elysia
const app = new Elysia()
  .onBeforeHandle(permix.setupMiddleware(({ context }) => {
    // You can access headers or other properties to determine permissions
    const isAuthorized = !!context.headers.authorization?.slice(7)

    return {
      post: {
        create: true,
        read: true,
        update: isAuthorized,
        delete: isAuthorized
      }
    }
  }))

The middleware preserves full type safety from your Permix definition, ensuring your permission checks are type-safe.

Checking Permissions

Use the checkMiddleware function in your Elysia routes to check permissions:

app.post('/posts', () => {
  // Create post logic here
  return { success: true }
}, {
  beforeHandle: permix.checkMiddleware('post.create')
})

// Check multiple actions
app.put('/posts/:id', () => {
  // Update post logic here
  return { success: true }
}, {
  beforeHandle: permix.checkMiddleware(c => c('post.read') && c('post.update'))
})

// Check all actions
app.delete('/posts/:id', () => {
  // Delete post logic here
  return { success: true }
}, {
  beforeHandle: permix.checkMiddleware('post.~all')
})

// Check any action
app.get('/posts', () => {
  // Get posts logic here
  return { posts: getAllPosts() }
}, {
  beforeHandle: permix.checkMiddleware('post.~any')
})

Accessing Permix Directly

You can access the Permix instance directly in your route handlers using get or getOrThrow:

app.get('/posts', (context) => {
  const { check } = permix.getOrThrow(context)

  // Check permissions manually
  if (check('post.read')) {
    // User has permission to read posts
    return { posts: getAllPosts() }
  } else {
    return { error: 'You do not have permission to read posts' }
  }
})

The get function returns the Permix instance with available methods, or null if the middleware has not run yet.

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
app.onBeforeHandle(permix.setupMiddleware(async ({ context }) => {
  const user = await getUserFromDb(context.headers.authorization?.slice(7))

  return user?.role === 'admin'
    ? adminTemplate()
    : userTemplate()
}))

Custom Error Handling

By default, the middleware returns a 403 Forbidden response. You can customize this behavior by providing an onForbidden handler:

Basic Error Handler

const permix = createPermix<Definition>({
  onForbidden: ({ context }) => {
    context.set.status = 403
    return { error: 'Custom forbidden message' }
  }
})

Dynamic Error Handler

You can also provide a handler that returns different responses based on the checked path:

const permix = createPermix<Definition>({
  onForbidden: ({ context, path }) => {
    context.set.status = 403

    if (path === 'post.create') {
      return {
        error: `You don't have permission for ${path}`
      }
    }

    return {
      error: 'You do not have permission to perform this action'
    }
  }
})

The onForbidden handler receives:

  • context: Elysia Context object
  • path: The permission path that was checked (or null for callback checks)
  • data: Optional entity data passed to the check

Advanced Usage

Async Permission Rules

You can use async functions in your permission setup:

app.onBeforeHandle(permix.setupMiddleware(async ({ context }) => {
  // Fetch user permissions from database
  const userId = context.headers.authorization?.slice(7)
  const userPermissions = await getUserPermissions(userId)

  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:

app.put('/posts/:id', async (context) => {
  const postId = context.params.id
  const post = await getPostById(postId)
  const { check } = permix.getOrThrow(context)

  // Check if user can update this specific post
  if (check('post.update', post)) {
    // Update post logic
    return { success: true }
  } else {
    return { error: 'You cannot update this post' }
  }
})

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