# Permissions

Role-based access control as a Bijection component — typed, sandboxed, runtime-editable roles and grants keyed by opaque refs

- Package: @bijectionhq/permissions
- Category: Authentication
- Status: Reference
- License: MIT
- Bijection source (repository access required): https://github.com/bijectionhq/permissions/tree/2f4fa8f5f4946a6158715221fcb4b661af28644c
- Upstream: https://github.com/vllnt/convex-permissions/tree/a2db4faabba934ee26eea53f48f0784b96cb95f3
- Documentation: https://docs.bijection.com/components/using
- Directory: https://bijection.com/components/permissions

## Get the source

Bijection repository access is required. Request access at https://bijection.com/contact.

```sh
git clone https://github.com/bijectionhq/permissions.git
cd permissions
git checkout 2f4fa8f5f4946a6158715221fcb4b661af28644c
```

## Before using this component

Read the package README at the recorded revision for setup, dependencies, and API examples. Upstream documentation describes the original package and may differ from the Bijection adaptation. This is a source candidate for evaluation. Catalog inclusion does not establish npm availability or runtime qualification. Check compatibility with your SDK and deployment before adoption. External providers run their own compute.

## Upstream package documentation

Recorded 2026-10-23. This describes @vllnt/convex-permissions; check the Bijection adaptation separately.

@vllnt/convex-permissions stores role-based access control rules in isolated Convex tables. Define your own role and action vocabulary, assign roles to subject references, and evaluate access in the backend.

Define roles and grants, assign or revoke roles, and use check or require before a protected operation. Stored roles can change through mutations without a code deployment. Optional scope references partition assignments; unmatched actions are denied.

The host authenticates the caller, derives trusted subject and scope references, and protects role-management functions. Global assignments can apply across scopes, so grant them deliberately. The component is authorization storage and evaluation, not a login provider or an automatic guard on every endpoint.

Maintained by [bntvllnt](https://bntvllnt.com) at [VLLNT](https://vllnt.com). Find the author on [GitHub](https://github.com/bntvllnt) and [X](https://x.com/bntvllnt).

### Use cases

- SaaS workspaces: distinguish owners, administrators, and members by workspace.
- Education tools: give instructors and learners different actions.
- Communities and games: authorize moderation or team-management operations.
- Internal tools and agents: limit service identities to explicitly granted actions.

### How it works

Define roles and grants, assign or revoke roles, and use check or require before a protected operation. Stored roles can change through mutations without a code deployment. Optional scope references partition assignments; unmatched actions are denied.

The host authenticates the caller, derives trusted subject and scope references, and protects role-management functions. Global assignments can apply across scopes, so grant them deliberately. The component is authorization storage and evaluation, not a login provider or an automatic guard on every endpoint.

### Upstream README

# @vllnt/convex-permissions

Role-based access control as a Convex component — typed, sandboxed,
runtime-editable roles and grants keyed by opaque refs.

```ts
// Define a role, assign it, enforce it — all from host functions.
await permissions.defineRole(ctx, { name: "editor", grants: ["doc.read", "doc.edit"] });
await permissions.assign(ctx, { subjectRef: userId, role: "editor" });
await permissions.require(ctx, { subjectRef: userId, action: "doc.edit" }); // throws on deny
```

## Features

- **Stored, runtime-editable RBAC** — roles, grants, and assignments live in sandboxed tables; change them with a mutation, no redeploy.
- **Typed end to end** — `Permissions<TRole, TAction>` is generic over your role and action unions.
- **Wildcard grants** — `"doc.*"` grants every `doc.` action; `"*"` grants everything.
- **Scoped / multi-tenant** — assign within an opaque `scopeRef`; unscoped assignments apply globally.
- **Agnostic by construction** — opaque `subjectRef` / `scopeRef`, no auth library, no domain model, no vendor.
- **`require` throws** — `ConvexError<PermissionDenied>` the host maps to a 403.
- **Default-deny** — unknown subjects and unmatched actions are denied.
- **Runs anywhere** — identical on Convex Cloud and self-hosted `convex-backend`.

## Installation

```bash
npm install @vllnt/convex-permissions
```

Peer dependency: `convex@^1.36.1`.

## Usage

```ts
// convex/convex.config.ts
import { defineApp } from "convex/server";
import permissions from "@vllnt/convex-permissions/convex.config";

const app = defineApp();
app.use(permissions);
export default app;
```

```ts
// convex/permissions.ts — instantiate the typed client, then define / assign / check.
import { components } from "./_generated/api";
import { Permissions } from "@vllnt/convex-permissions";

type Role = "admin" | "editor" | "viewer";
type Action = "doc.read" | "doc.edit" | "doc.delete";

export const permissions = new Permissions<Role, Action>(components.permissions);

// from any host mutation/query (gate management behind your own admin auth):
await permissions.defineRole(ctx, { name: "editor", grants: ["doc.read", "doc.edit"] });
await permissions.assign(ctx, { subjectRef: userId, role: "editor", scopeRef: orgId });
const allowed = await permissions.check(ctx, { subjectRef: userId, action: "doc.edit" });
await permissions.require(ctx, { subjectRef: userId, action: "doc.edit" }); // throws on deny
```

## API Reference

| Method | Kind | Result |
|--------|------|--------|
| `defineRole(ctx, { name, grants, description? })` | mutation | Upsert a role (runtime-editable) |
| `removeRole(ctx, name)` | mutation | Delete a role by name |
| `assign(ctx, { subjectRef, role, scopeRef? })` | mutation | Grant a role to a subject |
| `revoke(ctx, { subjectRef, role, scopeRef? })` | mutation | Remove a role from a subject |
| `check(ctx, { subjectRef, action, scopeRef? })` | query | Boolean permission check (default-deny) |
| `require(ctx, { subjectRef, action, scopeRef? })` | query | Enforce access — throws on deny |
| `rolesFor(ctx, { subjectRef, scopeRef? })` | query | List role names for a subject |
| `permissionsFor(ctx, { subjectRef, scopeRef? })` | query | List distinct grants for a subject |
| `listRoles(ctx)` | query | All role definitions |

Full reference: [docs/API.md](docs/API.md).

## Security

- **Host owns auth** — it authenticates the caller, resolves identity to an opaque `subjectRef`, and gates the management methods behind its own admin authorization.
- **Opaque refs only** — `subjectRef`, `scopeRef`, and action keys are arbitrary strings; tables are sandboxed (reached only via the client).
- **Default-deny** — no matching grant ⇒ denied; boundary validation rejects refs not matching `^[A-Za-z0-9_.:-]{1,128}$`.

See [docs/API.md](docs/API.md).

## Testing

```bash
pnpm test           # single run
pnpm test:coverage  # enforced 100% on covered files
```

Tests run against the real component runtime via `convex-test` (`@edge-runtime/vm`), not mocks.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md).

## Author

Built by [bntvllnt](https://github.com/bntvllnt) · [bntvllnt.com](https://bntvllnt.com) · [X @bntvllnt](https://x.com/bntvllnt)

Part of the [@vllnt](https://github.com/vllnt) Convex component fleet — [vllnt.com](https://vllnt.com)

If this is useful, [sponsor the work](https://github.com/sponsors/bntvllnt).

## License

MIT — see [LICENSE](LICENSE).