Permix Documentation

repository·main·Indexed 20 days ago

https://github.com/letstri/permix

A lightweight, framework-agnostic, and type-safe permissions management library for JavaScript applications on both client and server sides. Permix provides native support for hydration, ReBAC logic, and integrations with frameworks like TanStack Start, React, Vue, Solid, and Svelte, as well as server middleware for Express, Hono, Fastify, tRPC, and oRPC.

Tokens
66K
Snippets
233
Records
270
Agent score
69%

What's inside Permix

  1. Use Permix for authorization after initial setup

    main

    Permix is used to apply authorization once a schema has been established. It provides tools for checking permissions via permix.check(), implementing ReBAC callbacks, binding to frontend frameworks (React, Vue, Solid, Svelte), and protecting server routes via middleware.

    Note: This package is for usage after your initial setup. To create a schema and perform the first permix.setup(), you must use permix-getting-started first.

  2. Core rules for Permix authorization

    main

    When using Permix, follow these critical rules to ensure security and consistency:

    1. Server-side enforcement is mandatory: Client-side check calls (in React, Vue, Solid, or Svelte) are for UX purposes only (e.g., hiding buttons). You must mirror every permission path with checkMiddleware on the server to actually protect routes and data.
    2. Maintain path consistency: Use the exact same schema and path strings (e.g., post.update) across both client hooks and server middleware. Using ad-hoc strings will cause types and behavior to drift.
    3. Handle initialization timing: Calling check before the instance is ready throws a PermixNotReadyError.
      • On the frontend, gate your UI using isReady or isReadyAsync.
      • On the server, ensure setupMiddleware is called before checkMiddleware.
    4. SSR Hydration requirements: SSR hydrate only restores boolean values. If you use function-based rules or ReBAC, you must call setup again on the client side.
  3. Use type-based rules for entity data

    main

    If a permission check requires access to the data of the entity being acted upon (e.g., checking if a user is the author of a post), you can attach a type to the action in your schema definition. Instead of a simple boolean, the rule in setup becomes a function that accepts that type.

    import { createPermix } from 'permix'
    
    interface Post {
      id: string
      authorId: string
    }
    
    const permix = createPermix<{
      post: [{ name: 'update', type: Post }]
      comment: ['update']
    }>()
    
    permix.setup({
      post: {
        update: (post) => post.authorId === 'some-id'
      },
      comment: {
        update: (comment) => !!comment
      }
    })
  4. Identify what remains unchanged in Permix v4

    main

    The following core concepts and APIs maintain their behavior and structure in v4:

    • Rules shape: The setup() function still accepts nested objects containing boolean or function values.
    • template(): Used for creating reusable rule sets.
    • SSR Lifecycle: dehydrate() and hydrate() remain the primary methods for SSR, though hydrate() requires a subsequent setup() call for function rules.
    • Lifecycle Hooks: hook('setup') and hook('ready') are still available.
    • Readiness Checks: isReady() and isReadyAsync() are still used (note: isReadyAsync() now resolves to void).
    • Core Philosophy: The system remains type-safe, framework-agnostic, and ReBAC-friendly.
  5. React to the ready event

    main

    The ready event fires exactly once when the instance is first marked as ready (either during the initial createPermix call or the first call to setup()).

    To react to this state change, you must register your listener using permix.hook('ready', ...) before the first setup() call. Registering it after the instance is already ready will not trigger the event.

  6. Use async permission rules and data-based checks

    main

    Permix supports advanced permission scenarios:

    1. Async Rules: The function passed to setupMiddleware can be async, allowing you to fetch user permissions from a database or external service during the request lifecycle.
    2. Data-Based Permissions: You can pass specific data objects to check or checkMiddleware to perform fine-grained authorization (e.g., checking if a user owns a specific post).
    // Async setup
    permix.setupMiddleware(async ({ req }) => {
      const userId = req.headers.get('x-user-id')
      const userPermissions = await getUserPermissions(userId)
      return { post: { create: userPermissions.canCreatePosts } }
    })
    
    // Data-based check
    const { check } = permix.getOrThrow(req)
    if (check('post.update', post)) {
      // User can update this specific post
    }
  7. Perform async and data-based permission checks

    main

    Permix supports advanced permission logic:

    1. Async Rules: The setupMiddleware function can be async, allowing you to fetch user permissions from a database or external service before returning the permission object.
    2. Data-Based Permissions: You can pass specific entity data to the check function (either via permix.get(request).check(path, data) or within a custom preHandler) to validate permissions against specific resource instances (e.g., checking if a user owns a specific post).
    // Async setup
    await fastify.register(permix.setupMiddleware(async ({ request }) => {
      const userPermissions = await getUserPermissions(request.user.id)
      return {
        post: {
          create: userPermissions.canCreatePosts,
          read: userPermissions.canReadPosts,
          update: userPermissions.canUpdatePosts
        }
      }
    }))
    
    // Data-based check in handler
    fastify.put('/posts/:id', {
      preHandler: async (request, reply) => {
        const post = await getPostById(request.params.id)
        const { check } = permix.get(request)
    
        if (check('post.update', post)) {
          return
        } else {
          reply.status(403).send({ error: 'You cannot update this post' })
        }
      }
    })
  8. How Permix integrates with TanStack Start

    main

    The integration between Permix and TanStack Start relies on several key architectural patterns to ensure security and efficient client-side hydration:

    Per-request setup

    In src/start.ts, a global request middleware is registered using createMiddleware().server(permix.createSetupHandler(...)). This ensures that every incoming request receives an isolated Permix instance. By using the .server() boundary, the setup callback (which contains server-only imports) is automatically stripped from the client bundle.

    Server-side checks

    Server functions can enforce permissions by calling permix.getOrThrow(context) or checkMiddleware(). These are typically invoked from route loaders to ensure the user has the necessary rights before executing server-side logic.

    Router context and Hydration

    • Router Context: The Permix instance is placed on the TanStack Router context in src/router.tsx.
    • Hydration: The root route's beforeLoad hook hydrates the router-context instance with state transferred from the server.
    • Client Access: The same instance from the router context is used by PermixProvider and PermixHydrate to provide permission state to the client-side UI.

    Route Guards

    Permissions can be checked directly in the router's beforeLoad hook without needing a server function. For example, a route can call context.permix.check('permission.name') to guard access.

  9. Use async and data-based permission rules

    main

    Permix supports advanced permission logic:

    1. Async Rules: The setupMiddleware callback can be async, allowing you to fetch permissions from a database or external service.
    2. Data-Based Permissions: When using check manually in a handler, you can pass the specific entity data as a second argument. This allows you to verify if a user has permission to access a specific instance of an object (e.g., checking if a user is the author of a specific post).
    // Async setup
    app.use(permix.setupMiddleware(async ({ c }) => {
      const user = c.get('user')
      const userPermissions = await getUserPermissions(user.id)
      return { post: { create: userPermissions.canCreatePosts } }
    }))
    
    // Data-based check in handler
    app.put('/posts/:id', async (c) => {
      const post = await getPostById(c.req.param('id'))
      const { check } = permix.get(c)
    
      if (check('post.update', post)) {
        return c.json({ success: true })
      }
      return c.json({ error: 'Forbidden' }, 403)
    })
  10. Define complex permissions with types and closures

    main

    Permix allows you to define granular permissions using a PermissionsDefinition. You can associate actions with specific TypeScript types and use closures (functions) to implement relationship-based access control (ReBAC). This allows you to check permissions against specific object instances (e.g., checking if a user is the author of a comment).

    import { createPermix } from 'permix'
    
    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 }
    
    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<PermissionsDefinition>()
    
    // Using templates to create reusable permission sets
    const userPermissions = permix.template(({ id: userId }: User) => ({
      post: {
        create: false,
        read: true,
        update: false,
        delete: false,
      },
      comment: {
        create: true,
        read: true,
        // Relationship-based rule using a closure
        update: (comment: Comment) => comment?.authorId === userId,
      },
    }))
    
    // Applying the template via setup
    const user: User = { id: '1', role: 'user' }
    permix.setup(userPermissions(user))
    
    // Checking permission against an instance
    const comment: Comment = { id: '1', content: 'Hello', authorId: '1' }
    const canUpdate = permix.check('comment.update', comment) // true
  11. Understand the Permix skill inventory and loading logic

    main

    Permix provides two primary skill sets for coding agents to facilitate integration:

    • permix-getting-started (Type: core): Loaded for initial installations. Covers schema design, roles, and templates.
    • permix (Type: core): Loaded for advanced implementation. Covers authorization (ReBAC), frontend adapters (React, Vue, Solid, Svelte) with SSR, and server middleware (Express, Hono, Fastify, tRPC, oRPC).

    The permix skill acts as a router that loads specific reference files on demand: references/check.md, references/frontend.md, and references/server.md.

  12. How per-request isolation works in TanStack Start

    main

    Permix uses two distinct layers of isolation to ensure security and prevent state leakage between users:

    1. Server Request Context: Created via setupMiddleware(). This creates a fresh core instance per request. All calls to get(), getOrThrow(), or check() within a single request share this instance. This is the enforcement layer (trustworthy).
    2. Router Context: Created via getRouter(). On the server, each SSR render builds its own router with its own Permix instance. In the browser, the instance acts as a per-tab cache. This is the UX layer (not trustworthy, as rules are hydrated as plain booleans).
    FeatureServer request contextRouter context
    Created bysetupMiddleware()getRouter()
    Read withpermix.get(context) / getOrThrow(context)context.permix
    Available inserver functions, server routesbeforeLoad, loader, components
    Rulesfull, including function-basedhydrated booleans (until you setup() on the client)
    Trustworthyyes — enforcementno — UX only