diff --git a/_artifacts/domain_map.yaml b/_artifacts/domain_map.yaml
index 318aa7c9..c1823ab9 100644
--- a/_artifacts/domain_map.yaml
+++ b/_artifacts/domain_map.yaml
@@ -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'
@@ -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.
@@ -91,6 +91,7 @@ skills:
- svelte
- next
- tanstack-start
+ - react-router
- express
- hono
- fastify
@@ -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:
@@ -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'
@@ -153,6 +155,7 @@ coverage:
- examples/*
- next
- tanstack-start
+ - react-router
failure_modes:
- mistake: 'Using v3 { action, dataType } schema shape'
diff --git a/_artifacts/skill_tree.yaml b/_artifacts/skill_tree.yaml
index 70b42866..2d1d6a0d 100644
--- a/_artifacts/skill_tree.yaml
+++ b/_artifacts/skill_tree.yaml
@@ -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
@@ -50,6 +50,7 @@ skills:
- svelte
- next
- tanstack-start
+ - react-router
- express
- hono
- fastify
@@ -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'
@@ -88,3 +90,4 @@ coverage:
- examples/*
- next
- tanstack-start
+ - react-router
diff --git a/docs/content/docs/comparison.mdx b/docs/content/docs/comparison.mdx
index d0d9ea67..760b2971 100644
--- a/docs/content/docs/comparison.mdx
+++ b/docs/content/docs/comparison.mdx
@@ -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.
@@ -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 |
diff --git a/docs/content/docs/integrations/next.mdx b/docs/content/docs/integrations/next.mdx
index 5f1f1204..298ad0dd 100644
--- a/docs/content/docs/integrations/next.mdx
+++ b/docs/content/docs/integrations/next.mdx
@@ -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.
diff --git a/docs/content/docs/integrations/react-router.mdx b/docs/content/docs/integrations/react-router.mdx
new file mode 100644
index 00000000..b9192900
--- /dev/null
+++ b/docs/content/docs/integrations/react-router.mdx
@@ -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`.
+
+
+ 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.
+
+
+
+
+
+
+## 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()
+```
+
+Each `createPermix()` call uses its own context key, so two factories on the same request do not collide.
+
+
+
+
+
+## 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)`).
+
+
+
+
+
+## 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`).
+
+
+
+
+
+## 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()
+
+export async function loader({ context }: Route.LoaderArgs) {
+ return { permixState: serverPermix.dehydrate(context) }
+}
+
+export function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default function App({ loaderData }: Route.ComponentProps) {
+ return (
+
+
+
+
+
+ )
+}
+```
+
+
+ `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).
+
+
+
+
+
+
+## Use on the client
+
+```tsx title="app/components/edit-button.tsx"
+import { usePermix } from 'permix/react'
+
+export function EditButton({
+ permix,
+ post,
+}: {
+ permix: Parameters[0]
+ post: { id: string; authorId: string }
+}) {
+ const { check } = usePermix(permix)
+
+ if (!check('post.update', post)) {
+ return null
+ }
+
+ return
+}
+```
+
+Pass the same client singleton you gave to `PermixProvider`. See the [React integration](/docs/integrations/react) for `createComponents`.
+
+
+
+
+
+## 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()
+ }),
+]
+```
+
+
+
+
+
+## Example
+
+You can find a runnable example of the React Router integration [here](https://github.com/letstri/permix/tree/main/examples/react-router).
+
+
+
+
+
+## Remix
+
+Remix merged into React Router 7. Use `permix/react-router` — do not look for a `permix/remix` export.
+
+## API
+
+### `createPermix(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 ``. |
+| `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`. |
diff --git a/docs/content/docs/integrations/react.mdx b/docs/content/docs/integrations/react.mdx
index 2eb3e010..681922c2 100644
--- a/docs/content/docs/integrations/react.mdx
+++ b/docs/content/docs/integrations/react.mdx
@@ -156,7 +156,7 @@ function App({ dehydratedState }: { dehydratedState: DehydratedState }) {
}
```
-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).
diff --git a/docs/content/docs/meta.json b/docs/content/docs/meta.json
index dbd79bfc..bd98edf3 100644
--- a/docs/content/docs/meta.json
+++ b/docs/content/docs/meta.json
@@ -18,6 +18,7 @@
"integrations/react",
"integrations/next",
"integrations/tanstack-start",
+ "integrations/react-router",
"integrations/vue",
"integrations/solid",
"integrations/svelte",
diff --git a/docs/content/docs/migration-v3-to-v4.mdx b/docs/content/docs/migration-v3-to-v4.mdx
index 20be84ca..b9a12166 100644
--- a/docs/content/docs/migration-v3-to-v4.mdx
+++ b/docs/content/docs/migration-v3-to-v4.mdx
@@ -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).
---
@@ -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 |
diff --git a/docs/content/docs/quick-start.mdx b/docs/content/docs/quick-start.mdx
index c6bc2196..ba3ba407 100644
--- a/docs/content/docs/quick-start.mdx
+++ b/docs/content/docs/quick-start.mdx
@@ -170,6 +170,10 @@ Continuing from the quick start, you can now explore how Permix integrates with
Integration with TanStack Start.
+
+ Integration with React Router 7 middleware and hydration.
+
+
Integration with Solid via provider and hook.
diff --git a/examples/react-router/main.ts b/examples/react-router/main.ts
new file mode 100644
index 00000000..e22135d0
--- /dev/null
+++ b/examples/react-router/main.ts
@@ -0,0 +1,59 @@
+import { createServer } from 'node:http'
+
+import type { ValidateDefinition } from 'permix'
+import { createPermix } from 'permix/react-router'
+import type { ReactRouterContext } from 'permix/react-router'
+
+type PermissionsDefinition = ValidateDefinition<{
+ user: ['read', 'write']
+}>
+
+const permix = createPermix({
+ onForbidden: () =>
+ Response.json(
+ { error: 'You do not have permission to access this resource' },
+ { status: 403 }
+ ),
+})
+
+function createContext(): ReactRouterContext {
+ const store = new Map