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 Nuxt 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, Nuxt; 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
- nuxt
- 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/nuxt'
- '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/nuxt.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
- nuxt

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, Nuxt; 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
- nuxt
- 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/nuxt.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
- nuxt
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, nuxt 0.99 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 @@ -39,6 +39,7 @@ Built with `pnpm run build` in the `permix` package (`tsdown` for all entries ex
| `permix/solid` | 0.82 kB | adapter |
| `permix/svelte` | ~2.17 kB | adapter (`dist/svelte/`, `svelte-package` build) |
| `permix/next` | 1.00 kB | adapter |
| `permix/nuxt` | 0.99 kB | adapter |
| `permix/tanstack-start` | 2.05 kB | adapter |
| `permix/node` | 0.95 kB | adapter |
| `permix/server` | 1.10 kB | adapter |
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/integrations/next.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ 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, Remix, or a custom React SSR setup, you can still use `permix/react` on the client. For Nuxt, use [`permix/nuxt`](/docs/integrations/nuxt) on the server and `permix/vue` 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
Expand Down
258 changes: 258 additions & 0 deletions docs/content/docs/integrations/nuxt.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
---
title: Nuxt
description: Learn how to use Permix with Nuxt
---

## Overview

Permix provides middleware for [Nuxt](https://nuxt.com/) server routes through `permix/nuxt`. It follows the same shape as the other server integrations: `setupMiddleware` creates a **per-request** Permix instance on the Nitro `event.context`, and `checkMiddleware` guards a route. Concurrent requests never share state.

The client side reuses the Vue integration (`permix/vue`): the server dehydrates its state and the client hydrates it with `PermixProvider` + `PermixHydrate`.

<Callout>
Before getting started with the Nuxt 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) and the [Vue
integration](/docs/integrations/vue) helps too.
</Callout>

<Steps>

<Step>

## Define your permissions

Create the factory once in a server utility so every server file can import it:

```ts title="server/utils/permix.ts"
import { createPermix } from 'permix/nuxt'

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 factory holds no permission state at module scope — every request gets its own instance on `event.context`.

</Step>

<Step>

## Setup per request

Register `setupMiddleware` as a Nitro server middleware so it runs before every route (including SSR page renders). The callback receives the h3 `event`, so you can derive rules from the session, headers, or a database lookup:

```ts title="server/middleware/permix.ts"
export default defineEventHandler(
permix.setupMiddleware(async ({ event }) => {
const user = event.context.user

return {
post: {
create: !!user,
read: true,
update: (post) => post?.authorId === user?.id,
delete: user?.role === 'admin',
},
}
})
)
```

<Callout>
Nitro runs `server/middleware` files in alphabetical order. Name the file so
it runs after the middleware that populates `event.context.user`.
</Callout>

</Step>

<Step>

## Check on the server

Guard a route with `checkMiddleware` in the handler's `onRequest` hooks:

```ts title="server/api/posts/index.post.ts"
export default defineEventHandler({
onRequest: [permix.checkMiddleware('post.create')],
handler: async (event) => {
// create the post...
return { ok: true }
},
})
```

Or check manually anywhere the request `event` is available, including Vue pages during SSR via `useRequestEvent()`:

```ts title="server/api/posts/[id].put.ts"
export default defineEventHandler(async (event) => {
const post = await getPost(getRouterParam(event, 'id')!)

if (!permix.getOrThrow(event).check('post.update', post)) {
throw createError({ statusCode: 403, statusMessage: 'Forbidden' })
}

// update the post...
return { ok: true }
})
```

`checkMiddleware` accepts the same arguments as core `check()`: a dot path with optional data, `'~all'` / `'~any'`, or a callback such as `(c) => c('post.read') && c('post.update')`.

</Step>

<Step>

## Send permissions to the client

`permix/nuxt` is server-only. Create a client singleton with core `permix`, dehydrate the request's rules in a server plugin, and hydrate them with `PermixProvider` + `PermixHydrate` from `permix/vue`:

```ts title="app/utils/permix.ts"
import { createPermix } from 'permix'

// Same definition as on the server.
export 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 } },
]
}>()
```

```ts title="app/plugins/permix.server.ts"
import { permix } from '~~/server/utils/permix'

export default defineNuxtPlugin(() => {
const event = useRequestEvent()
useState('permix', () =>
event ? permix.getOrThrow(event).dehydrate() : null
)
})
```

```vue title="app/app.vue"
<script setup lang="ts">
import { PermixHydrate, PermixProvider } from 'permix/vue'
import { permix } from '~/utils/permix'

const state = useState('permix')
</script>

<template>
<PermixProvider :permix="permix">
<PermixHydrate v-if="state" :state="state">
<NuxtPage />
</PermixHydrate>
</PermixProvider>
</template>
```

<Callout type="warn">
`hydrate()` restores the boolean state but does not flip `isReady` —
function-based rules are lost during serialization. Call `permix.setup(...)`
on the client too if you need function rules or `isReady` there. See the
[Hydration guide](/docs/guide/hydration).
</Callout>

From any component, use `usePermix` from `permix/vue`:

```vue title="app/components/EditButton.vue"
<script setup lang="ts">
import { usePermix } from 'permix/vue'
import { permix } from '~/utils/permix'

defineProps<{ post: { id: string; authorId: string } }>()

const { check } = usePermix(permix)
</script>

<template>
<button v-if="check('post.update', post)">Edit post</button>
</template>
```

</Step>

<Step>

## Using Templates

`template()` works the same as in the core API:

```ts title="server/utils/permix.ts"
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="server/middleware/permix.ts"
export default defineEventHandler(
permix.setupMiddleware(({ event }) =>
event.context.user?.role === 'admin' ? adminTemplate() : guestTemplate()
)
)
```

</Step>

<Step>

## Example

You can find a runnable example of the Nuxt integration [here](https://github.com/letstri/permix/tree/main/examples/nuxt). It uses a plain h3 app, which is what Nitro runs under the hood.

</Step>

</Steps>

## Custom Error Handling

By default a denied `checkMiddleware` throws an error with `statusCode: 403`, which h3 turns into a 403 response. Provide `onForbidden` to customise it:

```ts
const permix = createPermix<Definition>({
onForbidden: ({ path }) => {
throw createError({
statusCode: 403,
statusMessage: `You don't have permission for ${path}`,
})
},
})
```

The `onForbidden` handler receives:

- `event`: the h3 event
- `path`: the permission path that was checked (or `null` for callback checks)
- `data`: optional entity data passed to the check

## API

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

| Method | Description |
| --- | --- |
| `setupMiddleware(rules \| callback)` | h3 handler that creates a per-request instance on `event.context`. Wrap with `defineEventHandler`. |
| `checkMiddleware(...args)` | h3 handler that throws via `onForbidden` when denied. Use in `onRequest`. |
| `get(event)` | Return the instance, or `null`. |
| `getOrThrow(event)` | Return the instance, or throw `PermixNotFoundError`. |
| `getRules(event)` | Return the current rules, or `null`. |
| `template(rules)` | Create a reusable rule set. |
| `hook` / `hookOnce` | Factory-level hooks, e.g. `permix.hook('check', ...)` across all requests. |
| `contextKey(key)` | Use a custom `event.context` key instead of the default `Symbol('permix')`. |
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/nuxt",
"integrations/vue",
"integrations/solid",
"integrations/svelte",
Expand Down
1 change: 1 addition & 0 deletions docs/content/docs/migration-v3-to-v4.mdx
Original file line number Diff line number Diff line change
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/nuxt` | Nuxt / Nitro, request-scoped instance |
| `permix/server` | Framework-agnostic fetch middleware |
| `permix/svelte` | Svelte 5 runes |
| `permix/drizzle` | Rules from Drizzle v1 schema |
Expand Down
4 changes: 4 additions & 0 deletions docs/content/docs/quick-start.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ Continuing from the quick start, you can now explore how Permix integrates with
Integration with TanStack Start.
</Card>

<Card href="/docs/integrations/nuxt" title="Nuxt">
Integration with Nuxt via Nitro request isolation.
</Card>

<Card href="/docs/integrations/solid" title="Solid">
Integration with Solid via provider and hook.
</Card>
Expand Down
Loading