# Elysia

Source: https://permdock.com/docs/adapters/elysia

permdock/elysia derives a request-scoped PermDock into Elysia's context and provides protect as a typed beforeHandle hook that keeps validated route types.

## Purpose [#purpose]

Elysia is Fetch-native and Bun-first, so the [server kernel](/docs/adapters/server-kernel) runs unchanged. `permdock/elysia` is a plugin that uses `derive` to attach `permdock` to the context and a `protect` hook for `beforeHandle`. The adapter is typed against Elysia's inferred route context so `params` and `body` validated with TypeBox or Zod keep their types inside `loadData` (permix's Elysia guard was typed against the raw `Context`, breaking with validated routes).

## API [#api]

```ts
import { Elysia } from "elysia";
import { createPermDock } from "permdock/elysia";
import { policy } from "./policy";
import { permissions } from "./permissions";

export const { permdock, protect } = createPermDock(policy, {
  subject: ({ store }) => store.user ?? null,
});

const app = new Elysia()
  .use(permdock()) // ctx.permdock is the request-scoped PermDock
  .delete(
    "/posts/:id",
    async ({ permdockData: post, set }) => {
      await deletePost(post);
      set.status = 204;
    },
    {
      beforeHandle: protect(permissions.post.delete, ({ params }) =>
        loadPost(params.id),
      ),
      detail: openapi.security(permissions.post.delete), // @elysiajs/openapi
    },
  );
```

| Export | Role |
| --- | --- |
| `permdock()` | Plugin. `derive` calls `subject(ctx)` once per request and adds `permdock` to the context, typed with the policy's roles, plans and permissions in every later handler. Uses `as: 'global'` scoping so routes registered after `.use` see it. Each `createPermDock` result gets its own plugin `seed`, so two factories in one app are not deduplicated into one, while `.use(permdock())` in a child app and its parent is. |
| `protect(permission, loadData?, options?)` | `beforeHandle` hook. The `tenant` option (`(ctx) => ctx.params?.org`) is resolved for each `protect`. Loads data (`ctx.permdockData`), validates untrusted input, decides. On denial it returns a `Response` with Problem Details, which Elysia sends without running the handler. |
| `permdockHandler()` | Plugin mounting the AuthZEN-shaped decision endpoint under a prefix. |
| `openapi` | Kernel hook contract; `openapi.security(permission)` returns the `detail` fragment for `@elysiajs/openapi` and `openapi.securitySchemes()` the document-level components. |

### Guarding a group [#guarding-a-group]

Elysia's `guard` and `group` apply hooks to many routes at once. `protect` composes with them for collection actions, and instance actions still take a per-route loader:

```ts
app.group("/admin", (admin) =>
  admin
    .guard({ beforeHandle: protect(permissions.admin.access) })
    .get("/audit", listAudit)
    .delete("/users/:id", removeUser, {
      beforeHandle: protect(permissions.user.delete, ({ params }) =>
        loadUser(params.id),
      ),
    }),
);
```

Because hooks run in registration order, the group-level `protect` decides first and the route-level one only runs for callers that already hold `admin.access`.

### WebSockets [#websockets]

`connection(ws, options?)` opens a kernel [`Connection`](/docs/adapters/server-kernel#connections) for an Elysia `.ws` socket (typed `ElysiaSocket`), memoised per socket, and closes the socket with `1008` and the Problem Details `type` when the connection aborts. Pass `revocations` to `createPermDock`.

```ts
app.ws("/ws", {
  async open(ws) {
    await connection(ws, { permission: permissions.project.read });
  },
  async message(ws, message) {
    const conn = await connection(ws);
    const decision = conn.check(permissions.post.update, message);
    if (decision.outcome !== "granted") ws.send({ denied: decision.denials });
  },
  async close(ws) {
    (await connection(ws)).close();
  },
});
```

## Request lifecycle [#request-lifecycle]

`permdock/elysia` follows the [shared adapter contract](/docs/adapters) through the [server kernel](/docs/adapters/server-kernel). What differs on Elysia:

* The plugin uses `derive`, not `resolve`, so the subject is resolved once per request before route validation; a subject that depends on a header reads it in the `subject` resolver.
* `protect` runs in `beforeHandle` after Elysia's own schema validation and puts the loaded, validated row on `permdockData`, so the handler does not run the loader again.
* The plugin's global `onError` maps PermDock errors thrown by `permdock.assert(...)` to Problem Details.
* `protect` without `permdock()` fails at startup with a message naming both.
* The [decision event](/docs/concepts/wire-formats) carries the permission, resource, subject, tenant and outcome, not the HTTP method or route; a sink that needs them reads its own request context.

The peer range is `elysia >=1`. Denials are the kernel's Problem Details responses.

## Example app [#example-app]

`apps/examples/elysia`: Bun runtime, posts API with the plugin and `protect`, `@elysiajs/openapi` emitting `securitySchemes`, the decision endpoint, and `bun test` cases using `app.handle(new Request(...))` asserting exact denial bodies.

The example serves `app.fetch` with `@hono/node-server` under Node. `tests/integration/src/http/elysia.test.ts` runs the [`testHttpAdapter`](/docs/adapters/testing) scenarios through `app.handle`, including a prefixed sub-app with its own `.use(permdock())`. Return `new Response(null, { status: 204 })` for an empty response: `set.status = 204` with an `undefined` return makes Elysia build an invalid 204 with a body.

## Related standards [#related-standards]

* [Problem Details](/docs/standards/problem-details), [OpenAPI 3.2](/docs/standards/openapi).
