# Comparison URL: https://permix.letstri.dev/docs/comparison Comparison with other libraries ## Overview [#overview] Permix is a library that provides a way to manage permissions in your application. It is designed to be used with React, Vue, etc. But not only Permix can manage permissions, there are other libraries that can do the same thing. ## Comparison [#comparison] | Feature | Permix | CASL | | ------------ | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | Type-safe | ✅ Native | ✅ Via [external type](https://casl.js.org/v6/en/advanced/typescript#permissions-inference) | | Saving rules | ✅ Via [template](/docs/guide/template) | ✅ Via [AppAbility](https://casl.js.org/v6/en/cookbook/cache-rules#the-issue) | | Hydration | ✅ Native | ⚠️ Via custom implementation | | Entity | ✅ Depends on props of a [passed object](/docs/guide/setup#type-based) | ⚠️ Class-based yes, but object-based via [external function](https://casl.js.org/v6/en/guide/subject-type-detection) | | Events | ✅ | ❌ | | Simple DX | ✅ Create instance, use built-in integrations | ❌ In CASL you need to manage a lot of stuff manually (type-safe, hydration, etc.) | | Modernity | ✅ Uses modern updates and features of each lib and framework | ❌ CASL was created a long time ago and hasn't updated the core | | Size | **2.64 kB** gzip (core) \[react 0.86 kB, vue 0.87 kB, solid 0.82 kB, next 1.00 kB, tanstack-start 2.05 kB, svelte \~2.17 kB, …] | **6.17 kB** min+gzip (core) \[@casl/react 0.62 kB] | Sizes are hard numbers from published builds — see [Bundle size](#bundle-size). Bracketed values are integration adapters imported on top of core. ## Bundle size [#bundle-size] All figures are **gzip**. Measured on **2026-06-02** from production builds. ### Permix [#permix] Built with `pnpm run build` in the `permix` package (`tsdown` for all entries except Svelte). | Entry | gzip | Notes | | ----------------------- | ----------: | ------------------------------------------------ | | **`permix` (core)** | **2.64 kB** | `dist/core/index.mjs` (7.96 kB raw) | | `permix/react` | 0.86 kB | adapter | | `permix/vue` | 0.87 kB | adapter | | `permix/solid` | 0.82 kB | adapter | | `permix/svelte` | \~2.17 kB | adapter (`dist/svelte/`, `svelte-package` build) | | `permix/next` | 1.00 kB | adapter | | `permix/tanstack-start` | 2.05 kB | adapter | | `permix/node` | 0.95 kB | adapter | | `permix/server` | 1.10 kB | adapter | | `permix/express` | 0.91 kB | adapter | | `permix/hono` | 0.88 kB | adapter | | `permix/fastify` | 1.04 kB | adapter | | `permix/elysia` | 0.88 kB | adapter | | `permix/trpc` | 0.96 kB | adapter | | `permix/orpc` | 0.92 kB | adapter | | `permix/effect` | 1.28 kB | adapter | | `permix/drizzle` | 0.89 kB | adapter | | `permix/drizzle/legacy` | 0.83 kB | adapter | Integration entries import `../core/index.mjs`, so a typical app ships **core + adapter** (for example React ≈ **2.64 + 0.86 ≈ 3.50 kB** gzip of published chunks before your bundler minifies further). ### CASL (`@casl/ability@7.0.0`, `@casl/react@7.0.0`) [#casl-caslability700-caslreact700] | Entry | gzip | Notes | | -------------------------- | ----------: | ------------------------------------------------------------------------ | | **`@casl/ability` (core)** | **6.17 kB** | minified + gzip, esbuild bundle of `createMongoAbility([])` | | `@casl/react` | 0.62 kB | published `dist/esm/index.mjs` only (adapter; ability loaded separately) | CASL core figure reflects what most apps ship after bundling and minification, not a single prebuilt file on disk. # Check URL: https://permix.letstri.dev/docs/guide/check Learn how to check permissions in your application ## Overview [#overview] Permix provides the `check` method for verifying permissions. It returns a boolean indicating whether the action is allowed. ## Dot paths [#dot-paths] Check a single action using a dot-separated path: ```ts permix.check('post.create') // returns true/false ``` ## Multiple actions [#multiple-actions] Combine multiple checks with a callback. All conditions must be truthy: ```ts // Check if both create and read are allowed permix.check(c => c('post.create') && c('post.read')) ``` ## All [#all] Use the `~all` special token to verify if all possible actions in a subtree are permitted: ```ts // Check if all post actions are allowed permix.check('post.~all') // Check if all actions in the entire permission tree are allowed permix.check('~all') ``` ## Any [#any] Use the `~any` special token to verify if any action in a subtree is permitted: ```ts // Check if any post action is allowed permix.check('post.~any') // Check if any action in the entire permission tree is allowed permix.check('~any') ``` ## Waiting for setup [#waiting-for-setup] When permissions might be set up asynchronously, wait for the instance to be ready before checking: ```ts setTimeout(() => { permix.setup({ post: { create: true } }) }, 1000) await permix.isReadyAsync() permix.check('post.create') ``` In most cases you should use `check` directly. Use `isReadyAsync()` when you need to ensure permissions are ready before checking, for example in route middleware. ## Data-Based [#data-based] You can define permissions that depend on the data being accessed: ```ts permix.setup({ post: { // Only allow updates if user is the author update: post => post.authorId === currentUserId, // Static permission read: true } }) // Check with data const post = { id: '1', authorId: 'user1' } permix.check('post.update', post) // true if currentUserId === 'user1' ``` You can still check permissions without providing the data, but dynamic rules that require entity data will return `false` in that case. ## Type Safety [#type-safety] Permix provides full type safety for your permissions: ```ts twoslash import { createPermix } from 'permix' const permix = createPermix<{ post: ['create', 'update'] }>() // @errors: 2345 permix.check('post.invalid-action') permix.check('invalid-entity.create') ``` Invalid paths cause a TypeScript error at compile time. At runtime, checking an undefined path throws `PermixRuleNotDefinedError`. ## Errors [#errors] Permix throws typed errors from `permix` when checks cannot run safely: | Error | When | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `PermixNotReadyError` | `check()` or `dehydrate()` runs before any rules exist (no `setup()` and no initial rules). | | `PermixRuleNotDefinedError` | `check()` targets a path that is not present in the current rules object, or is deeper than the rules tree (for example rules `{ post: true }` with `check('post.create')`). | After `hydrate()`, boolean rules are available immediately — `check()` works even while `isReady()` is still `false`. Function-based rules still need a follow-up `setup()` on the client. See [Hydration](/docs/guide/hydration) and [Ready State](/docs/guide/ready). `PermixNotFoundError` is thrown by server integrations when middleware was not registered on the request — see [Server](/docs/integrations/server) and [Node.js](/docs/integrations/node). # Events URL: https://permix.letstri.dev/docs/guide/events Learn how to handle permission updates in your application ## Overview [#overview] Permix provides an event system that allows you to react to permission changes in your application. Each event provides type-safe data and hooks to register handlers. ## Usage [#usage] You can register event handlers using the `hook` and `hookOnce` methods: ```ts const permix = createPermix<{ post: ['create', 'read'] }>() // The handler will be called every time setup is executed permix.hook('setup', () => { console.log('Permissions were updated') }) // The handler will be called only once permix.hookOnce('setup', () => { console.log('Permissions were updated once') }) // Calling `setup` triggers the `setup` event // and `ready` on the first successful setup permix.setup({ post: { create: true, read: true } }) ``` Available events: * `setup` - Triggered when permissions are updated through the `setup` method or `hydrate()`. * `ready` - Triggered **once** when the instance first becomes ready (first `setup()`, or initial rules passed to `createPermix`). Later `setup()` calls do not fire `ready` again. `hydrate()` alone does not trigger `ready`. * `check` - Triggered every time `check()` is called. The handler receives a context object with `path` and `data`. Use the `setup` event when UI or caches should refresh after every permission change. Use `ready` only for one-time bootstrap (for example, hiding a global loading shell). Use `check` for logging, analytics, or debugging permission evaluations. ## Check Event [#check-event] The `check` event fires before every permission evaluation and provides the path and data being checked: ```ts const permix = createPermix<{ post: ['create', { name: 'edit', type: { authorId: string }, required: true }] }>() permix.hook('check', ({ path, data }) => { console.log(`Checking permission: ${path}`, data) }) permix.setup({ post: { create: true, edit: (post) => post.authorId === currentUserId, }, }) permix.check('post.create') // logs: Checking permission: post.create undefined permix.check('post.edit', { authorId: '1' }) // logs: Checking permission: post.edit { authorId: '1' } ``` When using the callback form of `check()`, `path` will be `null`: ```ts permix.check(c => c('post.create') && c('post.edit', { authorId: '1' })) // logs: Checking permission: null undefined ``` # Hydration (SSR) URL: https://permix.letstri.dev/docs/guide/hydration Learn how to hydrate and dehydrate permissions in your application ## Overview [#overview] Hydration is the process of converting server-side state into client-side state. In Permix, hydration allows you to serialize permissions on the server and restore them on the client. Note that function-based permissions will be converted to `false` during dehydration since functions cannot be serialized to JSON. You should call `setup` method on the client side after hydration to fully restore function-based permissions. ## Usage [#usage] Permix provides two instance methods for handling hydration: * `dehydrate()` - Converts the current permissions state into a JSON-serializable format * `hydrate(state)` - Restores permissions from a previously dehydrated state ```ts twoslash import { createPermix } from 'permix' const permix = createPermix<{ post: [ { name: 'create', type: { isPublic: boolean } }, { name: 'read', type: { isPublic: boolean } }, ] }>() // Set up initial permissions permix.setup({ post: { create: true, read: post => !!post?.isPublic } }) // Dehydrate permissions to JSON const state = permix.dehydrate() // Result: { post: { create: true, read: false } } // Later, hydrate permissions from the state permix.hydrate(state) // isReady() is still false — call setup() to restore function-based rules // Hydrated booleans are checkable right away (even while not ready): permix.check('post.create') // true permix.check('post.read', { isPublic: true }) // false — function became false in JSON ``` `dehydrate()` throws `PermixNotReadyError` if you call it before `setup()` (or without initial rules). ## Server-Side Rendering [#server-side-rendering] Hydration is particularly useful in server-side rendering scenarios where you want to transfer permissions from the server to the client: ```ts twoslash // Express server import express from 'express' import { createPermix } from 'permix' const app = express() const permix = createPermix<{ post: ['create', 'read'] }>() app.get('/', (req, res) => { // Setup permissions on the server permix.setup({ post: { create: true, read: true } }) // Dehydrate permissions for client const dehydratedState = permix.dehydrate() // Send HTML with embedded permissions data res.send(`
`) }) ``` # Instance URL: https://permix.letstri.dev/docs/guide/instance Learn how to create a new Permix instance ## Overview [#overview] Instance is the main entry point for Permix that will check permissions in every returned method. To create an instance, you need to use the `createPermix` function. ## TypeScript [#typescript] Permix is built with TypeScript, providing type safety and validation. Using TypeScript enables autocompletion and compile-time checks for your permission definitions. ```ts twoslash title="/lib/permix-basic.ts" import { createPermix } from 'permix' const permix = createPermix<{ post: ['create', 'edit'] }>() ``` ### Generic type [#generic-type] Permix instance accepts a generic type to define permissions. #### Actions [#actions] List of action names for the entity. Use a tuple of strings for simple actions, or action spec objects (`{ name, type?, required? }`) when an action needs a typed entity. ```ts twoslash title="/lib/permix-actions.ts" import { createPermix } from 'permix' const permix = createPermix<{ post: ['create', 'edit'] // ^^^^^^ }>() ``` #### `type` [#type] Not required, but recommended. To type-check entity data in rule callbacks and `check`, add a `type` field on the action spec. Without it, callback data is `unknown`. ```ts twoslash title="/lib/permix-type.ts" import { createPermix } from 'permix' interface Post { id: string author: string content: string } const permix = createPermix<{ post: [ { name: 'create', type: Post }, { name: 'edit', type: Post }, ] }>() permix.setup({ post: { create: true, edit: post => post?.author === 'John Doe' // ^? } }) const somePost: Post = { id: '1', author: 'John Doe', content: 'Hello World' } const canEdit = permix.check('post.edit') // false const canEditWithPost = permix.check('post.edit', somePost) // true ``` #### `required` [#required] Not required, defaults to `false`. By default, when an action declares a `type`, the data argument in `check` is optional. Set `required: true` to require data for that action. ```ts twoslash title="/lib/permix-type-required.ts" import { createPermix } from 'permix' interface Post { id: string author: string content: string } const permix = createPermix<{ post: [ { name: 'create', type: Post, required: true }, { name: 'edit', type: Post }, ] }>() permix.setup({ post: { create: () => true, edit: post => post?.author === 'John Doe' // ^? } }) const somePost: Post = { id: '1', author: 'John Doe', content: 'Hello World' } // @errors: 2345 const canCreate = permix.check('post.create') const canEdit = permix.check('post.edit', somePost) // ✅ Valid ``` When `required` is `true`, TypeScript requires the data argument for that action. Use this when permission logic always depends on entity data. #### Flat definition [#flat-definition] You can also define permissions as a flat tuple without nesting: ```ts twoslash title="/lib/permix-flat.ts" import { createPermix } from 'permix' const permix = createPermix<['read', 'write']>() permix.setup({ read: true, write: false }) permix.check('read') // true ``` #### `Definition` [#definition] You can define your permission tree in a separate type alias and reuse it with `Rules`. ```ts twoslash title="/lib/permix-definition.ts" import type { Definition, Rules } from 'permix' import { createPermix } from 'permix' type PermissionsDefinition = { post: ['create', 'edit'] } async function getRules(): Promise> { // get user or something like that return { post: { create: true, edit: false } } } const permix = createPermix() permix.setup(await getRules()) ``` #### `$inferPath` [#inferpath] Use `$inferPath` to derive permission path types from an instance without restating the definition: ```ts title="/lib/permix-infer-path.ts" import { createPermix } from 'permix' const permix = createPermix<{ user: ['create'] job: ['remove'] }>() type PermissionPath = typeof permix.$inferPath // 'user.create' | 'job.remove' const path: PermissionPath = 'user.create' ``` ### Return type [#return-type] Each Permix instance provides a list of methods to manage and check permissions. These methods are documented in detail on their separate pages. ```ts twoslash title="/lib/permix-methods.ts" import { createPermix } from 'permix' const permix = createPermix() // @noErrors permix. // ^| ``` ## ValidateDefinition [#validatedefinition] When the same permission schema is shared between the server integration and the client bundle, wrap it with `ValidateDefinition` so TypeScript keeps the definition consistent across imports: ```ts title="/lib/permix-definition.ts" import type { ValidateDefinition } from 'permix' export type PermissionsDefinition = ValidateDefinition<{ post: ['create', 'read', 'update', 'delete'] }> ``` This helper is used in the [TanStack Start](/docs/integrations/tanstack-start) integration and in the repository examples. ## Merging definitions [#merging-definitions] Combine permission trees from multiple modules with the `MergePermix` type (for example, a core app schema plus a plugin schema): ```ts title="/lib/permix-merged.ts" import type { MergePermix } from 'permix' import { createPermix } from 'permix' type AppPermissions = { post: ['create', 'read'] } type PluginPermissions = { billing: ['view', 'update'] } type PermissionsDefinition = MergePermix const permix = createPermix() // Paths: 'post.create' | 'post.read' | 'billing.view' | 'billing.update' ``` When two definitions declare the same entity key with different actions, actions are concatenated. If one side is a branch and the other is a leaf list, the branch wins. ## JavaScript [#javascript] Not using TypeScript? Permix works perfectly fine even with plain JavaScript. ```ts title="/lib/permix.js" import { createPermix } from 'permix' const permix = createPermix() ``` ## Initial Rules [#initial-rules] You can provide initial rules when creating a Permix instance. This allows you to set up permissions immediately without calling `setup` separately. ```ts twoslash title="/lib/permix-initial.ts" import { createPermix } from 'permix' const permix = createPermix<{ post: ['create', 'edit'] }>({ post: { create: true, edit: false } }) // Permissions are immediately available console.log(permix.check('post.create')) // true console.log(permix.isReady()) // true ``` This is equivalent to: ```ts twoslash title="/lib/permix-initial-setup.ts" import { createPermix } from 'permix' const permix = createPermix<{ post: ['create', 'edit'] }>() permix.setup({ post: { create: true, edit: false } }) ``` Initial rules are useful when you have permissions that are known at initialization time and don't need to be loaded asynchronously. You still should pass generic type to `createPermix` even if you provide initial rules. Otherwise, Permix will not be able to validate your permissions. # Ready State URL: https://permix.letstri.dev/docs/guide/ready Learn how to use the `isReady()` method to check if permissions are ready to use. ## Overview [#overview] Sometimes you need to know when permissions are ready to use. For example, you might want to wait for permissions to be ready before rendering a component. That's where the `isReady()` and `isReadyAsync()` methods come in. `hydrate()` alone does **not** make the instance ready. You must call `setup()` after hydration to restore function-based rules and mark the instance as ready. ## Usage [#usage] ### Basic [#basic] Permix provides an `isReady()` method to check if permissions have been properly initialized: ```ts twoslash import { createPermix } from 'permix' const permix = createPermix<{ post: ['create', 'read'] }>() console.log(permix.isReady()) // false // After setup completes permix.setup({ post: { create: true, read: true } }) console.log(permix.isReady()) // true ``` ### Async [#async] If you need to wait for permissions to be ready in an async context, you can use the `isReadyAsync()` method. This returns a promise that resolves when permissions are ready: ```ts import { createPermix } from 'permix' const permix = createPermix<{ post: ['create'] }>() async function init() { await permix.isReadyAsync() // Permissions are now ready to use const canCreate = permix.check('post.create') } ``` ### SSR [#ssr] This is particularly useful in SSR applications when using function-based permissions, since the dehydration process converts all function permissions to `false` until they are properly rehydrated on the client. Read more about [hydration](/docs/guide/hydration) to learn how to transfer permissions from the server to the client. ```ts // Server instance import { createPermix } from 'permix' const serverPermix = createPermix<{ post: [{ name: 'read', type: { isPublic: boolean } }] }>() serverPermix.setup({ post: { read: post => !!post?.isPublic, }, }) const state = serverPermix.dehydrate() // { post: { read: false } } — functions evaluated without data // Client instance (separate from the server) const clientPermix = createPermix<{ post: [{ name: 'read', type: { isPublic: boolean } }] }>() clientPermix.hydrate(state) console.log(clientPermix.isReady()) // false // Boolean rules from hydration work immediately: console.log(clientPermix.check('post.read')) // false // Restore function-based rules and mark the instance ready clientPermix.setup({ post: { read: post => !!post?.isPublic, }, }) console.log(clientPermix.isReady()) // true console.log(clientPermix.check('post.read', { isPublic: true })) // true ``` The `ready` event fires **once** on the first `setup()` (or when initial rules are passed to `createPermix`). Register `permix.hook('ready', ...)` before that first `setup()` if you need to react to readiness — not after a later `setup()` on an already-ready instance. # Relationship-Based Access Control URL: https://permix.letstri.dev/docs/guide/rebac How to model ReBAC patterns with Permix using closures and data-based rules ## Overview [#overview] Relationship-Based Access Control (ReBAC) grants permissions based on how entities relate to each other — ownership, team membership, sharing, and so on — rather than just roles or static flags. Permix already supports ReBAC patterns out of the box. Because rule functions receive the resource at **check time** and capture the actor at **setup time**, you can encode any relationship predicate without a new API. ## Why no new API is needed [#why-no-new-api-is-needed] When you call `setup()`, each rule closure captures the current user (the **actor**). When you call `check()`, you pass the resource. The closure decides yes/no by inspecting the relationship between the two: ```ts permix.setup({ doc: { update: (doc) => doc.authorId === currentUser.id, }, }) ``` This is ReBAC: the permission depends on a **relation** (author ↔ document) rather than a role flag. The sections below show three progressively richer patterns. ## Ownership [#ownership] The simplest relation — the user who created a resource can modify it. ```ts import { createPermix } from 'permix' interface Doc { id: string authorId: string } const permix = createPermix<{ doc: [ 'read', { name: 'update', type: Doc, required: true }, { name: 'delete', type: Doc, required: true }, ] }>() const currentUser = { id: 'user-1' } permix.setup({ doc: { read: true, update: (doc) => doc.authorId === currentUser.id, delete: (doc) => doc.authorId === currentUser.id, }, }) const myDoc = { id: 'doc-1', authorId: 'user-1' } const otherDoc = { id: 'doc-2', authorId: 'user-2' } permix.check('doc.update', myDoc) // true permix.check('doc.update', otherDoc) // false ``` ## Team membership [#team-membership] Users belong to teams; a team member can read any document owned by their team. ```ts import { createPermix } from 'permix' interface Doc { id: string authorId: string teamId: string } interface User { id: string teamIds: string[] } const permix = createPermix<{ doc: [ { name: 'read', type: Doc, required: true }, { name: 'update', type: Doc, required: true }, ] }>() function setupForUser(me: User) { permix.setup({ doc: { read: (doc) => me.teamIds.includes(doc.teamId), update: (doc) => doc.authorId === me.id, }, }) } const alice: User = { id: 'alice', teamIds: ['team-a'] } setupForUser(alice) const teamDoc = { id: 'doc-1', authorId: 'bob', teamId: 'team-a' } const otherTeamDoc = { id: 'doc-2', authorId: 'charlie', teamId: 'team-b' } permix.check('doc.read', teamDoc) // true — same team permix.check('doc.read', otherTeamDoc) // false — different team permix.check('doc.update', teamDoc) // false — alice is not the author ``` You can extract this into a reusable [template](/docs/guide/template): ```ts const memberPermissions = permix.template((me: User) => ({ doc: { read: (doc) => me.teamIds.includes(doc.teamId), update: (doc) => doc.authorId === me.id, }, })) setupForUser(alice) // is equivalent to: permix.setup(memberPermissions(alice)) ``` ## Document sharing [#document-sharing] A sharing model where documents can be shared with individual users at different levels: `read`, `write`, or `admin`. Combine ownership and sharing in a single rule set. ```ts import { createPermix } from 'permix' interface Doc { id: string authorId: string teamId: string } type ShareLevel = 'read' | 'write' | 'admin' // In a real app this would come from your database const shares = new Map>() function hasShare(docId: string, userId: string, minLevel: ShareLevel): boolean { const level = shares.get(docId)?.get(userId) if (!level) return false const rank: Record = { read: 0, write: 1, admin: 2 } return rank[level] >= rank[minLevel] } const permix = createPermix<{ doc: [ { name: 'read', type: Doc, required: true }, { name: 'update', type: Doc, required: true }, { name: 'admin', type: Doc, required: true }, ] }>() interface User { id: string teamIds: string[] } function setupForUser(me: User) { const isOwner = (doc: Doc) => doc.authorId === me.id const isTeammate = (doc: Doc) => me.teamIds.includes(doc.teamId) permix.setup({ doc: { read: (doc) => isOwner(doc) || isTeammate(doc) || hasShare(doc.id, me.id, 'read'), update: (doc) => isOwner(doc) || hasShare(doc.id, me.id, 'write'), admin: (doc) => isOwner(doc) || hasShare(doc.id, me.id, 'admin'), }, }) } ``` You can combine checks using the callback form: ```ts const doc: Doc = { id: 'doc-1', authorId: 'alice', teamId: 'team-a' } // "Can the user either read OR administrate this doc?" permix.check((c) => c('doc.read', doc) || c('doc.admin', doc)) ``` ## When to reach for something else [#when-to-reach-for-something-else] The closure pattern works well when the relation data is **already in memory** at `setup()` or `check()` time. If your relations require async lookups from a database on every check, or you need a graph traversal engine (e.g. hierarchical folder permissions), the pattern above may become cumbersome. Follow [this issue](https://github.com/letstri/permix/issues/25) for updates on first-class ReBAC support in a future version. # Setup URL: https://permix.letstri.dev/docs/guide/setup Learn how to setup permissions in your project ## Overview [#overview] After creating Permix instance, you need to define permissions with `setup` method. You can call `setup` in any time with any permissions and Permix will replace the previous permissions. You always should describe all permissions in the `setup` method that was defined in the Permix generic type. For role separation, you can use the [`template`](/docs/guide/template) method. ### Object definition [#object-definition] ```ts const permix = createPermix<{ post: ['create'] comment: ['create', 'update'] }>() permix.setup({ post: { create: true, }, comment: { create: true, update: true, } }) ``` You can also use `enum` based permissions. See [Enum-based](https://github.com/letstri/permix/tree/main/examples/enum-based) for more information. ## Initial [#initial] You can set up initial rules directly when creating a Permix instance by passing them as the first parameter to `createPermix`. ```ts twoslash import { createPermix } from 'permix' const permix = createPermix<{ post: ['create', 'read'] comment: ['create', 'read', 'update'] }>({ post: { create: true, read: true, }, comment: { create: false, read: true, update: true, }, }) // The instance is immediately ready to use permix.check('post.create') // true permix.isReady() // true ``` When using initial rules, the Permix instance is immediately ready to use without calling `setup` first. ## Type-Based [#type-based] When creating a Permix instance, you can attach a `type` to actions that need entity data in rule callbacks. This allows you to check permissions for specific data entities. So instead of `boolean` you can use functions to check permissions. ```ts twoslash import { createPermix } from 'permix' interface Post { id: string authorId: string } const permix = createPermix<{ post: [{ name: 'update', type: Post }] comment: ['update'] }>() // @noErrors permix.setup({ post: { update: post => post. // ^| } comment: { update: comment => c // ^? } }) ``` ### Required [#required] By default, a `type` on an action makes the data argument optional in `check`. Set `required: true` to require it. ```ts twoslash import { createPermix } from 'permix' interface Post { id: string authorId: string } interface Comment { id: string postId: string } const permix = createPermix<{ post: [{ name: 'update', type: Post, required: true }] comment: [{ name: 'update', type: Comment }] }>() // @noErrors permix.setup({ post: { update: post => !!post.authorId // ^? } comment: { update: comment => !!comment.postId // ^? } }) ``` ### unknown [#unknown] If you cannot define entity types in the Permix instance, callback data is `unknown` by default, but you can still narrow it in the `setup` method. This approach is not recommended as it reduces type safety and IDE support. ```ts twoslash import { createPermix } from 'permix' const currentUser = { id: 'user-1' } const permix = createPermix<{ comment: ['update'] }>() // @noErrors permix.setup({ comment: { update: (comment: { authorId: string }) => comment.authorId === currentUser.id } }) ``` ## Dynamic [#dynamic] You can use async functions to fetch permissions from external sources and then set them up. See the [template](/docs/guide/template) for more examples and patterns. ```ts import { createPermix } from 'permix' const permix = createPermix<{ post: ['create', 'read', 'update', 'delete'] comment: ['create', 'read', 'update', 'delete'] }>() // Fetch permissions from API async function loadUserPermissions(userId: string) { const permissions = await getPermissionsFromAnyPlace() permix.setup({ post: { create: permissions.includes('post:create'), read: permissions.includes('post:read'), update: permissions.includes('post:update'), delete: permissions.includes('post:delete'), }, comment: { create: permissions.includes('comment:create'), read: permissions.includes('comment:read'), update: permissions.includes('comment:update'), delete: permissions.includes('comment:delete'), }, }) } // Usage await loadUserPermissions('user-123') ``` ## Typed rules factory [#typed-rules-factory] Use `createRules` when rules live in a separate module but must stay aligned with your schema: ```ts twoslash title="/lib/rules.ts" import { createPermix, createRules } from 'permix' type PermissionsDefinition = { post: ['create', 'read'] } export const postRules = createRules({ post: { create: true, read: true, }, }) export const permix = createPermix() permix.setup(postRules) ``` ## Getting Rules [#getting-rules] You can get the current rules from an existing Permix instance using the `getRules` method: ```ts // Get current rules const rules = permix.getRules() ``` The `getRules` method returns the exact rules object that was set using `setup`, including any permission functions. # Template URL: https://permix.letstri.dev/docs/guide/template Learn how to define permissions using templates ## Overview [#overview] Permix provides a `template` method that allows you to define permissions in a separate location from where they are set up. This is useful for organizing permission definitions and reusing them across different parts of your application. Templates are type-checked at compile time, ensuring your permission definitions match the instance definition. ## Basic Usage [#basic-usage] The simplest way to use templates is to define static permissions: ```ts const adminPermissions = permix.template({ post: { create: true, read: true } }) // Later, use the template to setup permissions permix.setup(adminPermissions()) ``` ## Dynamic Templates [#dynamic-templates] Templates can accept parameters to create dynamic permissions based on runtime values: ```ts interface User { id: string role: string } const userPermissions = permix.template(({ id: userId }: User) => ({ post: { create: true, read: true, update: post => post?.authorId === userId } })) // Use with specific user data const user = await getUser() permix.setup(userPermissions(user)) ``` ## Type Safety [#type-safety] Templates maintain full type safety from your Permix instance definition: ```ts twoslash import { createPermix } from 'permix' const permix = createPermix<{ post: ['create'] }>() // @errors: 2353 // This will cause a TypeScript error const invalidTemplate = permix.template({ post: { edit: true } }) ``` ## Role-Based Example [#role-based-example] Templates are particularly useful for role-based permission systems: ```ts const editorPermissions = permix.template({ post: { create: true, read: true, update: post => !post?.published, delete: post => !post?.published } }) const userPermissions = permix.template(({ id: userId }: User) => ({ post: { create: false, read: true, update: post => post?.authorId === userId, delete: false } })) // Setup based on user role async function setupPermissions() { const user = await getUser() const permissionsMap = { editor: () => editorPermissions(), user: () => userPermissions(user) } return permix.setup(permissionsMap[user.role]()) } ``` ## Standalone Templates [#standalone-templates] You can define permission templates outside of the Permix instance using the `Rules` type. This is useful when you want to organize your permission logic in separate files: ```ts twoslash title="/lib/permix-template-standalone.ts" import type { Rules } from 'permix' import { createPermix } from 'permix' type PermissionsDefinition = { post: [ { name: 'create', type: { id: string; authorId: string } }, { name: 'read', type: { id: string; authorId: string } }, { name: 'update', type: { id: string; authorId: string } }, { name: 'delete', type: { id: string; authorId: string } }, ] } // It can be in separate file and imported here const permix = createPermix() // Create a standalone template function function userPermissions(userId: string, role: 'admin' | 'user'): Rules { return { post: { create: role === 'admin', read: true, update: role === 'admin' ? true : (post) => post?.authorId === userId, delete: role === 'admin' } } } // Later, use it with your Permix instance const permissions = userPermissions('1', 'admin') permix.setup(permissions) ``` This approach allows you to: * Keep permission logic separate from your Permix instance * Reuse permission templates across different parts of your application * Maintain full type safety with your Permix definition # Introduction URL: https://permix.letstri.dev/docs The type-safe permission management you've always needed ## Idea [#idea] 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: ```ts if (user.role === 'admin') { // do something } ``` Later, I started using [CASL](https://casl.js.org) for permissions management in a [Vue](https://vuejs.org/) application. ```ts 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. ## Implementation [#implementation] I started to create my own solution. However, nothing occurred to me until I watched a Web Dev Simplified [video](https://www.youtube.com/watch?v=5GG-VUvruzE) 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. So I'm ready to present to you my permission management solution called Permix! ## DX [#dx] 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: ```ts twoslash import { createPermix } from 'permix' const permix = createPermix<{ post: ['read'] }>() permix.setup({ post: { read: true, } }) const canReadPost = permix.check('post.read') // true ``` It looks too simple, so here's a more interesting example: ```ts twoslash import { createPermix } from 'permix' // 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 definition to describe your permissions type PermissionsDefinition = { post: [ { name: 'create', type: Post }, { name: 'read', type: Post }, { name: 'update', type: Post }, { name: 'delete', type: Post }, ] comment: [ { name: 'create', type: Comment }, { name: 'read', type: Comment }, { name: 'update', type: Comment }, ] } const permix = createPermix() // Define permissions for different users const editorPermissions = permix.template({ post: { create: true, read: true, update: post => !post?.published, delete: post => !post?.published, }, comment: { create: false, read: true, update: false, }, }) const userPermissions = permix.template(({ id: userId }: User) => ({ post: { create: false, read: true, update: false, delete: false, }, comment: { create: true, read: true, update: comment => comment?.authorId === userId, }, })) async function getUser() { // Imagine that this function is fetching user from database return { id: '1', role: 'editor' as const, } } // 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') async function getComment() { // Imagine that this function is fetching comment from database return { id: '1', content: 'Hello, world!', authorId: '1', } } const comment = await getComment() const canUpdateComment = permix.check('comment.update', comment) ``` ## Benefits [#benefits] What are the benefits of using Permix? * 100% type-safe without writing TypeScript (except for initialization) * Single source of truth for your entire app * Perfect match for TypeScript monorepos * Zero dependencies * Built-in SSR via [dehydrate / hydrate](/docs/guide/hydration) * Relationship-based rules with closures — see [ReBAC patterns](/docs/guide/rebac) * Large number of integrations for different frameworks, such as [React](/docs/integrations/react), [Vue](/docs/integrations/vue), [Express](/docs/integrations/express), and more. ## Core concepts [#core-concepts] | Topic | Guide | | ----------------------- | ---------------------------------- | | Schema and instance API | [Instance](/docs/guide/instance) | | Assigning rules | [Setup](/docs/guide/setup) | | Checking permissions | [Check](/docs/guide/check) | | Role presets | [Template](/docs/guide/template) | | SSR transfer | [Hydration](/docs/guide/hydration) | | Async bootstrap | [Ready state](/docs/guide/ready) | | Permission change hooks | [Events](/docs/guide/events) | ## Ready? [#ready] Ready to take Permix to your project? Let's go to the [Quick Start](/docs/quick-start) page. # Drizzle URL: https://permix.letstri.dev/docs/integrations/drizzle Auto-generate a Permix definition and CRUD rules from your Drizzle schema ## Overview [#overview] Permix ships with a Drizzle integration that takes your existing Drizzle schema and produces a fully type-safe Permix instance with one permission entity per table. By default each table receives the four CRUD actions (`create`, `read`, `update`, `delete`), but the action list can be customised. Two entry points are exposed so you can choose the one that matches your installed Drizzle version: | Import path | Drizzle version | Notes | | ----------------------- | --------------------- | ----------------------------------------------------------------------------------- | | `permix/drizzle` | **v1** (`>=1.0.0-rc`) | Uses Drizzle's official `extractTablesFromSchema` helper. Detects tables and views. | | `permix/drizzle/legacy` | **v0** (`>=0.30 <1`) | Detects tables only via `is(value, Table)`. | Both entry points expose the same API surface, so migrating between them is a one-line import change once you upgrade Drizzle. Before getting started with the Drizzle integration, make sure you've completed the initial setup steps in the [Quick Start](/docs/quick-start) guide. ## Setup [#setup] Define your Drizzle schema as usual, then create a Permix instance from it. ### Drizzle v1 [#drizzle-v1] ```ts twoslash title="/lib/drizzle-v1.ts" import { defineRelations } from 'drizzle-orm' import { integer, pgTable, serial, text } from 'drizzle-orm/pg-core' import { createPermix } from 'permix/drizzle' const users = pgTable('users', { id: serial('id').primaryKey(), name: text('name').notNull(), }) const posts = pgTable('posts', { id: serial('id').primaryKey(), title: text('title').notNull(), authorId: integer('author_id').notNull().references(() => users.id), }) const relations = defineRelations({ users, posts }, r => ({ posts: { author: r.one.users({ from: r.posts.authorId, to: r.users.id }) }, })) const schema = { users, posts, relations } const permix = createPermix(schema) permix.setup({ users: { create: true, read: true, update: false, delete: false }, posts: { create: true, read: true, update: true, delete: false }, }) permix.check('users.read') // true permix.check('posts.delete') // false ``` ### Drizzle v0 [#drizzle-v0] ```ts twoslash title="/lib/drizzle-legacy.ts" import { relations } from 'drizzle-orm/_relations' import { integer, pgTable, serial, text } from 'drizzle-orm/pg-core' import { createPermix } from 'permix/drizzle/legacy' const users = pgTable('users', { id: serial('id').primaryKey(), name: text('name').notNull(), }) const posts = pgTable('posts', { id: serial('id').primaryKey(), title: text('title').notNull(), authorId: integer('author_id').notNull().references(() => users.id), }) const usersRelations = relations(users, ({ many }) => ({ posts: many(posts), })) const schema = { users, posts, usersRelations } const permix = createPermix(schema) permix.setup({ users: { create: true, read: true, update: false, delete: false }, posts: { create: true, read: true, update: true, delete: false }, }) permix.check('users.read') // true permix.check('posts.delete') // false ``` The returned instance is a regular Permix object, so every API you already know — `check`, `setup`, `template`, `dehydrate`, `hydrate`, `hook`, `isReady`, etc. — is available unchanged. ## Customising Actions [#customising-actions] If CRUD isn't quite what you want, pass an explicit `actions` tuple. Use `as const` so the literal action names flow through to your check sites: ```ts const permix = createPermix(schema, { actions: ['view', 'edit', 'archive'] as const, }) permix.setup({ users: { view: true, edit: false, archive: false }, posts: { view: true, edit: true, archive: false }, }) permix.check('posts.edit') // true permix.check('users.archive') // false ``` ## Discovering Tables and Actions at Runtime [#discovering-tables-and-actions-at-runtime] Both the detected table list and the action list are exposed on the instance, which is handy for building UIs or seeding rules dynamically: ```ts const permix = createPermix(schema) permix.tables // ['users', 'posts', ...] permix.actions // readonly ['create', 'read', 'update', 'delete'] ``` ## Notes [#notes] * **v1** detects both tables **and views** (via Drizzle's `extractTablesFromSchema`). **v0** detects tables only. * Both versions skip non-entity exports automatically, so `import * as schema` works as-is. In v1 a `defineRelations(...)` object is filtered out; in v0 the same is true of `relations(...)` calls. * Tables from any Drizzle dialect (`drizzle-orm/pg-core`, `drizzle-orm/mysql-core`, `drizzle-orm/sqlite-core`) are detected, so a single schema can mix dialects if needed. * Drizzle is declared as an optional peer dependency. You only need to install it if you plan to use a `permix/drizzle*` entry point. * The permission keys default to the **schema export names**, not the SQL table names. If you exported `pgTable('app_users', { ... })` as `users`, you check `users.read`, not `app_users.read`. # Effect URL: https://permix.letstri.dev/docs/integrations/effect Learn how to use Permix with Effect ## Overview [#overview] Permix provides a first-class Effect integration that exposes your permissions as an Effect service via a `Context` tag and `Layer` constructors. Checks return `Effect`, so you decide how to handle denial — in any Effect program, HTTP handler, or RPC service. Unlike the server integrations (which lock the surface down to `check` / `dehydrate` / `template`), the Effect integration exposes the **full** Permix instance — including `setup`, `hydrate`, `isReady`, hooks, and `getRules` — so it works equally well in client-side or long-running Effect programs. The integration depends only on `effect` (no `@effect/platform` required), so it works everywhere Effect runs. Before getting started with the Effect integration, make sure you've completed the initial setup steps in the [Quick Start](/docs/quick-start) guide. ## Installation [#installation] ```bash npm install permix effect ``` ## Setup [#setup] Create your Permix Effect instance using `createPermix`: ```ts import { Effect } from 'effect' import { createPermix } from 'permix/effect' const permix = createPermix<{ post: ['create', 'read', 'update'] user: ['delete'] }>() ``` ## Providing Rules [#providing-rules] ### Static rules [#static-rules] Use `permix.layer(rules)` to provide a `Layer` with fixed rules: ```ts const PermixLive = permix.layer({ post: { create: true, read: true, update: false }, user: { delete: false }, }) ``` ### Dynamic rules from another service [#dynamic-rules-from-another-service] Use `permix.layerSetup(effect)` when rules depend on other services (e.g. the current user). Requirements flow through automatically: ```ts import { Context } from 'effect' interface User { id: string; role: 'admin' | 'member' } class CurrentUser extends Context.Tag('CurrentUser')() {} const PermixLive = permix.layerSetup(Effect.gen(function* () { const user = yield* CurrentUser return { post: { create: user.role === 'admin', read: true, update: user.role === 'admin', }, user: { delete: user.role === 'admin' }, } })) ``` ## Checking Permissions [#checking-permissions] `permix.check(...)` returns `Effect`. Use it inside `Effect.gen`: ```ts const program = Effect.gen(function* () { if (yield* permix.check('post.create')) { return 'created' } return yield* Effect.fail(new Error('Forbidden')) }) Effect.runPromise(program.pipe(Effect.provide(PermixLive))) ``` ## Dehydrating Permissions [#dehydrating-permissions] Serialize the current rules into a JSON-safe object: ```ts const state = Effect.gen(function* () { return yield* permix.dehydrate() }) // { post: { create: true, read: true, update: false }, user: { delete: false } } ``` ## Using Templates [#using-templates] Create reusable permission rule sets with `template`: ```ts const adminTemplate = permix.template({ post: { create: true, read: true, update: true }, user: { delete: true }, }) const PermixLive = permix.layer(adminTemplate()) ``` ## Runtime Updates [#runtime-updates] Because the Effect integration exposes the full Permix instance, you can update permissions at runtime — useful on the client or in long-running programs where rules change after the initial setup (e.g. a role change). Start from an empty layer (call `permix.layer()` with no rules) and configure it later with `setup`: ```ts const program = Effect.gen(function* () { // Not ready yet yield* permix.isReady() // false // Configure once permissions are available yield* permix.setup({ post: { create: true, read: true, update: false }, user: { delete: false }, }) yield* permix.isReady() // true return yield* permix.check('post.create') }) Effect.runPromise(program.pipe(Effect.provide(permix.layer()))) ``` ### Hydration [#hydration] Restore previously serialized permissions (e.g. from SSR or storage) with `hydrate`: ```ts const program = Effect.gen(function* () { yield* permix.hydrate(serializedState) return yield* permix.check('post.read') }) ``` `hydrate` does not mark the instance ready and cannot restore function-based rules. Follow with `setup` using the full rule set when you need dynamic checks or `isReady`. See the [Hydration guide](/docs/guide/hydration). ### Reacting to Changes [#reacting-to-changes] Register hooks that fire when permissions are set up. `hook` yields a function that removes the listener: ```ts const program = Effect.gen(function* () { const remove = yield* permix.hook('setup', () => { console.log('Permissions updated') }) // ...later remove() }) ``` ### Inspecting Rules [#inspecting-rules] Read the current rules object (or `null` if not set up yet) with `getRules`: ```ts const rules = Effect.gen(function* () { return yield* permix.getRules() }) ``` ## Full Example [#full-example] ```ts import { Context, Effect, Layer } from 'effect' import { createPermix } from 'permix/effect' // 1. Define permissions const permix = createPermix<{ post: ['create', 'read', 'update'] user: ['delete'] }>() // 2. Define a service for the current user interface User { id: string; role: 'admin' | 'member' } class CurrentUser extends Context.Tag('CurrentUser')() {} // 3. Build a Layer that derives rules from CurrentUser const PermixLive = permix.layerSetup(Effect.gen(function* () { const user = yield* CurrentUser return { post: { create: user.role === 'admin', read: true, update: user.role === 'admin', }, user: { delete: user.role === 'admin' }, } })) // 4. Use in a program const program = Effect.gen(function* () { const canCreate = yield* permix.check('post.create') const canRead = yield* permix.check('post.read') return { canCreate, canRead } }) // 5. Provide the layers and run const CurrentUserLive = Layer.succeed(CurrentUser, { id: '1', role: 'admin' }) Effect.runPromise( program.pipe( Effect.provide(PermixLive), Effect.provide(CurrentUserLive), ), ).then(console.log) // { canCreate: true, canRead: true } ``` ## Multiple Instances [#multiple-instances] You can create multiple Permix instances with different `id` values. They coexist on the same Effect context without conflict: ```ts const admin = createPermix({ id: 'admin' }) const guest = createPermix({ id: 'guest' }) const program = Effect.gen(function* () { const adminCreate = yield* admin.check('post.create') const guestCreate = yield* guest.check('post.create') return { adminCreate, guestCreate } }) program.pipe( Effect.provide(admin.layer({ ... })), Effect.provide(guest.layer({ ... })), ) ``` # Elysia URL: https://permix.letstri.dev/docs/integrations/elysia Learn how to use Permix with Elysia ## Overview [#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](/docs/quick-start) guide. ## Setup [#setup] Here's a basic example of how to use the Permix middleware with Elysia: ```ts 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 [#checking-permissions] Use the `checkMiddleware` function in your Elysia routes to check permissions: ```ts 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 [#accessing-permix-directly] You can access the Permix instance directly in your route handlers using `get` or `getOrThrow`: ```ts 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 [#using-templates] Permix provides a template helper to create reusable permission rule sets: ```ts // 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 [#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 [#basic-error-handler] ```ts const permix = createPermix({ onForbidden: ({ context }) => { context.set.status = 403 return { error: 'Custom forbidden message' } } }) ``` ### Dynamic Error Handler [#dynamic-error-handler] You can also provide a handler that returns different responses based on the checked path: ```ts const permix = createPermix({ 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 [#advanced-usage] ### Async Permission Rules [#async-permission-rules] You can use async functions in your permission setup: ```ts 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 [#dynamic-data-based-permissions] You can check permissions based on the specific data being accessed: ```ts 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 [#hooks] You can register hooks at the factory level to listen for events across all requests: ```ts permix.hook('check', ({ path, data }) => { console.log(`Permission checked: ${path}`, data) }) ``` # Express URL: https://permix.letstri.dev/docs/integrations/express Learn how to use Permix with Express ## Overview [#overview] Permix provides middleware for Express that allows you to easily check permissions in your routes. The middleware can be created using the `createPermix` function. Before getting started with Express integration, make sure you've completed the initial setup steps in the [Quick Start](/docs/quick-start) guide. ## Setup [#setup] Here's a basic example of how to use the Permix middleware with Express: ```ts import express from 'express' import { createPermix } from 'permix/express' interface Post { id: string authorId: string title: string content: string } // Initialize Express const app = express() // Create your Permix instance const permix = createPermix<{ post: [ { name: 'create', type: Post }, { name: 'read', type: Post }, { name: 'update', type: Post }, ] }>() // Set up the middleware with your permission rules app.use(permix.setupMiddleware(({ req }) => { // You can access req.user or other properties to determine permissions return { post: { create: true, read: true, update: false } } })) ``` The middleware preserves full type safety from your Permix definition, ensuring your permission checks are type-safe. ## Checking Permissions [#checking-permissions] Use the `checkMiddleware` function in your Express routes to check permissions: ```ts app.post('/posts', permix.checkMiddleware('post.create'), (req, res) => { res.json({ success: true }) }) // Check multiple actions app.put('/posts/:id', permix.checkMiddleware(c => c('post.read') && c('post.update')), (req, res) => { res.json({ success: true }) }) // Check all actions app.delete('/posts/:id', permix.checkMiddleware('post.~all'), (req, res) => { res.json({ success: true }) }) // Check any action app.get('/posts', permix.checkMiddleware('post.~any'), (req, res) => { res.json({ posts: getAllPosts() }) }) ``` ## Accessing Permix Directly [#accessing-permix-directly] You can access the Permix instance directly in your route handlers using the `get` function: ```ts app.get('/posts', (req, res) => { const { check } = permix.get(req) // Check permissions manually if (check('post.read')) { // User has permission to read posts res.json({ posts: getAllPosts() }) } else { res.status(403).json({ error: 'You do not have permission to read posts' }) } }) ``` The `get` function returns the Permix instance with available methods. ## Using Templates [#using-templates] Permix provides a template helper to create reusable permission rule sets: ```ts // Create a template for admin permissions const adminTemplate = permix.template({ post: { create: true, read: true, update: true } }) // Use the template in your middleware app.use(permix.setupMiddleware(({ req }) => { // You can still customize the template based on request data if (req.user?.role === 'admin') { return adminTemplate() } return { post: { create: false, read: true, update: false } } })) ``` ## Custom Error Handling [#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 [#basic-error-handler] ```ts const permix = createPermix({ onForbidden: ({ res }) => { res.status(403).json({ error: 'Custom forbidden message', }) } }) ``` ### Dynamic Error Handler [#dynamic-error-handler] You can also provide a handler that returns different responses based on the checked path: ```ts const permix = createPermix({ onForbidden: ({ res, path }) => { if (path === 'post.create') { res.status(403).json({ error: `You don't have permission for ${path}`, }) return } res.status(403).json({ error: 'You do not have permission to perform this action', }) } }) ``` The `onForbidden` handler receives: * `req`: Express Request object * `res`: Express Response object * `path`: The permission path that was checked (or `null` for callback checks) * `data`: Optional entity data passed to the check ## Advanced Usage [#advanced-usage] ### Async Permission Rules [#async-permission-rules] You can use async functions in your permission setup: ```ts app.use(permix.setupMiddleware(async ({ req }) => { // Fetch user permissions from database const userPermissions = await getUserPermissions(req.user.id) return { post: { create: userPermissions.canCreatePosts, read: userPermissions.canReadPosts, update: userPermissions.canUpdatePosts } } })) ``` ### Dynamic Data-Based Permissions [#dynamic-data-based-permissions] You can check permissions based on the specific data being accessed: ```ts app.put('/posts/:id', async (req, res, next) => { const post = await getPostById(req.params.id) const { check } = permix.get(req) // Check if user can update this specific post if (check('post.update', post)) { next() } else { res.status(403).json({ error: 'You cannot update this post' }) } }) ``` ### Hooks [#hooks] You can register hooks at the factory level to listen for events across all requests: ```ts permix.hook('check', ({ path, data }) => { console.log(`Permission checked: ${path}`, data) }) ``` # Fastify URL: https://permix.letstri.dev/docs/integrations/fastify Learn how to use Permix with Fastify ## Overview [#overview] Permix provides a plugin for Fastify that allows you to easily check permissions in your routes. The plugin can be created using the `createPermix` function. Before getting started with Fastify integration, make sure you've completed the initial setup steps in the [Quick Start](/docs/quick-start) guide. ## Setup [#setup] Here's a basic example of how to use the Permix plugin with Fastify: ```ts import Fastify from 'fastify' import { createPermix } from 'permix/fastify' interface Post { id: string authorId: string title: string content: string } // Initialize Fastify const fastify = Fastify() // Create your Permix instance const permix = createPermix<{ post: [ { name: 'create', type: Post }, { name: 'read', type: Post }, { name: 'update', type: Post }, ] }>() // Set up the plugin with your permission rules await fastify.register(permix.setupMiddleware(({ request, reply }) => { // You can access request.user or other properties to determine permissions return { post: { create: true, read: true, update: false } } })) ``` The plugin preserves full type safety from your Permix definition, ensuring your permission checks are type-safe. ## Checking Permissions [#checking-permissions] Use the `checkMiddleware` function in your Fastify routes to check permissions: ```ts fastify.post('/posts', { preHandler: permix.checkMiddleware('post.create'), }, (request, reply) => { reply.send({ success: true }) }) // Check multiple actions fastify.put('/posts/:id', { preHandler: permix.checkMiddleware(c => c('post.read') && c('post.update')), }, (request, reply) => { reply.send({ success: true }) }) // Check all actions fastify.delete('/posts/:id', { preHandler: permix.checkMiddleware('post.~all'), }, (request, reply) => { reply.send({ success: true }) }) // Check any action fastify.get('/posts', { preHandler: permix.checkMiddleware('post.~any'), }, (request, reply) => { reply.send({ posts: getAllPosts() }) }) ``` ## Accessing Permix Directly [#accessing-permix-directly] You can access the Permix instance directly in your route handlers using the `get` function: ```ts fastify.get('/posts', (request, reply) => { const { check } = permix.get(request) // Check permissions manually if (check('post.read')) { // User has permission to read posts reply.send({ posts: getAllPosts() }) } else { reply.status(403).send({ error: 'You do not have permission to read posts' }) } }) ``` The `get` function returns the Permix instance with available methods. ## Using Templates [#using-templates] Permix provides a template helper to create reusable permission rule sets: ```ts // Create a template for admin permissions const adminTemplate = permix.template({ post: { create: true, read: true, update: true } }) // Use the template in your middleware await fastify.register(permix.setupMiddleware(({ request }) => { // You can still customize the template based on request data if (request.user?.role === 'admin') { return adminTemplate() } return { post: { create: false, read: true, update: false } } })) ``` ## Custom Error Handling [#custom-error-handling] By default, the plugin returns a 403 Forbidden response. You can customize this behavior by providing an `onForbidden` handler: ### Basic Error Handler [#basic-error-handler] ```ts const permix = createPermix({ onForbidden: ({ reply }) => { reply.status(403).send({ error: 'Custom forbidden message', }) } }) ``` ### Dynamic Error Handler [#dynamic-error-handler] You can also provide a handler that returns different responses based on the checked path: ```ts const permix = createPermix({ onForbidden: ({ reply, path }) => { if (path === 'post.create') { reply.status(403).send({ error: `You don't have permission for ${path}`, }) return } reply.status(403).send({ error: 'You do not have permission to perform this action', }) } }) ``` The `onForbidden` handler receives: * `request`: Fastify Request object * `reply`: Fastify Reply object * `path`: The permission path that was checked (or `null` for callback checks) * `data`: Optional entity data passed to the check ## Advanced Usage [#advanced-usage] ### Async Permission Rules [#async-permission-rules] You can use async functions in your permission setup: ```ts await fastify.register(permix.setupMiddleware(async ({ request }) => { // Fetch user permissions from database const userPermissions = await getUserPermissions(request.user.id) return { post: { create: userPermissions.canCreatePosts, read: userPermissions.canReadPosts, update: userPermissions.canUpdatePosts } } })) ``` ### Dynamic Data-Based Permissions [#dynamic-data-based-permissions] You can check permissions based on the specific data being accessed: ```ts fastify.put('/posts/:id', { preHandler: async (request, reply) => { const post = await getPostById(request.params.id) const { check } = permix.get(request) // Check if user can update this specific post if (check('post.update', post)) { return } else { reply.status(403).send({ error: 'You cannot update this post' }) } } }) ``` ### Hooks [#hooks] You can register hooks at the factory level to listen for events across all requests: ```ts permix.hook('check', ({ path, data }) => { console.log(`Permission checked: ${path}`, data) }) ``` # Hono URL: https://permix.letstri.dev/docs/integrations/hono Learn how to use Permix with Hono ## Overview [#overview] Permix provides middleware for Hono that allows you to easily check permissions in your routes. The middleware can be created using the `createPermix` function. Before getting started with Hono integration, make sure you've completed the initial setup steps in the [Quick Start](/docs/quick-start) guide. ## Setup [#setup] Here's a basic example of how to use the Permix middleware with Hono: ```ts import { Hono } from 'hono' import { createPermix } from 'permix/hono' interface Post { id: string authorId: string title: string content: string } // Initialize Hono const app = new Hono() // Create your Permix instance const permix = createPermix<{ post: [ { name: 'create', type: Post }, { name: 'read', type: Post }, { name: 'update', type: Post }, { name: 'delete', type: Post }, ] }>() // Set up the middleware with your permission rules app.use(permix.setupMiddleware(({ c }) => { // You can access c.get('user') or other properties to determine permissions const user = c.get('user') const isAdmin = user?.role === 'admin' return { post: { create: true, read: true, update: isAdmin, delete: isAdmin } } })) ``` The middleware preserves full type safety from your Permix definition, ensuring your permission checks are type-safe. ## Checking Permissions [#checking-permissions] Use the `checkMiddleware` function in your Hono routes to check permissions: ```ts app.post('/posts', permix.checkMiddleware('post.create'), (c) => { // Create post logic here return c.json({ success: true }) }) // Check multiple actions app.put('/posts/:id', permix.checkMiddleware(c => c('post.read') && c('post.update')), (c) => { // Update post logic here return c.json({ success: true }) }) // Check all actions app.delete('/posts/:id', permix.checkMiddleware('post.~all'), (c) => { // Delete post logic here return c.json({ success: true }) }) // Check any action app.get('/posts', permix.checkMiddleware('post.~any'), (c) => { // Get posts logic here return c.json({ posts: getAllPosts() }) }) ``` ## Accessing Permix Directly [#accessing-permix-directly] You can access the Permix instance directly in your route handlers using the `get` function: ```ts app.get('/posts', (c) => { const { check } = permix.getOrThrow(c) // Check permissions manually if (check('post.read')) { // User has permission to read posts return c.json({ posts: getAllPosts() }) } else { return c.json({ error: 'You do not have permission to read posts' }, 403) } }) ``` The `get` function returns the Permix instance with available methods. ## Using Templates [#using-templates] Permix provides a template helper to create reusable permission rule sets: ```ts // 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.use(permix.setupMiddleware(({ c }) => { const user = c.get('user') if (user?.role === 'admin') { return adminTemplate() } return userTemplate() })) ``` ## Custom Error Handling [#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 [#basic-error-handler] ```ts const permix = createPermix({ onForbidden: ({ c }) => { return c.json({ error: 'Custom forbidden message' }, 403) } }) ``` ### Dynamic Error Handler [#dynamic-error-handler] You can also provide a handler that returns different responses based on the entity and actions: ```ts const permix = createPermix({ onForbidden: ({ c, path }) => { if (path === 'post.create') { return c.json({ error: `You don't have permission for ${path}` }, 403) } return c.json({ error: 'You do not have permission to perform this action' }, 403) } }) ``` The `onForbidden` handler receives: * `c`: Hono Context object * `path`: The permission path that was checked (or `null` for callback checks) * `data`: Optional entity data passed to the check ## Advanced Usage [#advanced-usage] ### Async Permission Rules [#async-permission-rules] You can use async functions in your permission setup: ```ts app.use(permix.setupMiddleware(async ({ c }) => { // Fetch user permissions from database const user = c.get('user') const userPermissions = await getUserPermissions(user.id) return { post: { create: userPermissions.canCreatePosts, read: userPermissions.canReadPosts, update: userPermissions.canUpdatePosts, delete: userPermissions.canDeletePosts } } })) ``` ### Dynamic Data-Based Permissions [#dynamic-data-based-permissions] You can check permissions based on the specific data being accessed: ```ts app.put('/posts/:id', async (c) => { const postId = c.req.param('id') const post = await getPostById(postId) const { check } = permix.get(c) // Check if user can update this specific post if (check('post.update', post)) { // Update post logic return c.json({ success: true }) } else { return c.json({ error: 'You cannot update this post' }, 403) } }) ``` ### Hooks [#hooks] You can register hooks at the factory level to listen for events across all requests: ```ts permix.hook('check', ({ path, data }) => { console.log(`Permission checked: ${path}`, data) }) ``` # Next.js URL: https://permix.letstri.dev/docs/integrations/next Learn how to use Permix with Next.js App Router ## Overview [#overview] Permix provides a dedicated integration for the Next.js **App Router** through `permix/next`. It exposes a `createPermix` factory that returns a **per-request** Permix instance backed by React's [`cache()`](https://react.dev/reference/react/cache), so you can `setup()` the rules once and `check()` them anywhere on the server — layouts, pages, route handlers, and server actions — without threading the instance through props. The client side reuses the existing React integration (`permix/react`): the server `dehydrate()`s its state and the client hydrates it into its own singleton via `PermixProvider` + `PermixHydrate`. Before getting started with the Next.js integration, make sure you've completed the initial setup steps in the [Quick Start](/docs/quick-start) guide. Familiarity with the [Hydration guide](/docs/guide/hydration) helps too. This integration is designed for the **App Router**. It relies on `react`'s request-scoped `cache()`, which is available in server components, route handlers, and server actions within a single request. ## Define your permissions [#define-your-permissions] Create a Permix instance once in a shared module so it can be imported anywhere on the server: ```ts title="lib/permix.ts" import { createPermix } from 'permix/next' interface Post { id: string authorId: string } export const permix = createPermix<{ post: [ { name: 'create', type: Post }, { name: 'read', type: Post }, { name: 'update', type: Post }, { name: 'delete', type: Post }, ] }>() ``` The returned helper does **not** hold any permission state at module scope — every request gets its own isolated instance. ## Setup per request [#setup-per-request] Call `setup()` early in the request lifecycle. A common place is the root layout (or any server component that runs before the ones doing `check`s). Resolve any async data (session, headers, cookies, DB lookups) first, then pass plain rules to `setup`: ```tsx title="app/layout.tsx" import { permix } from '@/lib/permix' import { getSession } from '@/lib/auth' export default async function RootLayout({ children, }: { children: React.ReactNode }) { const session = await getSession() permix.setup({ post: { create: !!session, read: true, update: post => post?.authorId === session?.userId, delete: session?.role === 'admin', }, }) return ( {children} ) } ``` Because `setup()` is scoped to the current request, calling it again from a nested server component or route handler in the **same** request simply replaces the rules for that request. Other requests are unaffected. ## Check on the server [#check-on-the-server] Anywhere a server component, route handler, or server action runs in that request, you can use `check()`: ```tsx title="app/posts/[id]/page.tsx" import { notFound } from 'next/navigation' import { permix } from '@/lib/permix' import { getPost } from '@/lib/posts' export default async function PostPage({ params, }: { params: Promise<{ id: string }> }) { const { id } = await params const post = await getPost(id) if (!permix.check('post.read', post)) { notFound() } return
{/* ... */}
} ``` ```ts title="app/api/posts/route.ts" import { permix } from '@/lib/permix' export async function POST(req: Request) { if (!permix.check('post.create')) { return Response.json({ error: 'Forbidden' }, { status: 403 }) } // create the post... return Response.json({ ok: true }) } ``` You can also reach the underlying core instance through `permix.get()` if you need methods like `isReady()` or `getRules()`.
## Send permissions to the client [#send-permissions-to-the-client] Use `dehydrate()` to serialize the request's permissions and hand them to a client provider. The server cannot send the Permix instance itself across the boundary — only the JSON state. ```tsx title="app/providers.tsx" 'use client' import { createPermix } from 'permix' import { PermixHydrate, PermixProvider } from 'permix/react' import type { DehydratedState } from 'permix' // One singleton per browser tab. The same type definition as on the server. const permix = createPermix<{ post: [ { name: 'create', type: { id: string, authorId: string } }, { name: 'read', type: { id: string, authorId: string } }, { name: 'update', type: { id: string, authorId: string } }, { name: 'delete', type: { id: string, authorId: string } }, ] }>() export function Providers({ state, children, }: { state: DehydratedState children: React.ReactNode }) { return ( {children} ) } export { permix } ``` Then wire it up in your root layout right after `setup()`: ```tsx title="app/layout.tsx" import { permix } from '@/lib/permix' import { getSession } from '@/lib/auth' import { Providers } from './providers' export default async function RootLayout({ children, }: { children: React.ReactNode }) { const session = await getSession() permix.setup({ /* ...rules derived from session... */ }) return ( {children} ) } ``` `hydrate()` restores the boolean state but does not flip `isReady` on its own — function-based rules are lost during serialization. If you need `isReady` on the client (e.g. to gate UI on `usePermix(...).isReady`), call `permix.setup(...)` on the client too with the same shape (using booleans and any function rules you want active client-side). See the [Hydration guide](/docs/guide/hydration) for details. ## Use on the client [#use-on-the-client] From any client component, import the singleton from `app/providers.tsx` and the hooks/components from `permix/react`: ```tsx title="app/posts/[id]/edit-button.tsx" 'use client' import { usePermix } from 'permix/react' import { permix } from '@/app/providers' export function EditButton({ post }: { post: { id: string, authorId: string } }) { const { check } = usePermix(permix) if (!check('post.update', post)) { return null } return } ``` If you prefer the component API, create checkers with `createComponents` from `permix/react` and use them in your client components — see the [React integration](/docs/integrations/react#components) for details. ## Templates [#templates] `createPermix` exposes the same `template()` helper as the core API for reusing rule sets: ```ts title="lib/permix.ts" import { createPermix } from 'permix/next' export const permix = createPermix<{ post: ['create', 'read', 'update', 'delete'] }>() export const adminTemplate = permix.template({ post: { create: true, read: true, update: true, delete: true }, }) export const guestTemplate = permix.template({ post: { create: false, read: true, update: false, delete: false }, }) ``` ```tsx title="app/layout.tsx" import { permix, adminTemplate, guestTemplate } from '@/lib/permix' import { getSession } from '@/lib/auth' const session = await getSession() permix.setup(session?.role === 'admin' ? adminTemplate() : guestTemplate()) ``` ## Example [#example] You can find a runnable example of the Next.js integration [here](https://github.com/letstri/permix/tree/main/examples/next).
## How per-request isolation works [#how-per-request-isolation-works] `createPermix` from `permix/next` wraps a single core instance per request using React's `cache()`. Inside one Next.js request: * The first call to `setup`/`check`/`get`/`dehydrate` creates (or reuses) **one** instance. * All subsequent calls in the same request — across server components, route handlers, and server actions — share that instance. Across concurrent requests, each request gets its **own** instance. State never leaks between users. Do **not** store the result of `permix.get()` (or any rule data) in module-level variables. That would defeat per-request isolation. Always go through `permix.check()` / `permix.get()` so the request-scoped cache is consulted. ## API [#api] ### `createPermix()` [#createpermixd] Returns an object with the following methods: | Method | Description | | ----------------- | -------------------------------------------------------------------------------------------- | | `setup(rules)` | Set the per-request permission rules. Resolve any async data (session, etc.) before calling. | | `check(...args)` | Check a permission against the current request's rules. Same signature as the core `check`. | | `get()` | Return the underlying [`Permix`](/docs/guide/instance) instance for the current request. | | `getRules()` | Return the current rules object for the request-scoped instance, or `null`. | | `dehydrate()` | Serialize the current request's rules to JSON (for `` on the client). | | `template(rules)` | Create a reusable rule set. Same as the core [`template`](/docs/guide/template). | ## TanStack Start and other frameworks [#tanstack-start-and-other-frameworks] The client layer (`permix/react`) is framework-agnostic. If you're using TanStack Start, Remix, or a custom React SSR setup, you can still use `permix/react` on the client. For the server side, either: * Use the dedicated [`permix/tanstack-start`](/docs/integrations/tanstack-start) integration, which follows the same shape as `permix/next`, or * Create a core Permix instance per request manually (see [Hydration guide](/docs/guide/hydration)), or * Use an existing server integration like [`permix/node`](/docs/integrations/node), [`permix/express`](/docs/integrations/express), or [`permix/hono`](/docs/integrations/hono) when applicable. # Node.js URL: https://permix.letstri.dev/docs/integrations/node Learn how to use Permix with Node.js HTTP servers ## Overview [#overview] Permix provides middleware for Node.js HTTP servers that allows you to easily check permissions in your request handlers. The middleware can be created using the `createPermix` function. Before getting started with Node.js integration, make sure you've completed the initial setup steps in the [Quick Start](/docs/quick-start) guide. ## Setup [#setup] Here's a basic example of how to use the Permix middleware with a Node.js HTTP server: ```ts import http from 'node:http' import { createPermix } from 'permix/node' 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 }, ] }>() // Create an HTTP server const server = http.createServer(async (req, res) => { // Parse the URL const url = new URL(req.url || '/', `http://${req.headers.host}`) const path = url.pathname const method = req.method || 'GET' const next = () => {} // Setup Permix with permission rules await permix.setupMiddleware(({ req }) => { // Determine user permissions (e.g., from headers, auth token, etc.) const isAdmin = req.headers['x-user-role'] === 'admin' return { post: { create: true, read: true, update: isAdmin, delete: isAdmin } } })(req, res, next) // Route handling if (path === '/posts' && method === 'POST') { await permix.checkMiddleware('post.create')(req, res, next) } else if (path.startsWith('/posts/') && method === 'PUT') { await permix.checkMiddleware(c => c('post.read') && c('post.update'))(req, res, next) } else if (path.startsWith('/posts/') && method === 'DELETE') { await permix.checkMiddleware('post.delete')(req, res, next) } else { res.statusCode = 404 res.end(JSON.stringify({ error: 'Not found' })) } }) server.listen(3000, () => { console.log('Server running at http://localhost:3000/') }) ``` The middleware preserves full type safety from your Permix definition, ensuring your permission checks are type-safe. ## Checking Permissions [#checking-permissions] Use the `checkMiddleware` function to check permissions for specific routes: ```ts // Check a single action await permix.checkMiddleware('post.create')(req, res, next) // Check multiple actions await permix.checkMiddleware(c => c('post.read') && c('post.update'))(req, res, next) // Check all actions await permix.checkMiddleware('post.~all')(req, res, next) ``` ## Accessing Permix Directly [#accessing-permix-directly] You can access the Permix instance directly in your request handlers using the `get` function: ```ts http.createServer((req, res) => { const { check } = permix.get(req) // User has permission to read posts if (check('post.read')) { res.statusCode = 200 res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify({ posts: getAllPosts() })) } }) ``` The `get` function returns the Permix instance with available methods. ## Using Templates [#using-templates] Permix provides a template helper to create reusable permission rule sets: ```ts // 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 await permix.setupMiddleware(({ req }) => { const isAdmin = req.headers['x-user-role'] === 'admin' if (isAdmin) { return adminTemplate() } return userTemplate() })(req, res, next) ``` ## Custom Error Handling [#custom-error-handling] By default, the middleware returns a 403 Forbidden response if the user doesn't have permission. You can customize this behavior by providing an `onForbidden` handler: ### Basic Error Handler [#basic-error-handler] ```ts const permix = createPermix({ onForbidden: ({ res }) => { res.statusCode = 403 res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify({ error: 'Custom forbidden message' })) } }) ``` ### Dynamic Error Handler [#dynamic-error-handler] You can also provide a handler that returns different responses based on the entity and actions: ```ts const permix = createPermix({ onForbidden: ({ res, path }) => { res.statusCode = 403 res.setHeader('Content-Type', 'application/json') if (path === 'post.create') { res.end(JSON.stringify({ error: `You don't have permission for ${path}` })) return } res.end(JSON.stringify({ error: 'You do not have permission to perform this action' })) } }) ``` The `onForbidden` handler receives: * `req`: Node.js IncomingMessage object * `res`: Node.js ServerResponse object * `path`: The permission path that was checked (or `null` for callback checks) * `data`: Optional entity data passed to the check ## Advanced Usage [#advanced-usage] ### Async Permission Rules [#async-permission-rules] You can use async functions in your permission setup: ```ts permix.setupMiddleware(async ({ req }) => { // Extract user ID from request const userId = req.headers['x-user-id'] // Fetch user permissions from database const userPermissions = await getUserPermissions(userId) return { post: { create: userPermissions.canCreatePosts, read: userPermissions.canReadPosts, update: userPermissions.canUpdatePosts, delete: userPermissions.canDeletePosts } } })(req, res, next) ``` ### Dynamic Data-Based Permissions [#dynamic-data-based-permissions] You can check permissions based on the specific data being accessed: ```ts http.createServer(async (req, res) => { // Setup Permix middleware first... // Extract post ID from URL const url = new URL(req.url || '/', `http://${req.headers.host}`) const pathParts = url.pathname.split('/') const postId = pathParts[2] // e.g., /posts/123 if (req.method === 'PUT' && pathParts[1] === 'posts' && postId) { // Fetch the post data const post = await getPostById(postId) // Get Permix instance const { check } = permix.get(req) // Check if user can update this specific post if (check('post.update', post)) { // Process update... res.statusCode = 200 res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify({ success: true })) } else { res.statusCode = 403 res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify({ error: 'You cannot update this post' })) } } }) ``` ## Integration with Web Frameworks [#integration-with-web-frameworks] This integration is designed for raw Node.js HTTP servers. If you're using a web framework: * For Express, use [Permix Express integration](/docs/integrations/express) * For Hono, use [Permix Hono integration](/docs/integrations/hono) ### Hooks [#hooks] You can register hooks at the factory level to listen for events across all requests: ```ts permix.hook('check', ({ path, data }) => { console.log(`Permission checked: ${path}`, data) }) ``` # oRPC URL: https://permix.letstri.dev/docs/integrations/orpc Learn how to use Permix with oRPC ## Overview [#overview] Permix provides a middleware for oRPC that allows you to easily check permissions in your middlewares. The middleware can be created using the `createPermix` function. Before getting started with oRPC integration, make sure you've completed the initial setup steps in the [Quick Start](/docs/quick-start) guide. ## Setup [#setup] Here's a basic example of how to use the Permix middleware with oRPC: ```ts import { os } from '@orpc/server' import { createPermix } from 'permix/orpc' interface Post { id: string title: string } interface Context { user: { id: string role: string } } const orpcPermix = os.$context() // Create your Permix instance with a custom context key const permix = createPermix<{ post: ['create', 'read', 'update'] user: ['delete'] }>().contextKey('permissions') // Create a protected middleware with Permix const protectedMiddleware = orpcPermix.use(({ context, next }) => { const isAdmin = context.user.role === 'admin' return next({ context: permix.setupContext({ post: { create: true, read: true, update: isAdmin }, user: { 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 [#checking-permissions] Use the `checkMiddleware` function in your oRPC middlewares to check permissions: ```ts const router = orpcPermix.router({ createPost: protectedMiddleware .use(permix.checkMiddleware('post.create')) .handler(({ context }) => { // Create post logic here return { success: true } }), updatePost: protectedMiddleware .use(permix.checkMiddleware(c => c('post.read') && c('post.update'))) .handler(({ context }) => { // Update post logic here return { success: true } }), deleteUser: protectedMiddleware .use(permix.checkMiddleware('user.delete')) .handler(({ context }) => { // Delete user logic here return { success: true } }) }) ``` ## Accessing Permix in Middlewares [#accessing-permix-in-middlewares] Permix is automatically added to your oRPC context under the key you specified, so you can access it directly: ```ts const router = orpcPermix.router({ getPosts: protectedMiddleware .handler(({ context }) => { // Check permissions manually if (context.permissions.check('post.read')) { return getAllPosts() } throw new ORPCError('FORBIDDEN', { message: 'You do not have permission to read posts' }) }) }) ``` The `context.permissions` object provides: * `check`: Synchronously check a permission * `dehydrate`: Serialize the current rules for client hydration * `template`: Create reusable permission templates ## Using Templates [#using-templates] Permix provides a template helper to create reusable permission rule sets: ```ts // Create a template for admin permissions const adminTemplate = permix.template({ post: { create: true, read: true, update: true }, user: { delete: true } }) // Create a template for regular user permissions const userTemplate = permix.template({ post: { create: true, read: true, update: false }, user: { delete: false } }) // Use templates in your middleware const protectedMiddleware = orpcPermix.use(({ context, next }) => { return next({ context: permix.setupContext( context.user.role === 'admin' ? adminTemplate() : userTemplate() ), }) }) ``` ## Custom Error Handling [#custom-error-handling] By default, the middleware throws an `ORPCError` 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 [#throw-a-custom-error] ```ts const permix = createPermix({ onForbidden: ({ path, context }) => { throw new ORPCError('FORBIDDEN', { message: `User ${context.user.id} doesn't have permission for ${path}` }) } }) ``` ### Allow Through [#allow-through] You can also let denied requests through by calling `next()`: ```ts const permix = createPermix({ onForbidden: ({ path, next }) => { console.warn(`Permission denied for ${path}, allowing through`) return next() } }) ``` The `onForbidden` handler receives: * `path`: The permission path that was checked (e.g. `'post.create'`) * `data`: Optional data passed to the check * `context`: Your oRPC context object * `next`: The middleware `next` function — call it to allow the request through ## Advanced Usage [#advanced-usage] ### Async Permission Rules [#async-permission-rules] You can use async functions in your permission setup: ```ts const protectedMiddleware = orpcPermix.use(async ({ context, next }) => { const userPermissions = await getUserPermissions(context.user.id) return next({ context: permix.setupContext({ post: { create: userPermissions.canCreatePosts, read: userPermissions.canReadPosts, update: userPermissions.canUpdatePosts }, user: { delete: userPermissions.canDeleteUsers } }) }) }) ``` ### Hooks [#hooks] You can register hooks at the factory level to listen for events across all requests: ```ts permix.hook('check', ({ path, data }) => { console.log(`Permission checked: ${path}`, data) }) ``` # React URL: https://permix.letstri.dev/docs/integrations/react Learn how to use Permix with React applications ## Overview [#overview] Permix provides official React integration through the `PermixProvider` component and `usePermix` hook. This allows you to manage permissions reactively in your React app. Before getting started with React integration, make sure you've completed the initial setup steps in the [Quick Start](/docs/quick-start) guide. ## Setup [#setup] First, wrap your application with the `PermixProvider`: ```tsx title="App.tsx" import { PermixProvider } from 'permix/react' import { permix } from './lib/permix' function App() { return ( ) } ``` Remember to always pass the same Permix instance to both the `PermixProvider` and `usePermix` hook to maintain type safety. ## Hook [#hook] For checking permissions in your components, you can use the `usePermix` hook. And to avoid importing the hook and Permix instance in every component, you can create a custom hook: ```tsx title="hooks/use-permissions.ts" import { usePermix } from 'permix/react' import { permix } from '../lib/permix' export function usePermissions() { return usePermix(permix) } ``` ## Components [#components] If you prefer using components, you can import the `createComponents` function from `permix/react` and create checking components: ```ts title="lib/permix.ts" import { createComponents } from 'permix/react' // ... export const { Check } = createComponents(permix) ``` And then you can use the `Check` component in your components: ```tsx title="page.tsx" export default function Page() { return ( Will show this if a user doesn't have permission

} reverse > Will show this if a user has permission
) } ```
## Usage [#usage] Use the `usePermix` hook and checking components in your components: ```tsx title="page.tsx" 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
Loading permissions...
} const canEdit = check('post.edit', post) return (
{canEdit ? ( ) : (

You don't have permission to edit this post

)} Can I create a post inside the Check component?
) } ```
## Hydration [#hydration] For SSR applications, use `PermixHydrate` to restore dehydrated server state on the client. `hydrate()` does not mark the instance ready and cannot restore function-based rules — call `setup()` on the client with the full rule set (usually in the same place you restore the session): ```tsx title="App.tsx" import { useEffect } from 'react' import type { DehydratedState } from 'permix' import { PermixHydrate, PermixProvider } from 'permix/react' import { permix } from './lib/permix' import { getClientRules } from './lib/permissions' function App({ dehydratedState }: { dehydratedState: DehydratedState }) { useEffect(() => { permix.setup(getClientRules()) }, []) return ( ) } ``` See the [Hydration guide](/docs/guide/hydration) and framework-specific pages for [Next.js](/docs/integrations/next) and [TanStack Start](/docs/integrations/tanstack-start). ## Example [#example] You can find the example of the React integration [here](https://github.com/letstri/permix/tree/main/examples/react).
# Server URL: https://permix.letstri.dev/docs/integrations/server Learn how to use Permix with web-standard fetch-style servers ## Overview [#overview] Permix provides a framework-agnostic middleware for any runtime built on the web-standard `Request` / `Response` API. The middleware follows the `(req, next) => Response` pattern and can be created using the `createPermix` function. This integration is designed to plug directly into [srvx](https://srvx.h3.dev) — its `middleware` array uses the exact same `(req, next) => Response` shape — but it does not depend on srvx and can be composed in any fetch-style handler. Before getting started with the server integration, make sure you've completed the initial setup steps in the [Quick Start](/docs/quick-start) guide. ## Setup [#setup] Here's a basic example of how to use the Permix middleware with srvx: ```ts import { serve } from 'srvx' import { createPermix } from 'permix/server' 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 }, ] }>() // Set up the middleware with your permission rules serve({ middleware: [ permix.setupMiddleware(({ req }) => { // You can read headers, cookies, or any request data to determine permissions const isAdmin = req.headers.get('x-user-role') === 'admin' return { post: { create: true, read: true, update: isAdmin, delete: isAdmin, }, } }), ], fetch(req) { return Response.json({ ok: true }) }, }) ``` The middleware preserves full type safety from your Permix definition, ensuring your permission checks are type-safe. ## Checking Permissions [#checking-permissions] Use the `checkMiddleware` function to enforce a permission in your handler: ```ts serve({ middleware: [ permix.setupMiddleware({ /* ... */ }), ], fetch(req) { // Check a single path return permix.checkMiddleware('post.create')(req, () => Response.json({ success: true }), ) }, }) ``` `checkMiddleware` accepts the same arguments as the core `check`: ```ts // Check a single path permix.checkMiddleware('post.create') // Check with data permix.checkMiddleware('post.update', post) // Compose multiple checks with a callback permix.checkMiddleware(c => c('post.read') && c('post.update')) ``` If the check passes, `next()` is called. If it fails, the middleware short-circuits with the response from `onForbidden` (a 403 JSON response by default). ## Accessing Permix Directly [#accessing-permix-directly] You can access the Permix instance directly inside any handler that has the `Request`: ```ts fetch(req) { const { check } = permix.getOrThrow(req) // Check permissions manually if (check('post.read')) { return Response.json({ posts: getAllPosts() }) } return Response.json({ error: 'Forbidden' }, { status: 403 }) } ``` The `get` function returns the Permix instance attached to the request (or `null` if `setupMiddleware` has not run yet), while `getOrThrow` throws a `PermixNotFoundError` in that case. ## Using Templates [#using-templates] Permix provides a template helper to create reusable permission rule sets: ```ts // 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 serve({ middleware: [ permix.setupMiddleware(({ req }) => { const isAdmin = req.headers.get('x-user-role') === 'admin' return isAdmin ? adminTemplate() : userTemplate() }), ], fetch(req) { return Response.json({ ok: true }) }, }) ``` ## Custom Error Handling [#custom-error-handling] By default, the middleware returns a 403 Forbidden response with `{ "error": "Forbidden" }`. You can customize this behavior by providing an `onForbidden` handler: ### Basic Error Handler [#basic-error-handler] ```ts const permix = createPermix({ onForbidden: () => Response.json({ error: 'Custom forbidden message' }, { status: 403 }), }) ``` ### Dynamic Error Handler [#dynamic-error-handler] You can also provide a handler that returns different responses based on the path and data being checked: ```ts const permix = createPermix({ onForbidden: ({ req, path, data }) => { if (path === 'post.create') { return Response.json( { error: `You don't have permission for ${path}` }, { status: 403 }, ) } return Response.json( { error: 'You do not have permission to perform this action' }, { status: 403 }, ) }, }) ``` The `onForbidden` handler receives: * `req`: the incoming web-standard `Request` * `next`: the downstream handler — call it to let the request through anyway * `path`: the permission path that was checked (e.g. `'post.create'`) * `data`: optional data passed to the check ## Advanced Usage [#advanced-usage] ### Async Permission Rules [#async-permission-rules] You can use async functions in your permission setup: ```ts serve({ middleware: [ permix.setupMiddleware(async ({ req }) => { // Fetch user permissions from database const userId = req.headers.get('x-user-id') const userPermissions = await getUserPermissions(userId) return { post: { create: userPermissions.canCreatePosts, read: userPermissions.canReadPosts, update: userPermissions.canUpdatePosts, delete: userPermissions.canDeletePosts, }, } }), ], fetch(req) { return Response.json({ ok: true }) }, }) ``` ### Dynamic Data-Based Permissions [#dynamic-data-based-permissions] You can check permissions based on the specific data being accessed: ```ts async function fetch(req: Request) { const url = new URL(req.url) const postId = url.pathname.split('/')[2] const post = await getPostById(postId) const { check } = permix.getOrThrow(req) // Check if user can update this specific post if (check('post.update', post)) { return Response.json({ success: true }) } return Response.json({ error: 'You cannot update this post' }, { status: 403 }) } ``` ## Without a Framework [#without-a-framework] Because the middleware is just `(req, next) => Response`, you can compose it by hand in any `fetch` handler — no framework required. The pattern is to chain middlewares by passing each one as the `next` of the previous: ```ts import { createPermix } from 'permix/server' const permix = createPermix() const setup = permix.setupMiddleware({ post: { create: true, read: true, update: false, delete: false }, }) export default { fetch(req: Request) { return setup(req, () => permix.checkMiddleware('post.create')(req, () => Response.json({ ok: true }), ), ) }, } ``` For anything beyond a couple of middlewares, srvx (or any other web-standard runtime) will compose them for you. ### Hooks [#hooks] You can register hooks at the factory level to listen for events across all requests: ```ts permix.hook('check', ({ path, data }) => { console.log(`Permission checked: ${path}`, data) }) ``` # Solid URL: https://permix.letstri.dev/docs/integrations/solid Learn how to use Permix with Solid applications ## Overview [#overview] Permix provides official Solid integration through the `PermixProvider` component and `usePermix` hook. This allows you to manage permissions reactively in your Solid app. Before getting started with Solid integration, make sure you've completed the initial setup steps in the [Quick Start](/docs/quick-start) guide. ## Setup [#setup] First, wrap your application with the `PermixProvider`: ```tsx title="App.tsx" import { PermixProvider } from 'permix/solid' import { permix } from './lib/permix' function App() { return ( ) } ``` Remember to always pass the same Permix instance to both the `PermixProvider` and `usePermix` hook to maintain type safety. ## Hook [#hook] For checking permissions in your components, you can use the `usePermix` hook. And to avoid importing the hook and Permix instance in every component, you can create a custom utility: ```tsx title="hooks/use-permissions.ts" import { usePermix } from 'permix/solid' import { permix } from '../lib/permix' export function usePermissions() { return usePermix(permix) } ``` ## Components [#components] If you prefer using components, you can import the `createComponents` function from `permix/solid` and create checking components: ```ts title="lib/permix.ts" import { createComponents } from 'permix/solid' // ... export const { Check } = createComponents(permix) ``` And then you can use the `Check` component in your components: ```tsx title="page.tsx" export default function Page() { return ( Will show this if a user doesn't have permission

} reverse > Will show this if a user has permission
) } ```
## Usage [#usage] Use the `usePermix` hook and checking components in your components: ```tsx title="page.tsx" import { usePermix } from 'permix/solid' import { permix } from './lib/permix' import { Check } from './lib/permix-components' export default function Page() { const post = usePost() const { check, isReady } = usePermix(permix) const canEdit = () => check('post.edit', post) return ( <> {!isReady() ?
Loading permissions...
: (
{canEdit() ? ( ) : (

You don't have permission to edit this post

)} Can I create a post inside the Check component?
) } ) } ```
## Hydration [#hydration] For SSR, use `PermixHydrate` with the dehydrated server state, then call `permix.setup()` on the client to restore function-based rules: ```tsx title="App.tsx" import { onMount } from 'solid-js' import type { DehydratedState } from 'permix' import { PermixHydrate, PermixProvider } from 'permix/solid' import { permix } from './lib/permix' import { getClientRules } from './lib/permissions' export function App(props: { dehydratedState: DehydratedState, children: any }) { onMount(() => { permix.setup(getClientRules()) }) return ( {props.children} ) } ``` See the [Hydration guide](/docs/guide/hydration). ## Example [#example] You can find the example of the Solid integration [here](https://github.com/letstri/permix/tree/main/examples/solid).
# Svelte URL: https://permix.letstri.dev/docs/integrations/svelte Learn how to use Permix with Svelte applications ## Overview [#overview] Permix provides official Svelte integration through the `PermixProvider` component and `usePermix` hook. This allows you to manage permissions reactively in your Svelte app using runes. Before getting started with Svelte integration, make sure you've completed the initial setup steps in the [Quick Start](/docs/quick-start) guide. Svelte integration requires Svelte 5. ## Setup [#setup] First, wrap your application with the `PermixProvider`: ```svelte title="App.svelte" {@render children()} ``` Remember to always pass the same Permix instance to both the `PermixProvider` and `usePermix` hook to maintain type safety. ## Hook [#hook] For checking permissions in your components, you can use the `usePermix` hook. And to avoid importing the hook and Permix instance in every component, you can create a custom utility: ```ts title="lib/use-permissions.ts" import { usePermix } from 'permix/svelte' import { permix } from './permix' export function usePermissions() { return usePermix(permix) } ``` ## Components [#components] If you prefer using components, you can import the `createComponents` function from `permix/svelte` and create checking components: ```ts title="lib/permix.ts" import { createComponents } from 'permix/svelte' // ... export const { Check } = createComponents(permix) ``` And then you can use the `Check` component in your components. The default content is rendered when a user has permission, and the `otherwise` snippet is rendered when they don't: ```svelte title="Page.svelte" Will show this if a user has permission {#snippet otherwise()} Will show this if a user doesn't have permission {/snippet} ``` ## Usage [#usage] Use the `usePermix` hook and checking components in your components: ```svelte title="Page.svelte"
{#if !permissions.isReady}
Loading permissions...
{:else if permissions.check('post.edit', post)} {:else}

You don't have permission to edit this post

{/if} Can I create a post inside the Check component?
``` `usePermix` returns an object with a reactive `isReady` getter and a `check` method. Access them directly on the returned object (for example `permissions.isReady`) to keep reactivity — don't destructure `isReady`.
## Hydration [#hydration] For SSR, wrap the app with `PermixHydrate` and pass the dehydrated server state. Call `permix.setup()` on the client afterward to restore function-based rules and set `isReady`: ```svelte title="App.svelte" {@render children()} ``` See the [Hydration guide](/docs/guide/hydration).
# TanStack Start URL: https://permix.letstri.dev/docs/integrations/tanstack-start Learn how to use Permix with TanStack Start ## Overview [#overview] Permix provides a dedicated integration for [TanStack Start](https://tanstack.com/start) through `permix/tanstack-start`. It exposes a `createPermix` factory that returns a **per-request** Permix instance backed by TanStack Start's [server request context](https://tanstack.com/start/latest/docs/framework/react/guide/middleware). You `setupMiddleware()` the rules once per request and `get()` them on the server inside **server functions** and **server routes** — without threading the instance through props. A second instance lives on the **router context**, so `beforeLoad` and `loader` can check permissions isomorphically, and the same instance backs the React integration (`permix/react`) via `PermixProvider` + `PermixHydrate`. Before getting started with the TanStack Start integration, make sure you've completed the initial setup steps in the [Quick Start](/docs/quick-start) guide. Familiarity with the [Hydration guide](/docs/guide/hydration) helps too. ## Define your permissions [#define-your-permissions] Create a Permix instance once in a shared module so it can be imported anywhere on the server: ```ts title="lib/permix.ts" import type { ValidateDefinition } from 'permix' import { createPermix } from 'permix/tanstack-start' interface Post { id: string authorId: string } // Define your permissions once and reuse this type on the server and client. export type PermissionsDefinition = ValidateDefinition<{ post: [ { name: 'create', type: Post }, { name: 'read', type: Post }, { name: 'update', type: Post }, { name: 'delete', type: Post }, ] }> export const permix = createPermix() ``` The returned helper does **not** hold any permission state at module scope — every request gets its own isolated instance. By default the instance is stored on the request context under `'__permix'`. Call `.contextKey('permissions')` if you need a custom key (e.g. when running multiple Permix instances side by side). ## Setup per request [#setup-per-request] Register `setupMiddleware()` as a global request middleware in `src/start.ts` so it runs for every request and creates a fresh, request-scoped instance. The callback receives the `request`, so you can read cookies, headers, or fetch the user. ```ts title="src/start.ts" import { createStart } from '@tanstack/react-start' import { getSession } from './lib/auth' import { permix } from './lib/permix' export const startInstance = createStart(() => ({ requestMiddleware: [ permix.setupMiddleware(async ({ request }) => { const session = await getSession(request) return { post: { create: !!session, read: true, update: post => post?.authorId === session?.userId, delete: session?.role === 'admin', }, } }), ], })) ``` You can also attach `setupMiddleware()` to a specific server route's `middleware` array instead of registering it globally, if only some routes need permissions. ### Server-only imports in the setup callback [#server-only-imports-in-the-setup-callback] TanStack Start strips server code from the client bundle by rewriting `createMiddleware().server(...)` calls it finds **in your source files**. `setupMiddleware()` makes that call inside the Permix package, so the compiler never sees a `.server()` boundary in your module — the setup callback and everything it imports stay in the client graph. With plain isomorphic rules that's harmless. But as soon as the callback pulls in server-only dependencies — an auth library, a database client, `node:` builtins — they leak into the browser and fail at runtime: * `Buffer is not defined` * `Module "events" has been externalized for browser compatibility` * database drivers appearing in the client bundle When the callback needs server-only imports, write the `.server()` boundary yourself and pass in `createSetupHandler()` — the same setup logic, but placed where the compiler can strip it: ```ts title="src/start.ts" import { createMiddleware, createStart } from '@tanstack/react-start' import { auth } from './lib/auth' // server-only: stays out of the client bundle import { permix } from './lib/permix' const permixMiddleware = createMiddleware().server( permix.createSetupHandler(async ({ request }) => { const session = await auth.api.getSession({ headers: request.headers }) return { post: { create: !!session, read: true, update: post => post?.authorId === session?.userId, delete: session?.role === 'admin', }, } }), ) export const startInstance = createStart(() => ({ requestMiddleware: [permixMiddleware], })) ``` `createSetupHandler()` accepts exactly what `setupMiddleware()` accepts — a rules object or a callback receiving the `request` — and produces the same request-scoped instance, hooks included. `checkMiddleware()` is unaffected: it never closes over your setup imports. ## Check on the server [#check-on-the-server] The request-scoped instance lives on TanStack Start's **server** request context, which is shared with server functions and server routes. Read it with `getOrThrow(context)` (or `get(context)`) inside a `createServerFn` handler and call `check()`: ```ts title="src/server/posts.ts" import { notFound } from '@tanstack/react-router' import { createServerFn } from '@tanstack/react-start' import { permix } from '../lib/permix' import { getPost } from '../lib/posts' export const getPostPageData = createServerFn() .inputValidator((data: { id: string }) => data) .handler(async ({ data, context }) => { const post = await getPost(data.id) if (!post || !permix.getOrThrow(context).check('post.read', post)) { throw notFound() } return { post } }) ``` Then call the server function from your route loader: ```tsx title="src/routes/posts.$id.tsx" import { createFileRoute } from '@tanstack/react-router' import { getPostPageData } from '../server/posts' export const Route = createFileRoute('/posts/$id')({ loader: ({ params }) => getPostPageData({ data: { id: params.id } }), }) ``` Use `get(context)` instead of `getOrThrow(context)` when you want a nullable value rather than a [`PermixNotFoundError`](/docs/guide/instance) if setup didn't run. `permix.get(context)` only works where `context` is the **server request context** — server functions and server routes. A route `loader` or `beforeLoad` receives the [router context](https://tanstack.com/router/latest/docs/framework/react/guide/router-context) instead, and runs **isomorphically** on both server and client. To check permissions there, put an instance on the router context — see [Check in `beforeLoad` and loaders](#check-in-beforeload-and-loaders). ## Guard server functions [#guard-server-functions] Use `checkMiddleware()` to enforce a permission before a server function's handler runs: ```ts title="src/lib/posts.ts" import { createServerFn } from '@tanstack/react-start' import { permix } from './permix' export const createPost = createServerFn({ method: 'POST' }) .middleware([permix.checkMiddleware('post.create')]) .handler(async () => { // Only runs when `post.create` passed. // ... }) ``` By default a denied check throws a `PermixError`. Pass an `onForbidden` handler to `createPermix` to customise this — for example to throw a `redirect()`. ```ts const permix = createPermix({ onForbidden: ({ path }) => { throw new Error(`Forbidden: ${path}`) }, }) ``` ## Add Permix to the router context [#add-permix-to-the-router-context] `beforeLoad` and `loader` never see the server request context, but they do see the [router context](https://tanstack.com/router/latest/docs/framework/react/guide/router-context). Create a core Permix instance inside `getRouter()` and pass it there. `getRouter()` runs **once per request on the server** and **once per tab in the browser**, so this instance is request-scoped during SSR and a singleton in the browser — the same lifetime you want for the client cache. ```tsx title="src/router.tsx" import type { PermissionsDefinition } from './lib/permix' import { createRouter as createTanStackRouter } from '@tanstack/react-router' import { createPermix } from 'permix' import { routeTree } from './routeTree.gen' export function getRouter() { const permix = createPermix() return createTanStackRouter({ routeTree, context: { permix }, }) } ``` Type the context on the root route with `createRootRouteWithContext`: ```tsx title="src/routes/__root.tsx" import type { Permix } from 'permix' import type { PermissionsDefinition } from '../lib/permix' import { createRootRouteWithContext } from '@tanstack/react-router' export interface RouterContext { permix: Permix } export const Route = createRootRouteWithContext()({ // ... }) ``` Typing the context is not enough on its own — the runtime value must be passed to `createTanStackRouter({ context })` too, otherwise `context.permix` is `undefined` in every route. ## Hydrate it in the root route [#hydrate-it-in-the-root-route] Use `dehydrate(context)` to serialize the request's permissions, then hydrate the router-context instance in the root route's `beforeLoad`. The server cannot send the instance itself across the boundary — only the JSON state. `beforeLoad` and `loader` are **isomorphic** — they run on both server and client. Since the request-scoped instance only exists in the server context, `dehydrate()` must be called inside a `createServerFn` so it always executes on the server. ```ts title="src/lib/permix.server.ts" import { createServerFn } from '@tanstack/react-start' import { permix } from './permix' export const getPermixState = createServerFn() .handler(({ context }) => permix.dehydrate(context)) ``` The root route's `beforeLoad` runs before every child route's `beforeLoad` and `loader`, so hydrating there makes `context.permix` ready everywhere below it. Returning the state also puts it on the context for the components. ```tsx title="src/routes/__root.tsx" import { createRootRouteWithContext, Outlet } from '@tanstack/react-router' import { getPermixState } from '../lib/permix.server' import { Providers } from '../providers' // `RouterContext` from the previous step. export const Route = createRootRouteWithContext()({ beforeLoad: async ({ context }) => { const state = await getPermixState() context.permix.hydrate(state) return { state } }, component: RootComponent, }) function RootComponent() { const { permix, state } = Route.useRouteContext() return ( ) } ``` Pass the very same instance to `PermixProvider` so routes and components read one source of truth: ```tsx title="src/providers.tsx" import type { DehydratedState, Permix } from 'permix' import type { PermissionsDefinition } from './lib/permix' import { PermixHydrate, PermixProvider } from 'permix/react' export function Providers({ permix, state, children, }: { permix: Permix state: DehydratedState children: React.ReactNode }) { return ( {children} ) } ``` `hydrate()` restores the boolean state but does not flip `isReady` on its own — function-based rules are lost during serialization. If you need `isReady`, or rules that depend on check-time data (`update: post => post.authorId === userId`), call `permix.setup(...)` on the client too with the same shape. See the [Hydration guide](/docs/guide/hydration) for details. ## Check in `beforeLoad` and loaders [#check-in-beforeload-and-loaders] With the instance hydrated on the router context, any route can guard itself without a dedicated server function. The check runs on the server during SSR and on the client on subsequent navigations. ```tsx title="src/routes/dashboard.tsx" import { createFileRoute, redirect } from '@tanstack/react-router' export const Route = createFileRoute('/dashboard')({ beforeLoad: ({ context }) => { if (!context.permix.check('post.create')) { throw redirect({ to: '/login' }) } }, component: DashboardComponent, }) ``` This is a **UX guard**, not enforcement. The client can always call your server functions directly, so mirror every guard with `checkMiddleware()` (or a `getOrThrow(context).check()`) on the server. Two limits are worth knowing: * Hydrated rules are plain booleans, so a data-dependent rule like `post.update` collapses to its no-data result. Checks that need the entity belong in a server function — or re-run `setup()` on the client with the full rules. * `check()` throws [`PermixNotReadyError`](/docs/guide/instance) when the instance has neither been hydrated nor set up, which is what you'll see if the root `beforeLoad` was skipped. ## Use on the client [#use-on-the-client] Read the instance off the router context and pass it to `usePermix`: ```tsx title="src/lib/use-permix.ts" import { useRouteContext } from '@tanstack/react-router' import { usePermix as useReactPermix } from 'permix/react' export function usePermix() { const { permix } = useRouteContext({ from: '__root__' }) return useReactPermix(permix) } ``` ```tsx title="src/components/edit-button.tsx" import { usePermix } from '../lib/use-permix' export function EditButton({ post }: { post: { id: string, authorId: string } }) { const { check } = usePermix() if (!check('post.update', post)) { return null } return } ``` If you prefer the component API, create checkers with `createComponents` from `permix/react` — see the [React integration](/docs/integrations/react#components) for details. ## Templates [#templates] `createPermix` exposes the same `template()` helper as the core API for reusing rule sets: ```ts title="lib/permix.ts" import { createPermix } from 'permix/tanstack-start' export const permix = createPermix<{ post: ['create', 'read', 'update', 'delete'] }>() export const adminTemplate = permix.template({ post: { create: true, read: true, update: true, delete: true }, }) export const guestTemplate = permix.template({ post: { create: false, read: true, update: false, delete: false }, }) ``` ```ts title="src/start.ts" import { adminTemplate, guestTemplate, permix } from './lib/permix' permix.setupMiddleware(async ({ request }) => { const session = await getSession(request) return session?.role === 'admin' ? adminTemplate() : guestTemplate() }) ``` ## Example [#example] A runnable app wiring all of the above together lives [here](https://github.com/letstri/permix/tree/main/examples/tanstack-start). ## How per-request isolation works [#how-per-request-isolation-works] `setupMiddleware` creates a fresh core instance per request and stores it on TanStack Start's server request context. All subsequent `get()` / `getOrThrow()` / `check()` calls within the same request share that one instance. Across concurrent requests, each gets its **own** instance — state never leaks between users. The router-context instance is separate, and isolated for the same reason: `getRouter()` is called once per request on the server, so each SSR render builds its own router with its own Permix instance. In the browser `getRouter()` runs once, so that instance doubles as the per-tab client cache. | | Server request context | Router context | | ------------ | --------------------------------------------- | ----------------------------------------------------- | | Created by | `setupMiddleware()` | `getRouter()` | | Read with | `permix.get(context)` / `getOrThrow(context)` | `context.permix` | | Available in | server functions, server routes | `beforeLoad`, `loader`, components | | Rules | full, including function-based | hydrated booleans (until you `setup()` on the client) | | Trustworthy | yes — enforcement | no — UX only | ## API [#api] ### `createPermix(options?)` [#createpermixdoptions] Returns an object with the following methods: | Method | Description | | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `setupMiddleware(rules \| ({ request }) => rules)` | A **request** middleware that creates a per-request instance and runs `setup()`. Register it globally in `src/start.ts` or on a server route. | | `createSetupHandler(rules \| ({ request }) => rules)` | The server handler behind `setupMiddleware`, for passing to your own `createMiddleware().server(...)`. Use when the callback has [server-only imports](#server-only-imports-in-the-setup-callback). | | `checkMiddleware(...args)` | A **function** middleware that enforces a permission check before a server function's handler. | | `get(context)` | Read the request-scoped [`Permix`](/docs/guide/instance) instance from a context object, or `null` if missing. | | `getOrThrow(context)` | Like `get`, but throws `PermixNotFoundError` when the instance is missing. | | `getRules(context)` | Return the current rules object for the request-scoped instance, or `null`. | | `dehydrate(context)` | Serialize the current request's rules to JSON (for `` on the client). | | `template(rules)` | Create a reusable rule set. Same as the core [`template`](/docs/guide/template). | | `contextKey(key)` | Set a custom context key (string or symbol). Chainable; returns the same helper. | | `key` | The current context key. | #### Options [#options] | Option | Description | | ----------------------------- | ------------------------------------------------------------------------------------- | | `onForbidden({ path, data })` | Called when `checkMiddleware` denies a request. Defaults to throwing a `PermixError`. | ### Hooks [#hooks] You can register hooks at the factory level to listen for events across all requests: ```ts permix.hook('check', ({ path, data }) => { console.log(`Permission checked: ${path}`, data) }) ``` # tRPC URL: https://permix.letstri.dev/docs/integrations/trpc Learn how to use Permix with tRPC ## Overview [#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](/docs/quick-start) guide. ## Setup [#setup] Here's a basic example of how to use the Permix middleware with tRPC: ```ts 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().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 [#checking-permissions] Use the `checkMiddleware` function in your tRPC procedures to check permissions: ```ts 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 [#accessing-permix-in-procedures] Permix is automatically added to your tRPC context under the key you specified, so you can access it directly: ```ts 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 permission * `dehydrate`: Serialize the current rules for client hydration * `template`: Create reusable permission templates ## Using Templates [#using-templates] Permix provides a template helper to create reusable permission rule sets: ```ts // 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 [#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 [#throw-a-custom-error] ```ts const permix = createPermix({ onForbidden: ({ path, ctx }) => { throw new TRPCError({ code: 'FORBIDDEN', message: `User ${ctx.user.id} doesn't have permission for ${path}`, }) }, }).contextKey('permissions') ``` ### Allow Through [#allow-through] You can also let denied requests through by calling `next()`: ```ts const permix = createPermix({ 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 check * `ctx`: Your tRPC context object * `next`: The middleware `next` function — call it to allow the request through ## Advanced Usage [#advanced-usage] ### Async Permission Rules [#async-permission-rules] You can use async functions in your permission setup: ```ts 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 [#hooks] You can register hooks at the factory level to listen for events across all requests: ```ts permix.hook('check', ({ path, data }) => { console.log(`Permission checked: ${path}`, data) }) ``` # Vue URL: https://permix.letstri.dev/docs/integrations/vue Learn how to use Permix with Vue applications ## Overview [#overview] Permix provides official Vue integration through the `PermixProvider` component and `usePermix` composable. This allows you to manage permissions reactively in your Vue app. Before getting started with Vue integration, make sure you've completed the initial setup steps in the [Quick Start](/docs/quick-start) guide. ## Setup [#setup] First, wrap your application with the `PermixProvider`: ```vue title="App.vue" ``` You can also register the provider from `main.ts`: ```ts title="main.ts" import { createApp } from 'vue' import { PermixProvider } from 'permix/vue' import { permix } from './lib/permix' import App from './App.vue' createApp({ components: { PermixProvider, App }, template: '', setup() { return { permix } }, }).mount('#app') ``` Remember to always pass the same Permix instance to both the `PermixProvider` and `usePermix` composable to maintain type safety. ## Composable [#composable] For checking permissions in your components, you can use the `usePermix` composable. And to avoid importing the composable and Permix instance in every component, you can create a custom composable: ```ts title="composables/use-permissions.ts" import { usePermix } from 'permix/vue' import { permix } from './lib/permix' export function usePermissions() { return usePermix(permix) } ``` ## Components [#components] If you prefer using components, you can import the `createComponents` function from `permix/vue` and create checking components: ```ts title="lib/permix.ts" import { createComponents } from 'permix/vue' // ... export const { Check } = createComponents(permix) ``` And then you can use the `Check` component in your templates: ```vue title="page.vue" ``` ## Usage [#usage] Use the `usePermix` composable in your components to check permissions: ```vue title="page.vue" ``` ## Hydration [#hydration] For SSR applications, use `PermixHydrate` to restore dehydrated server state on the client. `hydrate()` does not mark the instance ready and cannot restore function-based rules — call `setup()` on the client with the full rule set (usually in the same place you restore the session): ```vue title="App.vue" ``` See the [Hydration guide](/docs/guide/hydration). ## Example [#example] You can find the example of the Vue integration [here](https://github.com/letstri/permix/tree/main/examples/vue). # Migrate v3 to v4 URL: https://permix.letstri.dev/docs/migration-v3-to-v4 Step-by-step guide for upgrading Permix from v3 to v4 ## Overview [#overview] Permix **v4** is a major release. It replaces the entity-config permission model with **action lists and dot paths**, rewrites the core `check()` engine, and adds first-class integrations (Next.js, TanStack Start, Svelte, Drizzle, Effect, fetch middleware, and more). This guide summarizes the breaking changes and how to update your app. For the full release context, see [PR #35](https://github.com/letstri/permix/pull/35). ## Install [#install] npm pnpm yarn bun ```bash npm install permix@^4 ``` ```bash pnpm add permix@^4 ``` ```bash yarn add permix@^4 ``` ```bash bun add permix@^4 ``` v4 is developed with **TypeScript 6**. Your app does not need to match the monorepo's exact TypeScript or pnpm versions, but upgrade TypeScript if you hit inference issues. ## Quick reference [#quick-reference] | v3 | v4 | | -------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | `createPermix<{ post: { action: 'read'; dataType: Post } }>()` | `createPermix<{ post: ['read', { name: 'edit', type: Post }] }>()` | | `dataRequired: true` on entity | `required: true` on the action spec | | `permix.check('post', 'read')` | `permix.check('post.read')` | | `permix.check('post', 'edit', post)` | `permix.check('post.edit', post)` | | `permix.check('post', 'all')` | `permix.check('post.~all')` | | `permix.check('post', 'any')` | `permix.check('post.~any')` | | `permix.check('post', ['read', 'update'])` | `permix.check(c => c('post.read') && c('post.update'))` | | `` | `` | | `checkMiddleware('post', 'create')` | `checkMiddleware('post.create')` | | `check()` before `setup()`: logs + returns `false` | `check()` before rules exist: throws `PermixNotReadyError` | | `hydrate()` + `isReady()` | Still `false` until `setup()`; hydrated booleans are `check()`-able in both versions | | Invalid / missing path: logs + `false` | `PermixRuleNotDefinedError` thrown | | `checkAsync()` on core instance | Removed — use `isReadyAsync()` then `check()` | | tRPC/oRPC `forbiddenError` option | `onForbidden` on `createPermix({ ... })` | `setup()`, `template()`, `dehydrate()`, `hook()`, and `isReady()` / `isReadyAsync()` still exist on the core instance; their **types** and some **semantics** changed. *** ## 1. Update permission definitions [#1-update-permission-definitions] v3 modeled each entity as a config object with `action`, optional `dataType`, and optional `dataRequired`: ```ts // v3 import { createPermix } from 'permix' export const permix = createPermix<{ post: { dataType: Post action: 'read' | 'edit' dataRequired?: true } }>() ``` v4 uses a **tuple of action names** or **action specs** per entity: ```ts // v4 — actions without entity data export const permix = createPermix<{ post: ['read', 'edit'] }>() // v4 — typed entity data per action export const permix = createPermix<{ post: [ 'read', { name: 'edit', type: Post }, { name: 'delete', type: Post, required: true }, ] }>() ``` v4 also supports **nested** permission trees (not available in v3's definition model): ```ts createPermix<{ workspace: { billing: ['view', 'update'] member: ['invite', 'remove'] } }>() permix.check('workspace.billing.view') ``` ### Enum-based actions [#enum-based-actions] If you used enums for actions, pass enum members in the tuple instead of `action: MyEnum`: ```diff export const permix = createPermix<{ - post: { action: PostPermission } + post: [ + PostPermission.Create, + PostPermission.Read, + PostPermission.Update, + PostPermission.Delete, + ] }>() ``` `setup()` / `template()` rule objects are unchanged — still keyed by action name. ### Flat (non-nested) definitions [#flat-non-nested-definitions] v4 supports a flat tuple when you do not need entity grouping: ```ts const permix = createPermix<['read', 'write']>() permix.setup({ read: true, write: false }) permix.check('read') ``` See the [Instance guide](/docs/guide/instance) for `MergePermix`, `ValidateDefinition`, and [initial rules](/docs/guide/instance#initial-rules). *** ## 2. Update `check()` calls [#2-update-check-calls] Replace the two-argument entity/action form with **dot paths**: ```diff -permix.check('post', 'read') -permix.check('post', 'edit', post) +permix.check('post.read') +permix.check('post.edit', post) ``` ### Aggregate checks (`all` / `any`) [#aggregate-checks-all--any] v3 used the literals `'all'` and `'any'` as the second argument on an entity. v4 uses **`~all`** and **`~any`** as path segments: ```diff -permix.check('post', 'all') -permix.check('post', 'any') +permix.check('post.~all') +permix.check('post.~any') -permix.check('post', ['read', 'update']) +permix.check(c => c('post.read') && c('post.update')) ``` v4 also supports tree-wide aggregation (new): `permix.check('~all')`, `permix.check('~any')`. Details: [Check guide](/docs/guide/check). ### Callback composition (new) [#callback-composition-new] ```ts permix.check(c => c('post.read') && c('post.update')) permix.check(c => c('post.delete') || c('admin.override')) ``` ### `checkAsync()` removed [#checkasync-removed] v3 exposed `permix.checkAsync(...)` which waited for `setup()` then delegated to `check()`. In v4, await readiness explicitly: ```diff -const allowed = await permix.checkAsync('post', 'read') +await permix.isReadyAsync() +const allowed = permix.check('post.read') ``` ### Path types without duplicating the schema [#path-types-without-duplicating-the-schema] ```ts const permix = createPermix<{ user: ['create']; job: ['remove'] }>() type PermissionPath = typeof permix.$inferPath // 'user.create' | 'job.remove' ``` *** ## 3. Update UI components and hooks [#3-update-ui-components-and-hooks] React, Vue, Solid, and Svelte integrations use a single **`path`** prop (and optional `data`) instead of `entity` + `action`: ```diff - + ... ``` ```diff -const canEdit = check('post', 'edit', post) +const canEdit = check('post.edit', post) ``` `usePermix` / composables delegate to the same `check()` implementation as core. **If there are no rules yet** (neither `setup()` nor `hydrate()`), `check()` throws `PermixNotReadyError` — it does not return `false` like v3 did. Gate UI on `isReady` (or catch errors) before calling `check()`: ```tsx const { check, isReady } = usePermix(permix) if (!isReady) { return
Loading permissions…
} const canEdit = check('post.edit', post) ``` After `hydrate()`, the provider updates rules via the `setup` hook, so **serialized** permissions can be checked before `isReady` is `true` — but you should still call `setup()` on the client to restore function-based rules and mark the instance ready. * [React](/docs/integrations/react) * [Vue](/docs/integrations/vue) * [Solid](/docs/integrations/solid) * [Svelte](/docs/integrations/svelte) (new in v4) *** ## 4. Hydration and ready state [#4-hydration-and-ready-state] Serialization is the same: **functions become `false` in JSON**. Ready-state behavior: | | v3 | v4 | | ---------------------------------- | --------------------------------------- | --------------------------- | | `isReady()` after `hydrate()` only | `false` (until `setup()` on the client) | `false` (until `setup()`) | | `check()` after `hydrate()` only | Works for hydrated booleans | Works for hydrated booleans | | `check()` before any rules | `false` + console error | `PermixNotReadyError` | | `dehydrate()` before rules | Throws generic `Error` | `PermixNotReadyError` | ```ts permix.hydrate(serverState) // isReady() === false permix.check('post.create') // true if dehydrated as true — even while not ready permix.setup(getClientRules(user)) // isReady() === true; function-based rules restored ``` `hydrate()` fires the **`setup` hook** (not a separate `hydrate` hook). Update listeners that used `hook('hydrate', ...)` in v3. See [Hydration](/docs/guide/hydration) and [Ready state](/docs/guide/ready). For App Router / TanStack Start, see [Next.js](/docs/integrations/next) and [TanStack Start](/docs/integrations/tanstack-start). *** ## 5. Error handling [#5-error-handling] v4 throws typed errors instead of logging to the console and returning `false`: | Error | When | | --------------------------- | ------------------------------------------------------- | | `PermixNotReadyError` | `check()` or `dehydrate()` when no rules exist yet | | `PermixRuleNotDefinedError` | Path not in schema or rule missing (`error.path`) | | `PermixNotFoundError` | Server integration: Permix missing from request context | Custom `onForbidden` handlers receive `{ path, data?, ... }` instead of v3's `{ entity, actions, ... }`. Wrap `check()` in try/catch only when you intentionally handle these cases. *** ## 6. Server integrations [#6-server-integrations] Middleware patterns are familiar; **paths**, **context setup**, and **error option names** changed. ### tRPC / oRPC [#trpc--orpc] In v3, `createPermix` from `permix/trpc` (or `permix/orpc`) exposed its own **`setup(rules)`** that returned a request-scoped `{ check, dehydrate }` object for context. Core `createPermix` from `permix` was separate. v4 uses **`setupContext(rules)`**, which returns `{ [contextKey]: PermixInstance }`, and optional **`.contextKey('name')`**: ```diff - import { createPermix } from 'permix/trpc' + import { createPermix } from 'permix/trpc' const permix = createPermix<{ - post: { dataType: Post; action: 'create' | 'read' } -}>() + post: ['create', 'read', 'update', 'delete'] +}>().contextKey('permissions') // optional; default key is 'permix' protectedProcedure.use(({ ctx, next }) => { - const p = permix.setup({ post: { ... } }) - return next({ ctx: { permix: p } }) + return next({ + ctx: permix.setupContext({ post: { ... } }), + }) }) -createPost.use(permix.checkMiddleware('post', 'create')) +createPost.use(permix.checkMiddleware('post.create')) -updatePost.use(permix.checkMiddleware('post', ['read', 'update'])) +updatePost.use(permix.checkMiddleware(c => c('post.read') && c('post.update'))) -adminAction.use(permix.checkMiddleware('post', 'all')) +adminAction.use(permix.checkMiddleware('post.~all')) ``` Other tRPC/oRPC changes: * `forbiddenError` → **`onForbidden`** on `createPermix({ onForbidden })`. * Default context key is still **`permix`**; use `.contextKey('permissions')` for `ctx.permissions`. * Context Permix is a **full instance** (`check`, `dehydrate`, `setup`, `template`, …), not only `check` + `dehydrate`. [tRPC integration](/docs/integrations/trpc) · [oRPC integration](/docs/integrations/orpc) ### Express, Hono, Fastify, Elysia, Node [#express-hono-fastify-elysia-node] `setupMiddleware` / `checkMiddleware` already existed in v3; update definitions and dot-path checks. v4 Express middleware can pass rules directly or via a callback: ```ts app.use(permix.setupMiddleware({ post: { read: true } })) // or app.use(permix.setupMiddleware(async ({ req }) => ({ post: { read: req.user.isAdmin } }))) ``` Consider **`permix/server`** for fetch-style handlers (Web Standard `Request` / `Response`): [Server middleware](/docs/integrations/server) ### Better Auth [#better-auth] The v3 Better Auth integration is **removed** in v4. There is no `permix/better-auth` package, no `permixPlugin`, and no session helpers (`permixClient`, `createPermix`). Map each Better Auth role to a rules object yourself — the same booleans you previously got from `roleToRules()`. Use [React](/docs/integrations/react) (or your UI integration) and [hydration](/docs/guide/hydration) as with any other Permix setup. *** ## 7. New packages (optional) [#7-new-packages-optional] These are additive — migrate the core API first, then adopt what you need: | Import | Use case | | ----------------------- | ------------------------------------------- | | `permix/next` | Next.js App Router, request-scoped instance | | `permix/tanstack-start` | TanStack Start middleware and SSR | | `permix/server` | Framework-agnostic fetch middleware | | `permix/svelte` | Svelte 5 runes | | `permix/drizzle` | Rules from Drizzle v1 schema | | `permix/drizzle/legacy` | Drizzle v0 (`>=0.30 <1`) | | `permix/effect` | Effect `Layer` / `Context` | See the [examples directory](https://github.com/letstri/permix/tree/main/examples) (`next`, `tanstack-start`, `svelte`, `rebac`, and updated `react`, `vue`, …). *** ## 8. Migration checklist [#8-migration-checklist] ### Bump `permix` to v4 [#bump-permix-to-v4] npm pnpm yarn bun ```bash npm install permix@^4 ``` ```bash pnpm add permix@^4 ``` ```bash yarn add permix@^4 ``` ```bash bun add permix@^4 ``` ### Replace `createPermix` generics [#replace-createpermix-generics] Convert `{ action, dataType, dataRequired }` → action tuples / specs. ### Replace every `check('entity', 'action')` [#replace-every-checkentity-action] Use `'entity.action'` dot paths (and callbacks for multi-action AND). ### Update `` / composables [#update-check--composables] `path="entity.action"` instead of `entity` + `action`; gate on `isReady` before `check()`. ### Fix SSR hydration [#fix-ssr-hydration] After `hydrate()`, call `setup()` for function rules; replace `hook('hydrate')` with `hook('setup')` if needed. ### Update server middleware [#update-server-middleware] tRPC/oRPC: `setupContext`, dot-path `checkMiddleware`, `forbiddenError` → `onForbidden`. ### Remove Better Auth plugin (if used) [#remove-better-auth-plugin-if-used] Drop `permixPlugin`, `permixClient`, and `permix/better-auth`. Wire Better Auth roles to core `permix.setup()` — see [Better Auth](#better-auth) above. ### Run tests and typecheck [#run-tests-and-typecheck] Use your project's usual commands (for example `pnpm run check-types` and `pnpm test` in this monorepo). ## What stayed the same [#what-stayed-the-same] * **Rules shape** in `setup()` — nested objects with boolean or function values. * **`template()`** for reusable rule sets. * **`dehydrate()` / `hydrate()`** for SSR (ready-state and client `setup()` caveats above). * **`hook('setup')` / `hook('ready')`** events and **`isReady()` / `isReadyAsync()`** (`isReadyAsync()` now resolves to `void`). * **Philosophy** — type-safe, framework-agnostic, ReBAC-friendly function rules ([ReBAC guide](/docs/guide/rebac)). ## Further reading [#further-reading] * [Quick Start](/docs/quick-start) — v4 defaults * [Instance](/docs/guide/instance) — definitions, `$inferPath`, initial rules * [Check](/docs/guide/check) — dot paths, `~all` / `~any`, callbacks * [PR #35](https://github.com/letstri/permix/pull/35) — full changelog and preview docs # Quick Start URL: https://permix.letstri.dev/docs/quick-start A quick start guide to start using Permix and validating your permissions ## Try Permix [#try-permix] Want to explore Permix before installing? Try our interactive sandbox environment where you can experiment with type-safe permissions management right in your browser. [Try Permix Sandbox](https://stackblitz.com/edit/permix-sandbox?file=src%2Fmain.ts\&terminal=dev) ## Installation via Agents [#installation-via-agents] Using Claude Code, Cursor, or another agent that supports skills? Add Permix skills, then hand it this prompt to set up permissions for you: ```bash npx skills add letstri/permix ``` ``` Install and set up a base Permix architecture for this project. 1. Detect my stack (framework(s), server/client split, e.g. React, Vue, Solid, Svelte, Next.js, TanStack Start, Node, Hono, Express, tRPC, oRPC, Fastify, Elysia, Drizzle) and install `permix` plus the matching integration package(s) from https://permix.letstri.dev/docs. 2. Scan the codebase for my real resources/entities (DB models, API routes, UI sections) and design a `createPermix<...>` schema from them instead of placeholder data. 3. Wire `permix.setup()` with actual rules for those resources, driven by my auth/session/role data wherever it already exists. 4. Add server-side checks (middleware) on the matching integration and client-side checks/guards in the relevant components or routes. 5. Follow the getting-started, setup, template, and framework integration guides in the Permix skill for conventions and API shape. ``` Or continue with the manual installation below. ## Installation [#installation] ### Install a package [#install-a-package] Typically you'll need to install Permix using your package manager: npm pnpm yarn bun ```bash npm install permix ``` ```bash pnpm add permix ``` ```bash yarn add permix ``` ```bash bun add permix ``` ### Create an instance [#create-an-instance] To create a base instance, you need to provide a schema as a generic type to `createPermix` function that defines your permissions: ```ts title="/lib/permix.ts" import { createPermix } from 'permix' export const permix = createPermix<{ post: ['create', 'read', 'update', 'delete'] }>() // ... ``` Learn more about features and configuration of instances in the [instance guide](/docs/guide/instance). ### Setup your permissions [#setup-your-permissions] You can setup your permissions by calling `setup` method on your instance in any place you want: ```ts title="/lib/permix.ts" // ... // Call setupPermissions in your application export function setupPermissions() { permix.setup({ post: { create: true, read: true, update: true, delete: false, }, }) } ``` ### Check permissions [#check-permissions] After setup, you can use `check` method to check available permissions: ```ts permix.check('post.create') // true ``` ### Finish [#finish] That's it! 🎉 You've now got a basic setup of Permix. Next, read the core guides: * [Instance](/docs/guide/instance) — schema, `ValidateDefinition`, `MergePermix`, `$inferPath` * [Setup](/docs/guide/setup) — rules, `createRules`, type-based callbacks * [Check](/docs/guide/check) — paths, `~all` / `~any`, errors * [Template](/docs/guide/template) — reusable role presets * [Hydration](/docs/guide/hydration) and [Ready state](/docs/guide/ready) — SSR and async bootstrap ## Integrations [#integrations] Continuing from the quick start, you can now explore how Permix integrates with other libraries and frameworks. Integration with React via provider and hook. Integration with Vue via plugin and composable. Integration with Node.js via middleware. Integration with native Request and Response handlers. Integration with Hono via middleware. Integration with Express via middleware. Integration with tRPC via middleware. Integration with Next.js App Router. Integration with TanStack Start. Integration with Solid via provider and hook. Integration with Svelte via provider and hook. Integration with oRPC via middleware. Integration with Fastify via plugin. Integration with Elysia via middleware. Schema-driven permissions from Drizzle tables. Integration with Effect services and layers.