https://github.com/vllnt/convex-permissions
Typed authorization for Convex — define roles and policies, enforce access with type-safe guards (require/check/can).
https://github.com/vllnt/convex-permissions
abac authorization authz convex convex-component permissions rbac typescript
Last synced: 23 days ago
JSON representation
Typed authorization for Convex — define roles and policies, enforce access with type-safe guards (require/check/can).
- Host: GitHub
- URL: https://github.com/vllnt/convex-permissions
- Owner: vllnt
- License: mit
- Created: 2026-06-12T12:52:11.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-06-14T15:47:09.000Z (about 1 month ago)
- Last Synced: 2026-06-14T17:23:37.191Z (about 1 month ago)
- Topics: abac, authorization, authz, convex, convex-component, permissions, rbac, typescript
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/@vllnt/convex-permissions
- Size: 99.6 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- Funding: .github/FUNDING.yml
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Security: SECURITY.md
- Roadmap: ROADMAP.md
- Agents: AGENTS.md
Awesome Lists containing this project
README
[](https://www.convex.dev/components)
[](https://www.npmjs.com/package/@vllnt/convex-permissions)
[](https://github.com/vllnt/convex-permissions/actions/workflows/ci.yml)
[](./LICENSE)
# @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` 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` 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(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).