Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions _artifacts/domain_map.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ domains:
- name: 'SSR and hydration'
slug: ssr
description: >
dehydrate/hydrate snapshots, PermixHydrate, Next.js and TanStack Start wiring.
dehydrate/hydrate snapshots, PermixHydrate, Next.js, TanStack Start, and React Router wiring.

skills:
- name: 'Getting started'
Expand Down Expand Up @@ -77,7 +77,7 @@ skills:
Everything past initial setup: dot-path check, callback combinators,
~all/~any, entity-aware ReBAC rules, isReady/isReadyAsync;
PermixProvider/usePermix/createComponents and SSR dehydrate/hydrate for
React, Vue, Solid, Svelte, Next.js, TanStack Start; setupMiddleware and
React, Vue, Solid, Svelte, Next.js, TanStack Start, React Router; setupMiddleware and
checkMiddleware for Express, Hono, Fastify, tRPC, oRPC, Node, Elysia.
Single skill with a thin SKILL.md router and three reference files
loaded on demand.
Expand All @@ -91,6 +91,7 @@ skills:
- svelte
- next
- tanstack-start
- react-router
- express
- hono
- fastify
Expand Down Expand Up @@ -119,7 +120,7 @@ skills:
- 'Wrap the app tree and hide actions based on permissions'
- 'Integrate permix/react, permix/vue, permix/solid, or permix/svelte'
- 'Pass permission booleans from server render to client'
- 'Wire permix/next or permix/tanstack-start'
- 'Wire permix/next, permix/tanstack-start, or permix/react-router'
- 'Protect HTTP or RPC routes on the server'
- 'Derive rules from authenticated request context'
references:
Expand All @@ -137,6 +138,7 @@ skills:
- 'letstri/permix:docs/content/docs/integrations/svelte.mdx'
- 'letstri/permix:docs/content/docs/integrations/next.mdx'
- 'letstri/permix:docs/content/docs/integrations/tanstack-start.mdx'
- 'letstri/permix:docs/content/docs/integrations/react-router.mdx'
- 'letstri/permix:docs/content/docs/integrations/express.mdx'
- 'letstri/permix:docs/content/docs/integrations/hono.mdx'
- 'letstri/permix:docs/content/docs/integrations/fastify.mdx'
Expand All @@ -153,6 +155,7 @@ coverage:
- examples/*
- next
- tanstack-start
- react-router

failure_modes:
- mistake: 'Using v3 { action, dataType } schema shape'
Expand Down
5 changes: 4 additions & 1 deletion _artifacts/skill_tree.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ skills:
check() dot paths, callbacks, ~all/~any, entity-aware ReBAC rules,
isReady/isReadyAsync; PermixProvider/usePermix/createComponents and SSR
dehydrate/hydrate for React, Vue, Solid, Svelte, Next.js, TanStack
Start; setupMiddleware/checkMiddleware for Express, Hono, Fastify,
Start, React Router; setupMiddleware/checkMiddleware for Express, Hono, Fastify,
tRPC, oRPC, Node, Elysia. Thin router with references loaded on demand.
requires:
- permix-getting-started
Expand All @@ -50,6 +50,7 @@ skills:
- svelte
- next
- tanstack-start
- react-router
- express
- hono
- fastify
Expand All @@ -72,6 +73,7 @@ skills:
- 'letstri/permix:docs/content/docs/integrations/svelte.mdx'
- 'letstri/permix:docs/content/docs/integrations/next.mdx'
- 'letstri/permix:docs/content/docs/integrations/tanstack-start.mdx'
- 'letstri/permix:docs/content/docs/integrations/react-router.mdx'
- 'letstri/permix:docs/content/docs/integrations/express.mdx'
- 'letstri/permix:docs/content/docs/integrations/hono.mdx'
- 'letstri/permix:docs/content/docs/integrations/fastify.mdx'
Expand All @@ -88,3 +90,4 @@ coverage:
- examples/*
- next
- tanstack-start
- react-router
3 changes: 2 additions & 1 deletion docs/content/docs/comparison.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Permix is a library that provides a way to manage permissions in your applicatio
| 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] |
| Size | **2.64 kB** gzip (core) [react 0.86 kB, vue 0.87 kB, solid 0.82 kB, next 1.00 kB, react-router 1.10 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.

Expand All @@ -40,6 +40,7 @@ Built with `pnpm run build` in the `permix` package (`tsdown` for all entries ex
| `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/react-router` | 1.10 kB | adapter |
| `permix/node` | 0.95 kB | adapter |
| `permix/server` | 1.10 kB | adapter |
| `permix/express` | 0.91 kB | adapter |
Expand Down
4 changes: 2 additions & 2 deletions docs/content/docs/integrations/next.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,8 @@ Returns an object with the following methods:

## 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:
The client layer (`permix/react`) is framework-agnostic. If you're using TanStack Start, React Router 7, 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
- Use the dedicated [`permix/tanstack-start`](/docs/integrations/tanstack-start) or [`permix/react-router`](/docs/integrations/react-router) integration, which follow 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.
255 changes: 255 additions & 0 deletions docs/content/docs/integrations/react-router.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
---
title: React Router
description: Learn how to use Permix with React Router 7
---

## Overview

Permix provides a dedicated integration for [React Router 7](https://reactrouter.com/) through `permix/react-router`. Remix apps that have moved to React Router 7 use this same adapter — there is no separate `permix/remix` export.

It stores a **per-request** Permix instance on React Router's middleware context (`context.set` / `context.get`), so loaders, actions, and middleware share one instance. The client reuses [`permix/react`](/docs/integrations/react): dehydrate on the server and hydrate with `PermixProvider` + `PermixHydrate`.

<Callout>
Before getting started, complete the [Quick Start](/docs/quick-start). This
adapter uses [React Router
middleware](https://reactrouter.com/how-to/middleware) (React Router 7.9+).
Enable `v8_middleware` (or the current middleware flag) in your React Router
config if it is not on by default.
</Callout>

<Steps>

<Step>

## Define your permissions

```ts title="app/lib/permix.ts"
import type { ValidateDefinition } from 'permix'
import { createPermix } from 'permix/react-router'

interface Post {
id: string
authorId: string
}

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<PermissionsDefinition>()
```

Each `createPermix()` call uses its own context key, so two factories on the same request do not collide.

</Step>

<Step>

## Setup per request

Register `setupMiddleware` on the root route so every request gets a fresh instance:

```ts title="app/root.tsx"
import { permix } from './lib/permix'
import { getSession } from './lib/auth'

export const middleware = [
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 it to a specific route's `middleware` array instead of the root. The callback also receives `context`, so rules can read values set by earlier middleware (for example `context.get(userContext)`).

</Step>

<Step>

## Check in loaders and actions

```ts title="app/routes/posts.$id.tsx"
import { data } from 'react-router'
import { permix } from '../lib/permix'
import { getPost } from '../lib/posts'
import type { Route } from './+types/posts.$id'

export async function loader({ params, context }: Route.LoaderArgs) {
const post = await getPost(params.id)

if (!permix.getOrThrow(context).check('post.read', post)) {
throw data('Not Found', { status: 404 })
}

return { post, permixState: permix.dehydrate(context) }
}

export async function action({ context }: Route.ActionArgs) {
if (!permix.getOrThrow(context).check('post.create')) {
throw data({ error: 'Forbidden' }, { status: 403 })
}

return { ok: true }
}
```

To guard a whole route, compose `checkMiddleware` after setup:

```ts title="app/routes/posts.new.tsx"
import { permix } from '../lib/permix'

export const middleware = [permix.checkMiddleware('post.create')]
```

Denied requests default to `403` with `{ error: 'Forbidden' }`. Customize with `onForbidden` (for example to `redirect`).

</Step>

<Step>

## Hydrate the client

Dehydrate in a loader (often the root loader) and wrap the tree with `permix/react`:

```tsx title="app/root.tsx"
import { PermixHydrate, PermixProvider } from 'permix/react'
import { createPermix } from 'permix'
import { permix as serverPermix } from './lib/permix'
import type { PermissionsDefinition } from './lib/permix'
import type { Route } from './+types/root'

const clientPermix = createPermix<PermissionsDefinition>()

export async function loader({ context }: Route.LoaderArgs) {
return { permixState: serverPermix.dehydrate(context) }
}

export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

export default function App({ loaderData }: Route.ComponentProps) {
return (
<PermixProvider permix={clientPermix}>
<PermixHydrate state={loaderData.permixState}>
<Outlet />
</PermixHydrate>
</PermixProvider>
)
}
```

<Callout type="warn">
`hydrate()` restores booleans but does not restore function-based rules or
mark the instance ready. Call `clientPermix.setup(...)` on the client with the
full rule set after hydration. See the [Hydration
guide](/docs/guide/hydration).
</Callout>

</Step>

<Step>

## Use on the client

```tsx title="app/components/edit-button.tsx"
import { usePermix } from 'permix/react'

export function EditButton({
permix,
post,
}: {
permix: Parameters<typeof usePermix>[0]
post: { id: string; authorId: string }
}) {
const { check } = usePermix(permix)

if (!check('post.update', post)) {
return null
}

return <button>Edit post</button>
}
```

Pass the same client singleton you gave to `PermixProvider`. See the [React integration](/docs/integrations/react) for `createComponents`.

</Step>

<Step>

## Templates

```ts title="app/lib/permix.ts"
import { createPermix } from 'permix/react-router'

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="app/root.tsx"
export const middleware = [
permix.setupMiddleware(async ({ request }) => {
const session = await getSession(request)
return session?.role === 'admin' ? adminTemplate() : guestTemplate()
}),
]
```

</Step>

<Step>

## Example

You can find a runnable example of the React Router integration [here](https://github.com/letstri/permix/tree/main/examples/react-router).

</Step>

</Steps>

## Remix

Remix merged into React Router 7. Use `permix/react-router` — do not look for a `permix/remix` export.

## API

### `createPermix<D>(options?)`

| Method | Description |
| --- | --- |
| `setupMiddleware(rules \| callback)` | Middleware that creates a per-request instance on `context`. |
| `checkMiddleware(...args)` | Middleware that allows or returns the `onForbidden` response. |
| `get(context)` | Return the instance, or `null`. |
| `getOrThrow(context)` | Return the instance, or throw `PermixNotFoundError`. |
| `dehydrate(context)` | Serialize rules for `<PermixHydrate>`. |
| `getRules(context)` | Return the current rules, or `null`. |
| `template(rules)` | Create a reusable rule set. |
| `context` | The opaque key passed to `context.set` / `context.get`. |
2 changes: 1 addition & 1 deletion docs/content/docs/integrations/react.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ function App({ dehydratedState }: { dehydratedState: DehydratedState<any> }) {
}
```

See the [Hydration guide](/docs/guide/hydration) and framework-specific pages for [Next.js](/docs/integrations/next) and [TanStack Start](/docs/integrations/tanstack-start).
See the [Hydration guide](/docs/guide/hydration) and framework-specific pages for [Next.js](/docs/integrations/next), [TanStack Start](/docs/integrations/tanstack-start), and [React Router](/docs/integrations/react-router).

</Step>

Expand Down
1 change: 1 addition & 0 deletions docs/content/docs/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"integrations/react",
"integrations/next",
"integrations/tanstack-start",
"integrations/react-router",
"integrations/vue",
"integrations/solid",
"integrations/svelte",
Expand Down
3 changes: 2 additions & 1 deletion docs/content/docs/migration-v3-to-v4.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ permix.setup(getClientRules(user))

`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).
See [Hydration](/docs/guide/hydration) and [Ready state](/docs/guide/ready). For App Router / TanStack Start / React Router, see [Next.js](/docs/integrations/next), [TanStack Start](/docs/integrations/tanstack-start), and [React Router](/docs/integrations/react-router).

---

Expand Down Expand Up @@ -340,6 +340,7 @@ These are additive — migrate the core API first, then adopt what you need:
| ----------------------- | ------------------------------------------- |
| `permix/next` | Next.js App Router, request-scoped instance |
| `permix/tanstack-start` | TanStack Start middleware and SSR |
| `permix/react-router` | React Router 7 middleware and SSR |
| `permix/server` | Framework-agnostic fetch middleware |
| `permix/svelte` | Svelte 5 runes |
| `permix/drizzle` | Rules from Drizzle v1 schema |
Expand Down
Loading