# Convex

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

The permdock/convex provider builds a PermDock subject from ctx.auth inside Convex queries, mutations and actions, and ships the snapshot to the client through a Convex query.

`permdock/convex` is a provider for Convex functions. Inside a query, mutation or action it turns `ctx.auth.getUserIdentity()` (plus any roles stored in Convex tables) into a PermDock subject and a request-scoped `PermDock`, so function bodies call `assert`, `can` and `filter` with typed permissions. A companion query returns the snapshot so the Convex React client can drive `PermDockProvider`.

## Purpose [#purpose]

Convex has authentication (`ctx.auth`) and leaves authorization to code inside each function. Permission logic therefore lives as `if` statements per function, unshared with the React client, and cannot be explained or audited. PermDock fits Convex's per-invocation model well: every function invocation is a request, so an immutable request-scoped `PermDock` per invocation is natural, and Convex's reactive queries deliver snapshot updates to the client without polling.

## API [#api]

```ts
// convex/permdock.ts
import { createPermDock } from "permdock/convex";
import { policy } from "./policy";

export const { withPermDock, snapshotQuery } = createPermDock(policy, {
  subject: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) return null;
    const user = await ctx.db
      .query("users")
      .withIndex("by_token", (q) =>
        q.eq("tokenIdentifier", identity.tokenIdentifier),
      )
      .unique();
    return user && { id: user._id, orgId: user.orgId, roles: user.roles };
  },
});

// convex/posts.ts
export const remove = mutation({
  args: { id: v.id("posts") },
  handler: withPermDock(async (ctx, { id }) => {
    const post = await ctx.db.get(id);
    ctx.permdock.assert(permissions.post.delete, post);
    await ctx.db.delete(id);
  }),
});

export const list = query({
  args: {},
  handler: withPermDock(async (ctx) =>
    ctx.permdock.filter(
      permissions.post.read,
      await ctx.db.query("posts").collect(),
    ),
  ),
});

// convex/permissions.ts
export const snapshot = snapshotQuery(); // client: useQuery(api.permissions.snapshot) → PermDockProvider snapshot
```

* `createPermDock(policy, options)` takes `subject(ctx)`, an optional `tenant` (a fixed id or `(ctx) => id`; a throw is no tenant) and an optional `actor(ctx)` (anything but an `Actor` is ignored), and returns `withPermDock`, a handler wrapper that resolves the subject from `ctx`, builds the request-scoped instance and attaches it as `ctx.permdock`; and `snapshotQuery`, a ready-made Convex query that returns `permdock.snapshot()` for the caller (optionally scoped: `snapshotQuery({ include: [permissions.post] })`).
* Handlers are wrapped one by one; there is no `customQuery` / `customMutation` builder. A closure grant marked `server-only` in the snapshot is answered by an app-defined Convex query that calls `ctx.permdock.can`.
* `subject` receives the Convex context so it can read identity and role tables; it may be sync when roles live in the identity claims.
* Works for `query`, `mutation`, `action` and `internal*` variants; in actions without database access the subject resolver must rely on identity claims.

## Request lifecycle [#request-lifecycle]

1. A Convex function is invoked with an authenticated (or anonymous) caller.
2. `withPermDock` calls `subject(ctx)`; identity comes from `ctx.auth`, roles from claims or a table lookup.
3. `createPermDock(policy, subject)` produces `ctx.permdock`, frozen for this invocation.
4. The handler calls `assert` before writes and `filter` / `can` for reads; conditions evaluate against documents already loaded from `ctx.db`.
5. `on('decision')` events are the app's to persist: a mutation can write them to an audit table through `ctx.db.insert`, while a query cannot write and logs instead.
6. Client: `useQuery(api.permissions.snapshot)` re-runs reactively when the user's roles document changes, so `PermDockProvider` receives the new snapshot without `invalidate()`.

## What it validates [#what-it-validates]

* Identity comes only from `ctx.auth`; function arguments never influence the subject.
* Arguments are validated by Convex validators (`v.*`) before the handler runs; documents loaded from `ctx.db` are trusted server data and are not re-validated (`validate: 'boundary'` treats them as trusted).
* Roles read from a table must match role names in `definePolicy`; unknown names are dropped with a warning.
* `filter` on collected documents is in-process; Convex has no `where` compiler target, so large tables should be indexed by the condition fields (for example `by_author`) and queried with `withIndex` before `filter` is applied as the final gate.

## How denials surface [#how-denials-surface]

* `assert` throws `PermDockDeniedError` or `PermDockApprovalRequiredError`; the wrapper converts them into a Convex `ConvexError` whose `data` is the RFC 9457 Problem Details object (`type`, `title`, `permission`, `denials`, `alternatives`), so the client receives structured, serialisable denial data. The class carries the `Symbol.for('ConvexError')` marker Convex's runtime looks for, so `data` reaches the client's `ConvexError` without `permdock/convex` importing `convex/values`.
* `filter` and `can` never throw; a query returns the permitted subset.
* Anonymous callers yield the anonymous subject; only anonymous grants apply.
* The snapshot query itself never fails on denial; it returns whatever grants the subject holds, including none.

## Example app [#example-app]

`apps/examples/convex`: a Vite React app with Convex Auth, a `users` table with roles, `withPermDock` on `posts` functions, `snapshotQuery` feeding `PermDockProvider`, `Protected` in the UI, and a test that a role change in the `users` document flips `usePermission` without a reload.

## Related standards [#related-standards]

* [Subject](/docs/concepts/subject): principal and roles.
* [Snapshots](/docs/concepts/snapshots): what the snapshot query returns.
* [Errors](/docs/concepts/errors): `PermDockDeniedError` mapped to `ConvexError`.
* [Landscape](/docs/research/landscape): where Convex fits among providers.
