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
7 changes: 5 additions & 2 deletions _artifacts/domain_map.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ domains:
slug: server
description: >
Per-request setupMiddleware and checkMiddleware for Express, Hono, Fastify,
tRPC, oRPC, Node, and Elysia.
tRPC, oRPC, Node, Elysia, and Astro.

- name: 'SSR and hydration'
slug: ssr
Expand Down Expand Up @@ -78,7 +78,7 @@ skills:
~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
checkMiddleware for Express, Hono, Fastify, tRPC, oRPC, Node, Elysia.
checkMiddleware for Express, Hono, Fastify, tRPC, oRPC, Node, Elysia, Astro.
Single skill with a thin SKILL.md router and three reference files
loaded on demand.
type: core
Expand All @@ -98,6 +98,7 @@ skills:
- orpc
- node
- elysia
- astro
covers:
- check
- isReady
Expand Down Expand Up @@ -144,6 +145,7 @@ skills:
- 'letstri/permix:docs/content/docs/integrations/orpc.mdx'
- 'letstri/permix:docs/content/docs/integrations/node.mdx'
- 'letstri/permix:docs/content/docs/integrations/server.mdx'
- 'letstri/permix:docs/content/docs/integrations/astro.mdx'
- 'letstri/permix:docs/content/docs/integrations/elysia.mdx'
- 'letstri/permix:permix/src/core/check.ts'

Expand All @@ -153,6 +155,7 @@ coverage:
- examples/*
- next
- tanstack-start
- astro

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 @@ -40,7 +40,7 @@ skills:
isReady/isReadyAsync; PermixProvider/usePermix/createComponents and SSR
dehydrate/hydrate for React, Vue, Solid, Svelte, Next.js, TanStack
Start; setupMiddleware/checkMiddleware for Express, Hono, Fastify,
tRPC, oRPC, Node, Elysia. Thin router with references loaded on demand.
tRPC, oRPC, Node, Elysia, Astro. Thin router with references loaded on demand.
requires:
- permix-getting-started
subsystems:
Expand All @@ -57,6 +57,7 @@ skills:
- orpc
- node
- elysia
- astro
references:
- 'references/check.md'
- 'references/frontend.md'
Expand All @@ -79,6 +80,7 @@ skills:
- 'letstri/permix:docs/content/docs/integrations/orpc.mdx'
- 'letstri/permix:docs/content/docs/integrations/node.mdx'
- 'letstri/permix:docs/content/docs/integrations/server.mdx'
- 'letstri/permix:docs/content/docs/integrations/astro.mdx'
- 'letstri/permix:docs/content/docs/integrations/elysia.mdx'
- 'letstri/permix:permix/src/core/check.ts'

Expand All @@ -88,3 +90,4 @@ coverage:
- examples/*
- next
- tanstack-start
- astro
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, astro 1.19 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 @@ -42,6 +42,7 @@ Built with `pnpm run build` in the `permix` package (`tsdown` for all entries ex
| `permix/tanstack-start` | 2.05 kB | adapter |
| `permix/node` | 0.95 kB | adapter |
| `permix/server` | 1.10 kB | adapter |
| `permix/astro` | 1.19 kB | adapter |
| `permix/express` | 0.91 kB | adapter |
| `permix/hono` | 0.88 kB | adapter |
| `permix/fastify` | 1.04 kB | adapter |
Expand Down
233 changes: 233 additions & 0 deletions docs/content/docs/integrations/astro.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
---
title: Astro
description: Learn how to use Permix with Astro
---

## Overview

Permix provides middleware for [Astro](https://astro.build/) through `permix/astro`. It stores a per-request instance on `context.locals`, so Astro middleware, endpoints, and server-rendered pages share the same rules.

UI checks in islands reuse the existing [`permix/react`](/docs/integrations/react), [`permix/vue`](/docs/integrations/vue), [`permix/solid`](/docs/integrations/solid), or [`permix/svelte`](/docs/integrations/svelte) integrations. Dehydrate on the server and hydrate in the island with `PermixHydrate`.

<Callout>
Before getting started with the Astro integration, make sure you've completed
the initial setup steps in the [Quick Start](/docs/quick-start) guide.
</Callout>

<Steps>

<Step>

## Setup

Create a Permix instance and run `setupMiddleware` from `src/middleware.ts`:

```ts title="src/lib/permix.ts"
import { createPermix } from 'permix/astro'

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 },
]
}>()
```

```ts title="src/middleware.ts"
import { defineMiddleware } from 'astro:middleware'
import { permix } from './lib/permix'

export const onRequest = defineMiddleware(
permix.setupMiddleware(({ request }) => {
const isAdmin = request.headers.get('x-user-role') === 'admin'

return {
post: {
create: true,
read: true,
update: isAdmin,
delete: isAdmin,
},
}
})
)
```

`setupMiddleware` is compatible with [`sequence`](https://docs.astro.build/en/guides/middleware/#chaining-middleware) if you already have other middleware.

The instance lives on `locals`, not on the `Request` object.

</Step>

<Step>

## Checking permissions

Use `checkMiddleware` to guard an endpoint, or `getOrThrow` inside the handler for entity-aware checks:

```ts title="src/pages/api/posts.ts"
import type { APIRoute } from 'astro'
import { permix } from '../../lib/permix'

export const POST: APIRoute = (context) =>
permix.checkMiddleware('post.create')(context, async () => {
return Response.json({ ok: true })
})
```

```ts title="src/pages/api/posts/[id].ts"
import type { APIRoute } from 'astro'
import { permix } from '../../../lib/permix'

export const PATCH: APIRoute = async (context) => {
const post = await getPost(context.params.id)

if (!permix.getOrThrow(context).check('post.update', post)) {
return Response.json({ error: 'Forbidden' }, { status: 403 })
}

return Response.json({ ok: true })
}
```

`checkMiddleware` accepts the same arguments as the core `check`: a path, a path plus entity data, or a callback.

Denied requests default to `403` with `{ error: 'Forbidden' }`. Customize with `onForbidden` in `createPermix` options.

</Step>

<Step>

## Pages and islands

In a `.astro` page, read the instance from `Astro.locals` (or pass `Astro` as the context — `get` accepts either):

```astro title="src/pages/posts/[id].astro"
---
import { permix } from '../../lib/permix'

const post = await getPost(Astro.params.id)

if (!permix.getOrThrow(Astro).check('post.read', post)) {
return Astro.redirect('/404')
}

const state = permix.getOrThrow(Astro).dehydrate()
---

<EditButton client:load post={post} state={state} />
```

Hydrate the island with the matching UI adapter:

```tsx title="src/components/EditButton.tsx"
import { createPermix } from 'permix'
import { PermixHydrate, PermixProvider, usePermix } from 'permix/react'

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 EditButton({
post,
state,
}: {
post: { id: string; authorId: string }
state: ReturnType<typeof permix.dehydrate>
}) {
return (
<PermixProvider permix={permix}>
<PermixHydrate state={state}>
<Inner post={post} />
</PermixHydrate>
</PermixProvider>
)
}

function Inner({ post }: { post: { id: string; authorId: string } }) {
const { check } = usePermix(permix)
if (!check('post.update', post)) return null
return <button>Edit post</button>
}
```

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

</Step>

<Step>

## Templates

```ts title="src/lib/permix.ts"
import { createPermix } from 'permix/astro'

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/middleware.ts"
import { defineMiddleware } from 'astro:middleware'
import { adminTemplate, guestTemplate, permix } from './lib/permix'

export const onRequest = defineMiddleware(
permix.setupMiddleware(({ request }) => {
const isAdmin = request.headers.get('x-user-role') === 'admin'
return isAdmin ? adminTemplate() : guestTemplate()
})
)
```

</Step>

<Step>

## Example

You can find a runnable example of the Astro integration [here](https://github.com/letstri/permix/tree/main/examples/astro).

</Step>

</Steps>

## API

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

Returns an object with the following methods:

| Method | Description |
| --- | --- |
| `setupMiddleware(rules \| callback)` | Astro middleware that creates a per-request instance on `locals`. |
| `checkMiddleware(...args)` | Astro middleware that allows or returns the `onForbidden` response. |
| `get(context \| locals)` | Return the instance, or `null`. |
| `getOrThrow(context \| locals)` | Return the instance, or throw `PermixNotFoundError`. |
| `getRules(context \| locals)` | Return the current rules, or `null`. |
| `template(rules)` | Create a reusable rule set. Same as the core [`template`](/docs/guide/template). |
| `contextKey(key)` | Store this factory under a custom `locals` key. |
1 change: 1 addition & 0 deletions docs/content/docs/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"integrations/svelte",
"integrations/node",
"integrations/server",
"integrations/astro",
"integrations/trpc",
"integrations/orpc",
"integrations/express",
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 @@ -341,6 +341,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/server` | Framework-agnostic fetch middleware |
| `permix/astro` | Astro middleware and `locals` |
| `permix/svelte` | Svelte 5 runes |
| `permix/drizzle` | Rules from Drizzle v1 schema |
| `permix/drizzle/legacy` | Drizzle v0 (`>=0.30 <1`) |
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 @@ -150,6 +150,10 @@ Continuing from the quick start, you can now explore how Permix integrates with
Integration with native Request and Response handlers.
</Card>

<Card href="/docs/integrations/astro" title="Astro">
Integration with Astro middleware and locals.
</Card>

<Card href="/docs/integrations/hono" title="Hono">
Integration with Hono via middleware.
</Card>
Expand Down
Loading
Loading