DEV Community

Cover image for 🔐 Modern Type-Safe Permissions Management via Permix
Valerii Strilets
Valerii Strilets

Posted on

🔐 Modern Type-Safe Permissions Management via Permix

In my many years of experience, I have worked extensively with permissions management, and early in my career I wrote solutions that looked like this:

if (user.role === 'admin') {
  // do something
}
Enter fullscreen mode Exit fullscreen mode

Later, I started using CASL for permission management in a Vue application.

can('read', ['Post', 'Comment']);
can('manage', 'Post', { author: 'me' });
can('create', 'Comment');
Enter fullscreen mode Exit fullscreen mode

But time goes on, CASL becomes older, and developers' needs grow, especially for type-safe libraries. Unfortunately, CASL couldn't satisfy my type validation needs and so I started thinking again about writing my own validation solution. But this time I wanted to make it as a library, as I already had experience with open-source.

Tries

A couple of weeks later, I started to create my own solution. However, nothing occurred to me until I watched a Web Dev Simplified video where he demonstrated an example of implementing permission management as he envisioned it. I really liked his approach because it was based on type-safety, which is exactly what I needed.

Permix

So I'm ready to present to you my permission management solution called Permix!

When creating Permix, the goal was to simplify DX as much as possible without losing type-safety and provide the necessary functionality.

That is why you only need to write the following code to get started:

import { createPermix } from 'permix'

const permix = createPermix<{
  post: {
    action: 'read'
  }
}>()

permix.setup({
  post: {
    read: true,
  }
})

permix.check('post', 'read') // true
Enter fullscreen mode Exit fullscreen mode

It looks too simple, so here's a more interesting example:

// You can take types from your database
interface User {
  id: string
  role: 'editor' | 'user'
}

interface Post {
  id: string
  title: string
  authorId: string
  published: boolean
}

interface Comment {
  id: string
  content: string
  authorId: string
}

// Create Permix to describe your permissions
const permix = createPermix<{
  post: {
    dataType: Post
    action: 'create' | 'read' | 'update'
  }
  comment: {
    dataType: Comment
    action: 'create' | 'read' | 'update'
  }
}>()

// Define permissions for different users
const editorPermissions = permix.template({
  post: {
    create: true,
    read: true,
    update: post => !post.published,
  },
  comment: {
    create: false,
    read: true,
    update: false,
  },
})

const userPermissions = permix.template(({ id: userId }: User) => ({
  post: {
    create: false,
    read: true,
    update: false,
  },
  comment: {
    create: true,
    read: true,
    update: comment => comment.authorId === userId,
  },
}))

// Setup permissions for signed in user
async function setupPermix() {
  const user = await getUser()
  const permissionsMap = {
    editor: () => editorPermissions(),
    user: () => userPermissions(user),
  }

  permix.setup(permissionsMap[user.role]())
}

// Call setupPermix where you need to setup permissions
setupPermix()

// Check if a user has permission to do something
const canCreatePost = permix.check('post', 'create')

const comment = await getComment()

const canUpdateComment = permix.check('comment', 'update', comment)
Enter fullscreen mode Exit fullscreen mode

Integrations

But a good library is not enough to satisfy a modern developer. That's why for the v1 release I decided to add integrations with modern frameworks like React, Vue, tRPC etc., let's take a look at one of them.

Next.js

Setup

First, create a Permix provider component that handles client-side setup:

'use client'

import { permix, setupPermix } from '@/lib/permix'
import { PermixProvider as Provider } from 'permix/react'
import { useEffect } from 'react'

export function PermixProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    setupPermix()
  }, [])

  return (
    <Provider permix={permix}>
      {children}
    </Provider>
  )
}
Enter fullscreen mode Exit fullscreen mode

Then, wrap your application with the PermixProvider and PermixHydrate components in your root layout:

import { permix, setupPermix } from '@/lib/permix'
import { dehydrate } from 'permix'
import { PermixHydrate } from 'permix/react'
import { PermixProvider } from './permix-provider'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  setupPermix()

  return (
    <html lang="en">
      <body>
        <PermixProvider>
          <PermixHydrate state={dehydrate(permix)}>
            {children}
          </PermixHydrate>
        </PermixProvider>
      </body>
    </html>
  )
}
Enter fullscreen mode Exit fullscreen mode

Usage

Use the usePermix hook and checking components in your components:

import { usePermix } from 'permix/react'
import { permix } from './lib/permix'
import { Check } from './lib/permix-components'

export default function Page() {
  const post = usePost()
  const { check, isReady } = usePermix(permix)

  if (!isReady) {
    return <div>Loading permissions...</div>
  }

  const canEdit = check('post', 'edit', post)

  return (
    <div>
      {canEdit ? (
        <button>Edit Post</button>
      ) : (
        <p>You don't have permission to edit this post</p>
      )}
      <Check entity="post" action="create">
        Can I create a post inside the Check component?
      </Check>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Examples

I created a lot of examples in the Permix repository

Summary

I'm very pleased with what we've done and wanted to share it with you. I hope for your feedback and thank you for your attention!

Go to the documentation to read more.

Top comments (0)