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
}
Later, I started using CASL for permission management in a Vue application.
can('read', ['Post', 'Comment']);
can('manage', 'Post', { author: 'me' });
can('create', 'Comment');
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
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)
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>
)
}
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>
)
}
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>
)
}
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)