This is the full developer documentation for VeguiPerms
# Permission to build.
> VeguiPerms brings predictable authorization to TypeScript. Model access with simple rules, let inheritance do the heavy lifting, and share one permission engine from server to UI.
**THE MATCHING ENGINEPause animation
01 / CHECK A PERMISSION
ability.can(
`workspaces.1.read`
)
One subject. Three grants.\
One deterministic answer.
02 / FIND THE WINNING GRANT
`workspaces.1.read`DIRECT · EXACT
DENY
`workspaces.1.*`DIRECT · WILDCARD
ALLOW
`workspaces.*`GROUP · DEVELOPERS
ALLOW
DENYExact direct grant wins over broader wildcards.
01 / 03
Illustrative checks for the same subject: direct grants take priority over inherited grants.
SMALL RULES. CLEAR DECISIONS.
## Access control that fits your application.
Users, services and groups. Explicit grants and inherited permissions. A predictable answer to one simple question: can they?
Express more with less
Dot-separated permissions and wildcards turn a collection of special cases into a model you can read. [Explore the concepts →](/concepts/permissions/)
Inherit. Override. Resolve.
Share access through groups and default parents. Resolve conflicts with deterministic precedence. [Understand inheritance →](/concepts/inheritance/)
Your data, your adapter
Start in memory. Persist with SQLite, MySQL, Postgres or MongoDB. The permission engine stays the same. [Choose an adapter →](/adapters/overview/)
One model across your stack
Authorize on the server. Use resolved snapshots for UI decisions with the same matching semantics. [Find your integration →](/integrations/client/)
START SMALL. BUILD FROM HERE.
## Your first permission check is a few lines away.
`bun add vperms`
[Getting Started ↗](/getting-started/)[Service guide ↗](/guides/service/)[API reference ↗](/reference/core/)
# Drizzle adapters
> SQLite, MySQL and Postgres via Drizzle ORM.
`@vperms/drizzle-adapter` provides one entry point per SQL dialect. The dialect comes from the import you use, and each ships its own migrations. All three extend [`VeguiPermsSqlAdapter`](/adapters/sql/) and require an already-created Drizzle instance.
## SQLite
[Section titled “SQLite”](#sqlite)
```ts
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
import {
sqliteSchema,
VeguiPermsSqliteAdapter,
} from "@vperms/drizzle-adapter/sqlite";
const adapter = new VeguiPermsSqliteAdapter({
db: drizzle(new Database(":memory:"), { schema: sqliteSchema }),
});
await adapter.migrate();
```
## MySQL
[Section titled “MySQL”](#mysql)
```ts
import { drizzle } from "drizzle-orm/mysql2";
import {
mysqlSchema,
VeguiPermsMysqlAdapter,
} from "@vperms/drizzle-adapter/mysql";
const adapter = new VeguiPermsMysqlAdapter({
db: drizzle(pool, { schema: mysqlSchema, mode: "default" }),
});
await adapter.migrate();
```
## Postgres
[Section titled “Postgres”](#postgres)
```ts
import { drizzle } from "drizzle-orm/node-postgres";
import {
postgresSchema,
VeguiPermsPostgresAdapter,
} from "@vperms/drizzle-adapter/postgres";
const adapter = new VeguiPermsPostgresAdapter({
db: drizzle(pool, { schema: postgresSchema }),
});
await adapter.migrate();
```
## Options
[Section titled “Options”](#options)
Each `VeguiPerms*Adapter` takes the Drizzle `db` instance plus optional `migrationsFolder`:
```ts
interface DriverOptions {
/** Folder with the bundled drizzle-kit migrations. */
migrationsFolder?: string;
}
```
Each dialect resolves its own bundled `migrations/` folder by default; override it to apply migrations from a custom location. `migrate()` is **explicit, idempotent** and never called automatically.
## Schema
[Section titled “Schema”](#schema)
Every entry point exports its Drizzle schema and tables, so you can include them in your own schema or introspection:
```ts
import { vpermsGrants, vpermsSubjects } from "@vperms/drizzle-adapter/sqlite";
```
The two tables are:
* `vperms_subjects` — primary key `(workspace_id, id)`, with `type` and `parents`.
* `vperms_grants` — primary key `(workspace_id, subject_id, permission)`, with `value`.
The root entry `@vperms/drizzle-adapter` re-exports the SQL base adapter and its types:
```ts
export { VeguiPermsSqlAdapter } from "@vperms/drizzle-adapter";
export type { GrantRecord, SqlAdapterDriver, SubjectRecord } from "@vperms/drizzle-adapter";
```
## Migrations
[Section titled “Migrations”](#migrations)
Regenerate the bundled migrations for all dialects from the repository with:
```bash
bun run db:generate
```
# In-memory adapter
> The reference adapter for tests and examples.
`VeguiPermsMemoryAdapter` is the reference implementation, exported by `@vperms/core` and re-exported by `vperms`. It keeps subjects and grants in `Map`s scoped by workspace.
```ts
import { SubjectType, VeguiPermsMemoryAdapter, VeguiPermsService } from "vperms";
const vperms = new VeguiPermsService({
adapter: new VeguiPermsMemoryAdapter(),
});
```
It has no options, no migration and no I/O — perfect for tests, examples and local development.
Caution
The in-memory adapter is **not persistent** and its state lives in a single process. Use a [database adapter](/adapters/overview/#official-adapters) in production.
It is used by the repository’s own tests and by [`examples/basic`](https://github.com/VeguiDev/VeguiPerms/tree/master/examples/basic).
# MongoDB adapter
> Document persistence for subjects and grants.
`@vperms/mongodb-adapter` persists subjects and grants in MongoDB. It accepts an already-connected `Db` instance and never opens or closes connections.
```ts
import { MongoClient } from "mongodb";
import { VeguiPermsMongoDBAdapter } from "@vperms/mongodb-adapter";
const adapter = new VeguiPermsMongoDBAdapter({
db: new MongoClient(url).db("vperms"),
});
await adapter.migrate();
```
## Options
[Section titled “Options”](#options)
```ts
interface VeguiPermsMongoDBAdapterOptions {
db: Db;
subjectsCollection?: string; // default "vperms_subjects"
grantsCollection?: string; // default "vperms_grants"
}
```
## Migration
[Section titled “Migration”](#migration)
`migrate()` is idempotent and must be called explicitly during setup. It:
* creates the subjects and grants collections when missing, and
* creates a unique index on `(workspaceId, id)` for subjects and on `(workspaceId, subjectId, permission)` for grants.
## Documents
[Section titled “Documents”](#documents)
```ts
type SubjectDocument = {
workspaceId: string;
id: string;
type: SubjectType;
parents: string[];
};
type GrantDocument = {
workspaceId: string;
subjectId: string;
permission: string;
value: boolean;
};
```
Collection names are exported as `SUBJECTS_COLLECTION` and `GRANTS_COLLECTION`, and the document types as `SubjectDocument` and `GrantDocument`.
```ts
import {
GRANTS_COLLECTION,
SUBJECTS_COLLECTION,
} from "@vperms/mongodb-adapter";
SUBJECTS_COLLECTION; // "vperms_subjects"
GRANTS_COLLECTION; // "vperms_grants"
```
# Overview
> How adapters persist subjects and grants.
An **adapter** is the only place VeguiPerms touches persistence. It stores and retrieves subjects and grants; it performs no validation, resolution, inheritance or evaluation. That guarantees identical authorization behavior across every backend.
Every adapter implements the same six-method contract from `@vperms/core` — see [Writing a custom adapter](/guides/custom-adapter/).
## Design rules
[Section titled “Design rules”](#design-rules)
* **Dependency injection.** An adapter receives an **already-created** database client (or Drizzle instance). It never opens or closes connections.
* **Explicit migration.** Database adapters expose `migrate()`, which the application calls once at startup. Nothing touches the database implicitly.
* **Upserts.** `grantPermission` replaces the value for an existing `(workspaceId, subjectId, permission)`; `saveSubject` replaces the record.
* **Plain data.** Subjects and grants are JSON-safe objects.
```ts
const adapter = new VeguiPermsSqliteAdapter({ db });
await adapter.migrate();
const vperms = new VeguiPermsService({ adapter });
```
## Official adapters
[Section titled “Official adapters”](#official-adapters)
| Adapter | Backend | Tier |
| ---------------------------------- | ----------------------- | --------- |
| `VeguiPermsMemoryAdapter` | In-process memory | Reference |
| `@vperms/drizzle-adapter/sqlite` | SQLite (better-sqlite3) | SQL |
| `@vperms/drizzle-adapter/mysql` | MySQL | SQL |
| `@vperms/drizzle-adapter/postgres` | Postgres | SQL |
| `@vperms/mongodb-adapter` | MongoDB | Document |
* [In-memory adapter](/adapters/memory/)
* [SQL base adapter](/adapters/sql/)
* [Drizzle adapters](/adapters/drizzle/)
* [MongoDB adapter](/adapters/mongodb/)
## Contract testing
[Section titled “Contract testing”](#contract-testing)
All official adapters run the same shared contract suite from the private `@vperms/adapter-contract` package, so behavior stays identical across backends. Unit tests include SQLite and an in-memory MongoDB; integration tests run against real databases:
```bash
bun run test # unit tests
docker compose up -d # MySQL, Postgres, MongoDB
bun run test:integration # same contract against real databases
```
Integration tests are skipped unless `RUN_INTEGRATION=1` is set (the `test:integration` script sets it).
# SQL base adapter
> Dialect-agnostic SQL adapter and driver contract.
`@vperms/sql-adapter` is a dialect-agnostic base adapter. It maps between the public `Subject`/`PermissionGrant` shapes and flat rows, leaving the concrete engine to a **driver**. The [Drizzle adapters](/adapters/drizzle/) are built on top of it, and you can build your own driver on the same contract.
## `VeguiPermsSqlAdapter`
[Section titled “VeguiPermsSqlAdapter”](#veguipermssqladapter)
```ts
import { VeguiPermsSqlAdapter } from "@vperms/sql-adapter";
const adapter = new VeguiPermsSqlAdapter(myDriver);
await adapter.migrate(); // delegates to the driver
```
It implements every `VeguiPermsAdapter` method in terms of `SqlAdapterDriver`, and adds `migrate()`.
## `SqlAdapterDriver`
[Section titled “SqlAdapterDriver”](#sqladapterdriver)
```ts
interface SqlAdapterDriver {
migrate(): Promise;
findSubject(workspaceId: string, subjectId: string): Promise;
upsertSubject(record: SubjectRecord): Promise;
deleteSubject(workspaceId: string, subjectId: string): Promise;
findGrants(workspaceId: string, subjectId: string): Promise;
upsertGrant(record: GrantRecord): Promise;
deleteGrant(workspaceId: string, subjectId: string, permission: string): Promise;
}
```
Rows are flat:
```ts
interface SubjectRecord {
workspaceId: string;
id: string;
type: SubjectType;
parents: string[]; // encode as JSON text, jsonb, ...
}
interface GrantRecord {
workspaceId: string;
subjectId: string;
permission: string;
value: boolean;
}
```
The driver deals only with rows and has **no knowledge** of validation, inheritance or matching. `upsertSubject` must replace the subject and `upsertGrant` must replace any previous value for the same `(workspaceId, subjectId, permission)`.
## Exports
[Section titled “Exports”](#exports)
`@vperms/sql-adapter` exports `VeguiPermsSqlAdapter`, and the types `SqlAdapterDriver`, `SubjectRecord` and `GrantRecord`. The [`@vperms/drizzle-adapter`](/adapters/drizzle/) root entry re-exports them.
# LLM documentation
> Access machine-readable VeguiPerms documentation for AI assistants and coding agents.
Use these text versions of the documentation to provide context to an AI assistant or coding agent. They are generated from the same source pages as this site and updated whenever the documentation is built.
## Available files
[Section titled “Available files”](#available-files)
| File | Contents | When to use it |
| --------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- |
| [llms.txt](/llms.txt) | An index linking to the available documentation sets. | Start here to discover the available context. |
| [llms-full.txt](/llms-full.txt) | The complete documentation in a single text file. | Provide comprehensive context about VeguiPerms. |
| [llms-small.txt](/llms-small.txt) | The plugin’s reduced documentation output. | Use the compact output when context space is limited. |
| [API Reference](/_llms-txt/api-reference.txt) | Public API reference for all packages. | Look up exports, options and method signatures. |
| [Adapters](/_llms-txt/adapters.txt) | Adapter documentation and the custom adapter guide. | Choose a persistence backend or implement an adapter. |
The reduced output is generated by the plugin; its size depends on the content and configuration and may be similar to the full version.
## Use with an assistant
[Section titled “Use with an assistant”](#use-with-an-assistant)
Open a file above and copy its URL into an assistant that supports fetching web content. For example:
```text
Read the documentation at , then use the API Reference
and Adapters sets to help me integrate VeguiPerms into my application.
```
If your tool cannot fetch URLs, download the relevant text file and attach it as context instead. A locally hosted URL is only accessible to tools that can reach your local server.
These files provide documentation context; they do not install vperms or run permission checks. For a runnable introduction, see [Getting Started](/getting-started/).
# Grants
> Allow and deny grants and how they are persisted.
A **grant** attaches a permission pattern to a subject with a boolean value: `true` is an **allow**, `false` is an explicit **deny**.
```ts
import type { PermissionGrant } from "vperms";
interface PermissionGrant {
permission: string;
value: boolean;
}
```
## Allowing and denying
[Section titled “Allowing and denying”](#allowing-and-denying)
`setPermission` upserts a grant; `unsetPermission` removes it:
```ts
await vperms.setPermission("workspace", "user", "posts.read", true); // allow
await vperms.setPermission("workspace", "user", "posts.delete", false); // deny
await vperms.unsetPermission("workspace", "user", "posts.read"); // remove
```
`setPermission` is an **upsert**: calling it again with the same permission replaces the value. There is at most one grant per `(subject, permission)` pair.
Tip
Representing denies explicitly is what lets a broad allow be narrowed later. For example, grant `workspaces.*` as an allow and `workspaces.1.*` as a deny — the more specific deny wins.
## Deny wins ties
[Section titled “Deny wins ties”](#deny-wins-ties)
When two grants match a requested permission with the **same** specificity, depth and layer, an explicit deny wins over an allow. This makes it safe to add a deny that cancels an inherited allow without removing it.
## Reading grants
[Section titled “Reading grants”](#reading-grants)
The adapter exposes the raw grants of a subject:
```ts
import { VeguiPermsMemoryAdapter } from "vperms";
const adapter = new VeguiPermsMemoryAdapter();
await adapter.grantPermission("workspace", "user", "posts.read", true);
await adapter.findSubjectGrants("workspace", "user");
// [{ permission: "posts.read", value: true }]
```
Grants returned by an adapter are plain data. All validation, matching, inheritance and precedence happen in the service and the core engine — never in the adapter.
## Inheritance source
[Section titled “Inheritance source”](#inheritance-source)
Grants are attached to a subject but can be reached by its descendants. A subject inherits the grants of every id listed in its `parents` (plus virtual default parents). Continue with [Inheritance](/concepts/inheritance/) to see how the engine walks them.
# Inheritance and default parents
> How subjects inherit grants through parents and virtual default parents.
A subject inherits the grants of every id listed in its `parents`, and those parents inherit from their own parents, and so on. Inheritance is resolved at evaluation time — nothing is copied or persisted.
```ts
await vperms.saveSubject("workspace", {
id: "user",
type: SubjectType.User,
parents: ["developers"],
});
await vperms.saveSubject("workspace", {
id: "developers",
type: SubjectType.Group,
parents: ["staff"],
});
await vperms.setPermission("workspace", "staff", "posts.read", true);
await vperms.can("workspace", "user", "posts.read"); // true (two levels up)
```
## Direct grants win first
[Section titled “Direct grants win first”](#direct-grants-win-first)
Evaluation is two-phase:
1. **Direct grants** of the subject are matched first. If any direct grant matches, it decides the result and inheritance is not consulted.
2. Otherwise the **inherited** grants are resolved and matched.
If neither phase matches, the built-in [`vperms.subject.me.permissions`](/guides/exporting/) grant is checked last.
```ts
await vperms.setPermission("workspace", "developers", "posts.read", true);
await vperms.setPermission("workspace", "user", "posts.read", false);
await vperms.can("workspace", "user", "posts.read"); // false (direct deny wins)
```
## Parent layers
[Section titled “Parent layers”](#parent-layers)
Parents come from three sources, called **layers**. Lower is higher priority:
| Layer | Name | Source |
| ----- | -------------- | ----------------------------- |
| `0` | Explicit | `subject.parents` |
| `1` | Type default | `defaultParents.byType[type]` |
| `2` | Global default | `defaultParents.global` |
| `3` | Built-in | the self-permissions grant |
The next layer is only consulted when the previous one produced **no matching permission** — not merely when it had no grants. Within a layer, closer parents take priority over more distant ancestors.
## Virtual (default) parents
[Section titled “Virtual (default) parents”](#virtual-default-parents)
`defaultParents` applies virtual parents to every subject the service evaluates, without storing them:
```ts
const vperms = new VeguiPermsService({
adapter,
defaultParents: {
global: ["everyone"],
byType: { user: ["users"], service: ["services"] },
},
});
```
Parents reached through a default are tagged with the worse of the two layers, so a default edge can never outrank an explicit edge. A parent reached through a default also contributes its own explicit parents and its own defaults.
Default parents are validated by `DefaultParentsSchema`. They **cannot** be negation directives — opting out happens on the subject, not in configuration.
## Negating a virtual parent
[Section titled “Negating a virtual parent”](#negating-a-virtual-parent)
Prefix an entry in `parents` with `!` to opt the subject out of that virtual parent:
```ts
await vperms.saveSubject("workspace", {
id: "user",
type: SubjectType.User,
parents: ["!everyone"],
});
```
Negation behaves as follows:
* It **only** opts out of virtual parents (`byType` and `global`), removing the id from both sources at once.
* It **never** removes an explicit parent with the same id. If the subject also lists `everyone` explicitly, the explicit entry wins and inheritance still happens.
* When declared on the evaluated subject, it is **propagated through the whole walk**: the negated id is never reached through a virtual source, not even when an intermediate parent would apply it as one of its own defaults.
* A parent that lists the negated id **explicitly** still reintroduces it.
The negation prefix is exported as `VIRTUAL_PARENT_NEGATION` (`"!"`), and `splitParents(parents)` returns the explicit parents and the set of excluded ids:
```ts
import { splitParents } from "vperms";
splitParents(["developers", "!everyone"]);
// { explicit: ["developers"], excluded: Set { "everyone" } }
```
## Cycles and order independence
[Section titled “Cycles and order independence”](#cycles-and-order-independence)
Inheritance uses a visited set, so cyclic parents (`a → b → a`) never cause infinite recursion and duplicate paths are collapsed. The result does not depend on the order of the `parents` array: every tiebreak is content-based.
```ts
import { effectiveParentLayers } from "vperms";
effectiveParentLayers(
{ id: "user", type: SubjectType.User, parents: ["developers", "!everyone"] },
{ global: ["everyone"], byType: { user: ["users"] } },
);
// {
// explicit: ["developers"],
// byType: ["users"],
// global: [],
// }
```
`effectiveParentLayers(subject, defaults)` returns the three layers already deduplicated, with negation applied. Continue with [Resolution](/concepts/resolution/) to see how the engine turns all of this into a single effective permission set.
# Permissions and patterns
> Dot-separated permissions, wildcards and specificity.
Permissions are **dot-separated segments**, for example `workspaces.create`, `workspaces.1.read`, or `posts.comments.delete`. Segments may contain any character except `.`, and a permission cannot have empty segments:
```plaintext
^[^.]+(?:\.[^.]+)*$ // PermissionSchema
```
Granted permissions act as **patterns**: a granted permission implies a requested one when every segment matches. A segment is a match when it is equal, or when the granted segment is `*` (matches any single segment). A trailing `*` matches any number of remaining segments, including none.
| Granted | Requested | Allowed |
| ------------------- | --------------------- | ------- |
| `workspaces.1.read` | `workspaces.1.read` | yes |
| `workspaces.1.*` | `workspaces.1.read` | yes |
| `workspaces.*.read` | `workspaces.7.read` | yes |
| `workspaces.*` | `workspaces.7.create` | yes |
| `workspaces.1.*` | `workspaces.2.read` | no |
| `workspaces.1` | `workspaces.1.read` | no |
## `matchesPattern`
[Section titled “matchesPattern”](#matchespattern)
The matcher is exported from `@vperms/core` (and re-exported by `vperms`):
```ts
import { matchesPattern } from "vperms";
matchesPattern("workspaces.1.*", "workspaces.1.read"); // true
matchesPattern("workspaces.*.read", "workspaces.7.read"); // true
matchesPattern("workspaces.1.*", "workspaces.2.read"); // false
```
`matchesPattern(grantPattern, permission)` is a pure, synchronous segment comparison. It performs no inheritance and knows nothing about grants or denies.
## Specificity
[Section titled “Specificity”](#specificity)
When several grants match the same requested permission, the engine must decide which one wins. It scores each pattern with `permissionSpecificity`:
* an exact segment adds **2**,
* an inner `*` adds **1**,
* a trailing `*` adds **0**.
```ts
import { permissionSpecificity } from "vperms";
permissionSpecificity("workspaces.1.read"); // 6
permissionSpecificity("workspaces.*.read"); // 5
permissionSpecificity("workspaces.1.*"); // 4
permissionSpecificity("workspaces.*"); // 2
```
More specific patterns win over broader wildcards. See [Resolution](/concepts/resolution/) for the full precedence order and how specificity combines with inheritance depth and source layer.
## Matching a permission against grants
[Section titled “Matching a permission against grants”](#matching-a-permission-against-grants)
`matchPermission(grants, permission)` returns the value of the best matching grant, or `null` when nothing matches:
```ts
import { matchPermission } from "vperms";
const grants = [
{ permission: "workspaces.*", value: true },
{ permission: "workspaces.1.*", value: false },
];
matchPermission(grants, "workspaces.1.read"); // false (more specific deny)
matchPermission(grants, "workspaces.2.read"); // true
matchPermission(grants, "posts.read"); // null
```
Grants are sorted with `compareGrants` first, so the result does not depend on the order of the array.
## Reserved permissions
[Section titled “Reserved permissions”](#reserved-permissions)
VeguiPerms reserves a small permission namespace for reading resolved permissions:
* `vperms.subject.me.permissions` — always granted to every subject (at the lowest priority, so a deny can override it). Lets a subject read its own resolved permission set.
* `vperms.subject..permissions` — permission to read another subject’s resolved set. Normal wildcards apply, e.g. `vperms.subject.*.permissions`.
```ts
import { subjectPermissionsPermission } from "vperms";
subjectPermissionsPermission("user"); // "vperms.subject.user.permissions"
```
See [Exporting resolved permissions](/guides/exporting/).
# Resolution and precedence
> How grants are ordered, deduplicated and weighted into a resolved subject.
**Resolution** is the process of turning the direct and inherited grants of a subject into a single, ordered list of effective permissions. It is what makes `can()` deterministic and what clients evaluate locally.
```ts
const resolved = await vperms.resolvePermissions("workspace", "user");
// {
// id: "user",
// type: "user",
// parents: ["developers"],
// permissions: [{ permission: "workspaces.1.read", value: true, weight: 100 }],
// }
```
## Precedence order
[Section titled “Precedence order”](#precedence-order)
Grants are sorted with `compareGrants`, highest priority first:
1. **Layer** — explicit parents, then type defaults, then global defaults, then built-in (lower wins).
2. **Depth** — closer parents before more distant ancestors.
3. **Specificity** — more specific pattern before broader wildcard.
4. **Exact segments** — more exact segments before fewer.
5. **Deny before allow** — an explicit deny wins an exact tie.
6. **Pattern**, then **subject id** — stable, content-based tiebreak.
Because every tiebreak is content-based, the result is independent of the order of the `parents` array and of grant insertion order.
## Deduplication and weights
[Section titled “Deduplication and weights”](#deduplication-and-weights)
Once ordered, grants are **deduplicated by permission**, keeping the highest-priority occurrence. Each remaining permission is then assigned a **weight** that decreases with its position:
```plaintext
weight = (number of effective permissions) - index
```
The highest-priority permission receives the largest weight. `weight` is computed on the fly and never persisted. It folds source layer, inheritance depth and pattern specificity into a single comparable number.
## Evaluating a resolved subject
[Section titled “Evaluating a resolved subject”](#evaluating-a-resolved-subject)
`canResolved(permissions, permission)` re-evaluates a permission using only the DTO: among all matching entries, the one with the highest weight wins.
```ts
import { canResolved } from "vperms";
const permissions = [
{ permission: "workspaces.*", value: true, weight: 1 },
{ permission: "workspaces.1.*", value: false, weight: 2 },
];
canResolved(permissions, "workspaces.1.read"); // false
canResolved(permissions, "workspaces.2.read"); // true
canResolved(permissions, "posts.read"); // false
```
`canResolved` is guaranteed to agree with server-side `can()`, because both use the same matcher and the same precedence. This is the mechanism behind the [browser client](/integrations/client/) and [React bindings](/integrations/react/): they ship the JSON snapshot and need no adapter, no inheritance and no evaluation on the server.
## Manual resolution
[Section titled “Manual resolution”](#manual-resolution)
Lower-level building blocks are exported from `@vperms/core`:
```ts
import {
resolveInheritedPermissions,
resolveSubjectPermissions,
} from "@vperms/core";
// Only the inherited grants, tagged with `depth` and `layer`.
const inherited = await resolveInheritedPermissions(adapter, "workspace", subject);
// Direct + inherited + built-in, deduplicated and weighted.
const effective = await resolveSubjectPermissions(adapter, "workspace", subject);
```
`ResolvedPermissionGrant` extends `PermissionGrant` with `depth` and `layer`, so you can inspect where a permission came from before it is flattened into a `ResolvedSubject`.
## Next steps
[Section titled “Next steps”](#next-steps)
* [Guides: the service](/guides/service/) — `can()`, `resolvePermissions()` and the request runtime.
* [Guides: exporting](/guides/exporting/) — exposing the DTO over HTTP.
* [Adapters](/adapters/overview/) — persisting subjects and grants.
# Subjects
> Users, services, groups and the anonymous subject.
A **subject** is anything that can be granted permissions: a user, a service, a group, or the anonymous visitor. Subjects are persisted inside a **workspace**, a logical partition that namespaces both subjects and grants.
## The `Subject` type
[Section titled “The Subject type”](#the-subject-type)
```ts
import type { Subject } from "vperms";
interface Subject {
id: string;
type: SubjectType;
parents: string[];
}
```
* `id` is the subject identifier. It must be a non-empty string and is unique within a workspace.
* `type` classifies the subject (see `SubjectType`). Type defaults can attach virtual parents to whole types.
* `parents` lists the ids this subject inherits permissions from. See [Inheritance](/concepts/inheritance/) for how parents are resolved and how the `!` negation prefix works.
## `SubjectType`
[Section titled “SubjectType”](#subjecttype)
`SubjectType` is a string enum with four members:
| Member | Value | Typical use |
| --------- | ----------- | ------------------------------------ |
| `User` | `"user"` | Human accounts. |
| `Service` | `"service"` | Machine-to-machine callers. |
| `Group` | `"group"` | Roles and teams that hold grants. |
| `Anon` | `"anon"` | The anonymous, unauthenticated user. |
```ts
import { SubjectType } from "vperms";
const group = {
id: "developers",
type: SubjectType.Group,
parents: [],
};
```
## Workspaces
[Section titled “Workspaces”](#workspaces)
Every service method receives a workspace identifier as its first argument. Subjects are stored per workspace, so the same id in two workspaces is two different subjects:
```ts
await vperms.saveSubject("workspace-a", { id: "user", type: SubjectType.User, parents: [] });
await vperms.saveSubject("workspace-b", { id: "user", type: SubjectType.User, parents: [] });
```
Workspace ids are opaque strings validated by `WorkspaceIdSchema`. Use a workspace to isolate tenants, environments or independent authorization domains.
## Principals
[Section titled “Principals”](#principals)
A `Principal` is any object that can answer which subject id it represents. Methods that identify a subject accept either a raw id or a `Principal`:
```ts
import type { Principal } from "vperms";
class UserPrincipal implements Principal {
constructor(private readonly id: string) {}
getSubjectId(): string {
return this.id;
}
}
await vperms.can("workspace", new UserPrincipal("user"), "posts.read");
```
The service normalizes the value with `getSubjectId()` before validating it, so adapters only ever see subject ids. This lets you pass an authenticated user object, an actor wrapper, or any object that knows its own subject id without unwrapping it at the call site.
## The anonymous subject
[Section titled “The anonymous subject”](#the-anonymous-subject)
When an integration’s subject resolver returns `null` or `undefined`, the request is treated as anonymous. The reserved id is exported as `ANONYMOUS_SUBJECT_ID`:
```ts
import { ANONYMOUS_SUBJECT_ID } from "vperms";
console.log(ANONYMOUS_SUBJECT_ID); // "anonymous"
```
The anonymous subject is created on demand with `SubjectType.Anon` the first time it is needed (for example, by `resolveRequestContext`), so it can receive grants just like any other subject. See [Middleware and requests](/guides/service/) for the request context.
## Validation
[Section titled “Validation”](#validation)
Subjects are validated by `SubjectSchema` before they reach an adapter:
* `id` is a non-empty string (`SubjectIdSchema`).
* `type` is one of the `SubjectType` values.
* `parents` is an array of ids; the `!` prefix is allowed in stored parents (it is a negation directive), but **not** in default parents.
Invalid input throws a `ZodError` (see [Errors](/reference/errors/)).
# Getting Started
> Open-source authorization library based on hierarchical permissions.
**vperms** (VeguiPerms) is an open-source authorization library based on hierarchical, dot-separated permissions such as `workspaces.create`, `workspaces.1.read`, or `workspaces.1.*`.
Permissions are stored as **grants** (`allow` or `deny`), attached to **subjects** (users, services, groups). Subjects inherit grants from their **parents**, and virtual **default parents** can apply to every subject automatically. The engine resolves the whole effective permission set with a deterministic precedence, so evaluation is consistent on the server and in the browser.
## Install
[Section titled “Install”](#install)
```sh
bun add vperms
```
## Your first permission check
[Section titled “Your first permission check”](#your-first-permission-check)
```ts
import {
SubjectType,
VeguiPermsMemoryAdapter,
VeguiPermsService,
} from "vperms";
const vperms = new VeguiPermsService({
adapter: new VeguiPermsMemoryAdapter(),
});
await vperms.saveSubject("workspace", {
id: "user",
type: SubjectType.User,
parents: ["developers"],
});
await vperms.saveSubject("workspace", {
id: "developers",
type: SubjectType.Group,
parents: [],
});
await vperms.setPermission("workspace", "developers", "workspaces.1.*", true);
await vperms.can("workspace", "user", "workspaces.1.read"); // true (inherited)
await vperms.can("workspace", "user", "workspaces.2.read"); // false
```
## Packages
[Section titled “Packages”](#packages)
VeguiPerms is a monorepo. Install only what you need:
| Package | Description |
| ------------------------- | ------------------------------------------------------------- |
| `vperms` | Application-facing service, validation and public types. |
| `@vperms/core` | Pure permission engine (matching, inheritance, resolution). |
| `@vperms/client` | Framework-independent resolved-permission client. |
| `@vperms/react` | React and React Server Component bindings. |
| `@vperms/express` | Express middleware and route guards. |
| `@vperms/hono` | Hono middleware and route guards. |
| `@vperms/nest` | NestJS module, guard, decorators. |
| `@vperms/next` | Next.js integration (Server Components, Route Handlers, RSC). |
| `@vperms/sql-adapter` | Dialect-agnostic SQL base adapter. |
| `@vperms/drizzle-adapter` | SQLite, MySQL and Postgres adapters via Drizzle ORM. |
| `@vperms/mongodb-adapter` | MongoDB adapter. |
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* **[Concepts](/concepts/subjects/)** — subjects, permissions, grants, inheritance and resolution.
* **[Guides](/guides/core/)** — the core engine, the service, exporting resolved permissions and writing a custom adapter.
* **[Adapters](/adapters/overview/)** — persistence backends.
* **[Integrations](/integrations/client/)** — HTTP frameworks and the browser client.
* **[API Reference](/reference/core/)** — the exported surface of every package.
## Requirements
[Section titled “Requirements”](#requirements)
* [Bun](https://bun.sh) 1.x to build and test the repository.
* [Node.js](https://nodejs.org) 18+ is supported at runtime; the package is published as runtime-neutral ESM.
# The core engine
> The pure permission engine behind the service.
`@vperms/core` is the **pure TypeScript engine**: pattern matching, inheritance traversal, precedence and resolution. It has no dependencies, no validation and no I/O beyond the adapter abstraction. Most applications use the higher-level [`vperms` service](/guides/service/) instead, but the core is public and useful when you need the building blocks directly.
## The adapter contract
[Section titled “The adapter contract”](#the-adapter-contract)
Everything the engine needs from storage goes through one abstract class:
```ts
import type { PermissionGrant, Subject } from "@vperms/core";
abstract class VeguiPermsAdapter {
abstract findSubject(workspaceId: string, subjectId: string): Promise;
abstract saveSubject(workspaceId: string, subject: Subject): Promise;
abstract deleteSubject(workspaceId: string, subjectId: string): Promise;
abstract findSubjectGrants(workspaceId: string, subjectId: string): Promise;
abstract grantPermission(
workspaceId: string,
subjectId: string,
permission: string,
value: boolean,
): Promise;
abstract ungrantPermission(
workspaceId: string,
subjectId: string,
permission: string,
): Promise;
}
```
An adapter only stores and retrieves data. It performs **no validation, resolution, inheritance or evaluation** — the engine and service handle all of that. See [Writing a custom adapter](/guides/custom-adapter/).
Note
`PermissionGrant` carries `subjectId` and `workspaceId` in addition to `permission` and `value`.
## Matching
[Section titled “Matching”](#matching)
```ts
import { matchesPattern, matchPermission } from "@vperms/core";
matchesPattern("workspaces.1.*", "workspaces.1.read"); // true
matchPermission(
[
{ permission: "workspaces.*", value: true, subjectId: "u", workspaceId: "w" },
{ permission: "workspaces.1.*", value: false, subjectId: "u", workspaceId: "w" },
],
"workspaces.1.read",
); // false
```
`matchesPattern` is the raw segment matcher. `matchPermission` sorts with `compareGrants` and returns the best match, or `null`.
## Precedence
[Section titled “Precedence”](#precedence)
```ts
import { compareGrants, permissionSpecificity } from "@vperms/core";
permissionSpecificity("workspaces.*.read"); // 5
permissionSpecificity("workspaces.1.*"); // 4
```
`compareGrants(a, b)` orders grants highest-priority-first. See [Resolution](/concepts/resolution/) for the full rule list.
## Inheritance and resolution
[Section titled “Inheritance and resolution”](#inheritance-and-resolution)
```ts
import {
resolveInheritedPermissions,
resolveSubjectPermissions,
canResolved,
} from "@vperms/core";
const inherited = await resolveInheritedPermissions(adapter, workspaceId, subject, {
defaultParents,
});
const effective = await resolveSubjectPermissions(adapter, workspaceId, subject, {
defaultParents,
});
canResolved(effective, "workspaces.1.read");
```
These are the exact functions the service calls. The `vperms` package re-exports the most common of them (`canResolved`, `resolveSubjectPermissions`, `subjectPermissionsPermission`, `SELF_PERMISSIONS_PERMISSION`, the layer constants, and the shared types).
## Layers
[Section titled “Layers”](#layers)
```ts
import {
EXPLICIT_PARENT_LAYER, // 0
TYPE_DEFAULT_PARENT_LAYER, // 1
GLOBAL_DEFAULT_PARENT_LAYER, // 2
BUILTIN_PERMISSION_LAYER, // 3
} from "@vperms/core";
```
Layer numbers are ordered by priority: lower is stronger. See the full [API reference](/reference/core/).
# Writing a custom adapter
> Implement the persistence boundary for a new backend.
An **adapter** is the only persistence boundary in VeguiPerms. Implementing one means providing six methods over subjects and grants. Everything else — validation, matching, inheritance, resolution — is handled by the engine.
## Extend the abstract class
[Section titled “Extend the abstract class”](#extend-the-abstract-class)
```ts
import type { PermissionGrant, Subject } from "vperms";
import { VeguiPermsAdapter } from "vperms";
export class MyAdapter extends VeguiPermsAdapter {
async findSubject(workspaceId: string, subjectId: string): Promise {
// load, or return null
}
async saveSubject(workspaceId: string, subject: Subject): Promise {
// create or update, then return the stored subject
}
async deleteSubject(workspaceId: string, subjectId: string): Promise {
// return true when a record was removed
}
async findSubjectGrants(workspaceId: string, subjectId: string): Promise {
// return [] when the subject has no grants
}
async grantPermission(
workspaceId: string,
subjectId: string,
permission: string,
value: boolean,
): Promise {
// MUST upsert and return the stored grant
}
async ungrantPermission(
workspaceId: string,
subjectId: string,
permission: string,
): Promise {
// return true when a grant was removed
}
}
```
## Rules
[Section titled “Rules”](#rules)
* **Upsert grants.** For a given `(workspaceId, subjectId, permission)` there is at most one grant; a new `value` replaces the previous one.
* **Do no validation.** Every value has already been validated by `VeguiPermsService` with Zod. `PermissionGrant` includes `subjectId` and `workspaceId`.
* **Do no resolution.** Never traverse parents, match patterns or evaluate permissions in the adapter.
* **Return plain data.** Subjects and grants are JSON-safe objects.
* **Be idempotent where it matters.** `deleteSubject` and `ungrantPermission` return `false` when there was nothing to remove.
## Migrations
[Section titled “Migrations”](#migrations)
VeguiPerms has no implicit schema management. Database adapters expose an explicit `migrate()` that the application calls once at startup:
```ts
const adapter = new VeguiPermsSqliteAdapter({ db });
await adapter.migrate(); // explicit, idempotent
```
Follow the same convention for a database-backed custom adapter: dependency-inject an already-created client, never open or close connections inside the adapter, and apply schema changes only when `migrate()` is called.
## Base adapters
[Section titled “Base adapters”](#base-adapters)
If your backend is SQL, extend `VeguiPermsSqlAdapter` from `@vperms/sql-adapter` instead of writing everything from scratch — it maps rows to the public types and adds `migrate()`. The [Drizzle adapters](/adapters/drizzle/) are built on it.
## Testing your adapter
[Section titled “Testing your adapter”](#testing-your-adapter)
The private `@vperms/adapter-contract` package holds the shared contract suite every official adapter must pass. Run it against your adapter to guarantee identical behavior across backends. Official adapters are also tested against real databases via the integration suite (`docker compose up -d`, then `bun run test:integration`).
# Exporting resolved permissions
> Expose the resolved-permission DTO over HTTP with authorization.
The `vperms` package ships a framework-independent **permission export** behavior, used by the Express, Hono and Nest integrations. It serves the JSON-safe `ResolvedSubject` of a target subject after authorizing the request.
## Reserved permissions
[Section titled “Reserved permissions”](#reserved-permissions)
Reading resolved permissions is itself protected by reserved permission nodes:
* `vperms.subject.me.permissions` — every subject can read **its own** set. It is resolved as the lowest-priority grant, so an explicit deny overrides it.
* `vperms.subject..permissions` — required to read **another** subject’s set. Normal wildcards apply, e.g. `vperms.subject.*.permissions`.
```ts
import {
SELF_PERMISSIONS_PERMISSION,
subjectPermissionsPermission,
} from "vperms";
SELF_PERMISSIONS_PERMISSION; // "vperms.subject.me.permissions"
subjectPermissionsPermission("user"); // "vperms.subject.user.permissions"
```
To let a group read everyone’s permissions:
```ts
await vperms.setPermission("workspace", "admins", "vperms.subject.*.permissions", true);
```
## Route pattern
[Section titled “Route pattern”](#route-pattern)
The export path must contain exactly one `:param` segment, which identifies the target subject. `parsePermissionsExportPath` turns it into a matcher:
```ts
import { parsePermissionsExportPath } from "vperms";
const route = parsePermissionsExportPath("/subject/:subjectId");
route.base; // "subject"
route.routePattern; // "subject/:subjectId"
route.param; // "subjectId"
route.match("/subject/user"); // { subjectId: "user" }
route.match("/subject/a/b"); // null
```
The literal target `"me"` is treated as the current subject. Any other value is validated as a subject id.
## Authorizing and loading
[Section titled “Authorizing and loading”](#authorizing-and-loading)
`exportResolvedSubject` is the shared implementation:
```ts
import { exportResolvedSubject } from "vperms";
const resolved = await exportResolvedSubject({
service,
adapter,
workspaceId: "workspace",
currentSubjectId: context.id,
targetSubjectId: "user",
ability: context.ability,
});
```
It always **authorizes before loading** the target:
1. Resolve the target (treating `"me"` as the current subject) and validate it.
2. Require `ability.can(...)` for the matching reserved permission.
3. Load the subject record and return its resolved permissions.
It throws typed errors so each integration can map them to an HTTP response:
| Error | Meaning | HTTP |
| ----------------------- | ------------------------------------ | ---- |
| `PermissionDeniedError` | The ability lacks the reserved node. | 403 |
| `SubjectNotFoundError` | No record for the target subject. | 404 |
| `InvalidSubjectIdError` | The target id is malformed. | 400 |
## Configuring an integration
[Section titled “Configuring an integration”](#configuring-an-integration)
Enable the endpoint by passing `permissionsExport` to the integration’s options. The route is disabled unless configured.
```ts
// Express
vpermsMiddleware({
adapter,
workspace: "workspace",
resolver: (req) => req.user?.id ?? null,
permissionsExport: { path: "/subject/:subjectId" },
});
// NestJS
VPermsModule.forRoot({
adapter,
workspace: "workspace",
resolver: (req) => req.user?.id ?? null,
permissionsExport: { path: "/subject/:subjectId" },
});
```
The result is a JSON `ResolvedSubject` that the [client](/integrations/client/) and [React](/integrations/react/) bindings can evaluate directly. See the per-framework pages for the exact behavior:
* [Express](/integrations/express/)
* [Hono](/integrations/hono/)
* [NestJS](/integrations/nest/)
# The service
> VeguiPermsService, validation, principals and request runtime.
`VeguiPermsService` is the application-facing API exported by the `vperms` package. It owns validation, permission resolution, inheritance, cycle protection and evaluation; the adapter only persists data.
```ts
import { VeguiPermsMemoryAdapter, VeguiPermsService } from "vperms";
const vperms = new VeguiPermsService({
adapter: new VeguiPermsMemoryAdapter(),
defaultParents: { global: ["everyone"] },
});
```
## Options
[Section titled “Options”](#options)
```ts
interface VeguiPermsServiceOptions {
adapter: VeguiPermsAdapter;
defaultParents?: DefaultParents;
}
```
`defaultParents` is validated with `DefaultParentsSchema` when the service is constructed. See [Inheritance](/concepts/inheritance/).
## Methods
[Section titled “Methods”](#methods)
| Method | Returns | Description |
| -------------------------------------------------------- | -------------------------- | ----------------------------------------------- |
| `can(workspaceId, subject, permission)` | `Promise` | Whether the subject may perform the permission. |
| `resolvePermissions(workspaceId, subject)` | `Promise` | Full effective permission snapshot. |
| `saveSubject(workspaceId, subject)` | `Promise` | Create or update a subject. |
| `deleteSubject(workspaceId, subject)` | `Promise` | Delete a subject; `true` when it existed. |
| `setPermission(workspaceId, subject, permission, value)` | `Promise` | Upsert an allow/deny grant. |
| `unsetPermission(workspaceId, subject, permission)` | `Promise` | Remove a grant; `true` when it existed. |
Every method takes the workspace id first and accepts a raw subject id or a [`Principal`](/concepts/subjects/#principals). All public inputs are validated with [Zod](https://zod.dev); invalid input throws a `ZodError`.
```ts
await vperms.saveSubject("workspace", {
id: "user",
type: SubjectType.User,
parents: ["developers"],
});
await vperms.setPermission("workspace", "developers", "posts.read", true);
await vperms.can("workspace", "user", "posts.read"); // true
await vperms.resolvePermissions("workspace", "user");
```
`can()` returns `false` when the adapter has no record for the subject. `resolvePermissions()` throws `SubjectNotFoundError` in that case, because the snapshot needs a subject record. Direct grants short-circuit inheritance; then explicit, type-default and global-default layers are consulted in order.
## Request runtime
[Section titled “Request runtime”](#request-runtime)
The runtime helpers are shared by all framework integrations. They hydrate a request-scoped context exactly once per request.
```ts
import {
createAbility,
resolveRequestContext,
resolveSubjectId,
} from "vperms";
const ability = createAbility(vperms, "workspace", "user");
await ability.can("posts.read"); // memoized per permission
```
`createAbility(service, workspaceId, subject)` returns an `Ability` whose `can(permission)` memoizes the promise, so the same permission is never evaluated twice within a request.
`resolveRequestContext` performs the whole dance:
```ts
const context = await resolveRequestContext({
adapter,
service: vperms,
workspaceId: "workspace",
subject: await getCurrentUser(), // SubjectId | Principal | null
});
context.id; // resolved subject id ("anonymous" when subject was null)
context.subject; // Subject record, or undefined when not persisted
context.kind; // SubjectType, or undefined
context.ability; // Ability
```
When the resolver returns `null`/`undefined`, the **anonymous subject** is loaded — created on demand with `SubjectType.Anon` via `ensureAnonymousSubject` — so `defaultParents.byType.anon` applies automatically. `ANONYMOUS_SUBJECT_ID` is `"anonymous"`.
The `Ability` interface is intentionally minimal:
```ts
interface Ability {
can(permission: string): Promise;
}
```
Middleware, guards, decorators and permission-export handlers all reuse the same instance.
# Vanilla client
> Load and evaluate resolved subjects anywhere.
`@vperms/client` is a framework-independent client for the [resolved-permission DTO](/concepts/resolution/). Resolved subjects are plain JSON, so they can be evaluated without a server, an adapter or any inheritance logic.
```ts
import { createVPerms } from "@vperms/client";
const vperms = createVPerms("https://api.example.com", {
prefix: "/vperms", // default
subjectResolver: async () => currentUser,
fetchOptions: { credentials: "include" },
});
const ability = await vperms.getAbility();
ability.can("workspaces.1.read"); // synchronous, same result as server can()
```
## Configuration
[Section titled “Configuration”](#configuration)
```ts
interface VPermsConfig {
prefix?: string; // default "/vperms"
subjectResolver?: () => SubjectId | Principal | null | Promise<...>;
fetch?: FetchLike; // default globalThis.fetch
fetchOptions?: RequestInit;
}
```
* `origin` may be absolute (`https://api.example.com`), relative (`/api`) or empty (`""` for the current origin).
* `subjectResolver` resolves the current subject. Returning `null` (or omitting it) loads the anonymous subject through `/subject/me`.
## Methods
[Section titled “Methods”](#methods)
```ts
interface VPermsClient {
readonly origin: string;
readonly prefix: string;
getResolvedSubject(subjectId?: SubjectId | Principal): Promise;
getAbility(subjectId?: SubjectId | Principal): Promise;
}
```
With no explicit subject, the client requests `{origin}{prefix}/subject/me`; with one, it requests `{origin}{prefix}/subject/:subjectId`. Responses are validated with `ResolvedSubjectSchema` before use.
## The ability
[Section titled “The ability”](#the-ability)
```ts
import { createAbility, fetchResolvedSubject } from "@vperms/client";
const ability = createAbility(resolvedSubject);
ability.subject; // ResolvedSubject snapshot (immutable)
ability.permissions; // ResolvedPermission[]
ability.can("posts.read");
```
`PermissionAbility` (also exported as `Ability`) wraps a single snapshot. Its `can()` is **synchronous** and only evaluates `ResolvedPermission[]` with the same matcher and weight precedence as `canResolved()`. It knows nothing about parents, default parents, `!parent`, inheritance depth or adapters.
## Low-level loading
[Section titled “Low-level loading”](#low-level-loading)
```ts
import { fetchResolvedSubject, VPermsHttpError } from "@vperms/client";
const me = await fetchResolvedSubject("/api/vperms/subject/me");
```
`fetchResolvedSubject(url, { fetch, requestInit })` is what the client uses internally: it validates the body with `ResolvedSubjectSchema` and throws `VPermsHttpError` (with `status` and `url`) on non-2xx responses. Plug in `fetch` for SSR, cookie forwarding or tests.
Caution
Client-side permissions are for UX only. Every sensitive operation must still be authorized on the server with [`VeguiPermsService`](/guides/service/).
# Express
> Middleware, route guards and the permissions export endpoint.
`@vperms/express` wires a [`VeguiPermsService`](/guides/service/) into an Express application: it resolves the current subject on every request, exposes an ability on `req`, ships route guards and can serve the [permissions export endpoint](/guides/exporting/).
```ts
import express from "express";
import { vpermsMiddleware, hasPermission } from "@vperms/express";
import { VeguiPermsMemoryAdapter } from "vperms";
const app = express();
app.use(
vpermsMiddleware({
adapter: new VeguiPermsMemoryAdapter(),
workspace: "workspace",
resolver: (req) => req.session?.userId ?? null,
}),
);
app.get("/posts/:id", hasPermission("posts.read"), async (req, res) => {
res.json({ allowed: await req.ability.can("posts.write") });
});
```
## Middleware options
[Section titled “Middleware options”](#middleware-options)
```ts
interface VpermsMiddlewareOptions {
adapter: VeguiPermsAdapter;
resolver: SubjectResolver;
workspace: string | WorkspaceResolver;
defaultParents?: DefaultParents;
permissionsExport?: { path: string };
}
type SubjectResolver =
(req: Request) => SubjectId | Principal | null | Promise<...>;
type WorkspaceResolver = (req: Request) => string | Promise;
```
* `resolver` returns the subject identity for the request. Returning `null` resolves the anonymous subject (`anonymous`, type `anon`).
* `workspace` is a constant id or a function when the workspace depends on the request (host, tenant, path segment…).
* `permissionsExport.path` enables the export route, e.g. `"/vperms/subject/:subjectId"`.
On each request the middleware hydrates `req.ability`, `req.subject` and `req.kind`, then calls `next()`. Any thrown error is forwarded to `next(error)`.
## Route guards
[Section titled “Route guards”](#route-guards)
```ts
import { hasPermission, hasAnyPermission } from "@vperms/express";
app.get("/posts/:id", hasPermission("posts.read"), handler);
app.delete("/posts/:id", hasAnyPermission("admin", "posts.delete"), handler);
```
`hasPermission` requires **all** inputs, `hasAnyPermission` requires **at least one**. Permissions may also be functions of the request:
```ts
import type { PermissionBuilder } from "@vperms/express";
const ownsPost: PermissionBuilder = (req) => `posts.${req.params.id}.write`;
app.put("/posts/:id", hasPermission(ownsPost), handler);
```
Both guards respond `403` with `res.sendStatus(403)` when denied. If `vpermsMiddleware` was not registered first they throw a descriptive error instead of silently allowing the request.
## Request additions
[Section titled “Request additions”](#request-additions)
The package augments the global Express `Request`:
```ts
declare global {
namespace Express {
interface Request {
ability: RequestAbility; // can(permission): Promise
subject?: Subject;
kind?: SubjectType;
}
}
}
```
`req.ability.can()` is memoized per request and permission.
## Permissions export
[Section titled “Permissions export”](#permissions-export)
```ts
app.use(
vpermsMiddleware({
adapter,
workspace: "workspace",
resolver,
permissionsExport: { path: "/vperms/subject/:subjectId" },
}),
);
```
A matching `GET` is answered directly by the middleware:
| Status | Meaning |
| ------ | --------------------------------------------- |
| `200` | `ResolvedSubject` JSON |
| `403` | The caller cannot read the target permissions |
| `404` | The subject does not exist |
| `400` | The subject id is invalid |
See [Exporting resolved permissions](/guides/exporting/) for the authorization rules.
## Exports
[Section titled “Exports”](#exports)
`RequestAbility`, `vpermsMiddleware`, `hasPermission`, `hasAnyPermission`, `PermissionBuilder`, `PermissionInput`, `SubjectResolver`, `SubjectResolverResult`, `WorkspaceResolver`, `PermissionsExportOptions`, `VpermsMiddlewareOptions`, `ANONYMOUS_SUBJECT_ID`.
# Hono
> Hono middleware and route guards.
`@vperms/hono` is the Hono counterpart of the [Express integration](/integrations/express/). It sets the ability on the Hono context instead of the request object.
```ts
import { Hono } from "hono";
import {
vpermsMiddleware,
hasPermission,
type VPermsEnv,
} from "@vperms/hono";
import { VeguiPermsMemoryAdapter } from "vperms";
const app = new Hono();
app.use(
vpermsMiddleware({
adapter: new VeguiPermsMemoryAdapter(),
workspace: "workspace",
resolver: (c) => c.get("session")?.userId ?? null,
}),
);
app.get("/posts/:id", hasPermission("posts.read"), (c) =>
c.json({ allowed: true }),
);
```
## Middleware options
[Section titled “Middleware options”](#middleware-options)
```ts
interface VpermsMiddlewareOptions {
adapter: VeguiPermsAdapter;
resolver: SubjectResolver;
workspace: string | WorkspaceResolver;
defaultParents?: DefaultParents;
permissionsExport?: { path: string };
}
type VPermsContext = Context;
type SubjectResolver =
(c: VPermsContext) => SubjectId | Principal | null | Promise<...>;
type WorkspaceResolver = (c: VPermsContext) => string | Promise;
```
The middleware stores three context variables:
```ts
interface VPermsVariables {
ability: RequestAbility;
subject: Subject | undefined;
kind: SubjectType | undefined;
}
```
Use `VPermsEnv` as the app’s generic so `c.get("ability")` is typed.
## Route guards
[Section titled “Route guards”](#route-guards)
```ts
import { hasPermission, hasAnyPermission } from "@vperms/hono";
app.get("/posts/:id", hasPermission("posts.read"), handler);
app.delete("/posts/:id", hasAnyPermission("admin", "posts.delete"), handler);
```
`hasPermission` requires all inputs, `hasAnyPermission` at least one. Both return a bare `403` response (`c.body(null, 403)`) when denied. Permissions can be functions:
```ts
import type { PermissionBuilder } from "@vperms/hono";
const ownsPost: PermissionBuilder = (c) => `posts.${c.req.param("id")}.write`;
app.put("/posts/:id", hasPermission(ownsPost), handler);
```
## Permissions export
[Section titled “Permissions export”](#permissions-export)
With `permissionsExport.path` set, a matching `GET` is answered as JSON:
| Status | Body | Meaning |
| ------ | --------------------- | ----------------------------- |
| `200` | `ResolvedSubject` | Success |
| `403` | empty | Caller cannot read the target |
| `404` | empty | Subject does not exist |
| `400` | `{ "error": string }` | Invalid subject id |
Other requests continue through `await next()`.
## Exports
[Section titled “Exports”](#exports)
`RequestAbility`, `VPermsEnv`, `VPermsVariables`, `VPermsContext`, `vpermsMiddleware`, `hasPermission`, `hasAnyPermission`, `PermissionBuilder`, `PermissionInput`, `SubjectResolver`, `SubjectResolverResult`, `WorkspaceResolver`, `PermissionsExportOptions`, `VpermsMiddlewareOptions`, `ANONYMOUS_SUBJECT_ID`.
# NestJS
> Module, global guard and decorators for NestJS.
`@vperms/nest` integrates with NestJS through a global guard, decorators and a dynamic module. Decorators only store metadata; the guard hydrates the request context once and evaluates the metadata.
```ts
import { Module } from "@nestjs/common";
import { VPermsModule } from "@vperms/nest";
import { VeguiPermsMemoryAdapter } from "vperms";
@Module({
imports: [
VPermsModule.forRoot({
adapter: new VeguiPermsMemoryAdapter(),
workspace: "workspace",
resolver: (req) => req.user?.id ?? null,
}),
],
})
export class AppModule {}
```
## Module options
[Section titled “Module options”](#module-options)
```ts
interface VPermsModuleOptions {
adapter: VeguiPermsAdapter;
resolver: SubjectResolver;
workspace: string;
defaultParents?: DefaultParents;
permissionsExport?: { path: string };
}
type SubjectResolverResult = SubjectId | Principal | null;
type SubjectResolver =
(req: Request) => SubjectResolverResult | Promise;
```
`forRoot` is global. It registers the service, the `VPermsGuard` as an `APP_GUARD` and — when `permissionsExport` is set — a controller that serves the [export endpoint](/guides/exporting/).
## Decorators
[Section titled “Decorators”](#decorators)
```ts
import {
Ability,
AnyPermission,
Kind,
Permission,
Subject,
} from "@vperms/nest";
@Controller("posts")
export class PostsController {
@Get(":id")
@Permission("posts.read")
read(@Ability() ability: RequestAbility) {
return { canWrite: ability.can("posts.write") };
}
@Delete(":id")
@AnyPermission("admin", "posts.delete")
remove(@Subject() subject: Subject, @Kind() kind: SubjectType) {}
}
```
* `@Permission(...)` requires **all** permissions; `@AnyPermission(...)` at least one. Both can be applied to methods or whole controllers, and can be combined with `handler`/`class` metadata resolution.
* Parameter decorators `@Ability()`, `@Subject()` and `@Kind()` read the hydrated state. `@Ability()` throws if the context is missing.
* Permissions may be functions of the request:
```ts
import type { PermissionBuilder } from "@vperms/nest";
const ownsPost: PermissionBuilder = (req) => `posts.${req.params.id}.write`;
@Put(":id")
@Permission(ownsPost)
update() {}
```
## Evaluation order
[Section titled “Evaluation order”](#evaluation-order)
`VPermsGuard` never denies on its own. For each request it hydrates the context idempotently (guarded by a symbol on the request), then:
1. If `@Permission` metadata exists, every permission must pass, otherwise `canActivate` returns `false` (Nest responds `403`).
2. Else if `@AnyPermission` metadata exists, at least one must pass.
3. Routes without metadata are untouched.
## Custom guards
[Section titled “Custom guards”](#custom-guards)
Extend `AbilityGuard` for imperatively defined rules:
```ts
import { AbilityGuard } from "@vperms/nest";
@Injectable()
export class PostOwnerGuard extends AbilityGuard {
protected async check(ability, context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
return ability.can(`posts.${request.params.id}.write`);
}
}
```
`check(ability, context)` is abstract; `getAbility`, `getSubject` and `getKind` are available to subclasses.
## Request shape
[Section titled “Request shape”](#request-shape)
```ts
interface VpermsRequest extends Request {
ability: Ability;
subject?: Subject;
kind?: SubjectType;
}
```
## Exports
[Section titled “Exports”](#exports)
`VPermsModule`, `VPermsGuard`, `AbilityGuard`, `Permission`, `AnyPermission`, `Ability`, `Subject`, `Kind`, `PERMISSION_METADATA`, `ANY_PERMISSION_METADATA`, `VPERMS_OPTIONS`, `VPERMS_SERVICE`, `PermissionBuilder`, `PermissionInput`, `VPermsModuleOptions`, `VpermsRequest`, `SubjectResolver`, `SubjectResolverResult`, `PermissionsExportOptions`.
# Next.js
> App Router integration with local or external backends.
`@vperms/next` targets the Next.js App Router. It can resolve permissions in-process with an adapter, or proxy to a separate vperms server over HTTP.
permissions.ts
```ts
import { createNextVPerms, nextBackend } from "@vperms/next";
import { VeguiPermsMemoryAdapter } from "vperms";
export const vperms = createNextVPerms({
backend: nextBackend({
adapter: new VeguiPermsMemoryAdapter(),
workspace: "workspace",
subjectResolver: (request) => request?.headers.get("x-user-id") ?? null,
}),
});
```
## Backends
[Section titled “Backends”](#backends)
### Local backend
[Section titled “Local backend”](#local-backend)
`nextBackend` resolves entirely inside Next.js — no HTTP hop to a vperms server.
```ts
interface NextBackendOptions {
adapter: VeguiPermsAdapter;
workspace: string;
subjectResolver: SubjectResolver;
defaultParents?: DefaultParents;
}
```
### External backend
[Section titled “External backend”](#external-backend)
`externalBackend` points at a deployed vperms HTTP server. Server Components forward `cookie` and `authorization` (configurable) to the origin.
```ts
import { createNextVPerms, externalBackend } from "@vperms/next";
export const vperms = createNextVPerms({
backend: externalBackend({
origin: "https://api.example.com",
prefix: "/vperms",
subjectResolver: (request) => request?.headers.get("x-user-id") ?? null,
}),
});
```
```ts
interface ExternalBackendOptions {
origin: string;
prefix?: string; // default "/vperms"
subjectResolver?: SubjectResolver;
fetch?: FetchLike;
fetchOptions?: RequestInit;
forwardHeaders?: string[]; // default ["cookie", "authorization"]
}
```
## Configuration
[Section titled “Configuration”](#configuration)
```ts
interface NextVPermsConfig {
backend: Backend;
prefix?: string; // default "/vperms"
client?: "direct" | "proxy"; // default "proxy"
cache?: CacheWrapper;
browserFetchOptions?: RequestInit;
}
```
* `client: "proxy"` (default) routes browser requests through the app’s route handler; `"direct"` calls the external origin from the browser.
* `cache` defaults to `React.cache`, making resolution request/render-scoped.
## Server Components
[Section titled “Server Components”](#server-components)
```tsx
import { vperms } from "@/permissions";
export default async function Page() {
const ability = await vperms.getAbility();
return (
}>
);
}
```
The returned `NextVPerms` exposes `getResolvedSubject`, `getAbility`, `Provider`, `Ability` and `handlers`.
## Route handler
[Section titled “Route handler”](#route-handler)
Export the built-in handlers to serve the client and the permissions export endpoint:
app/vperms/\[...path]/route.ts
```ts
import { vperms } from "@/permissions";
export const { GET } = vperms.handlers;
```
With the local backend the handler authorizes and resolves locally. With the external backend it proxies matching `GET /subject/...` requests to the origin, stripping `host`/`content-length` and disabling redirect following.
## Client Components
[Section titled “Client Components”](#client-components)
```ts
"use client";
import { createNextVPerms } from "@vperms/next/client";
export const vperms = createNextVPerms({
origin: "", // "" for same-origin proxy, external origin for "direct"
prefix: "/vperms",
});
```
`@vperms/next/client` re-exports everything from `@vperms/react/client`.
## Exports
[Section titled “Exports”](#exports)
Server: `createNextVPerms`, `nextBackend`, `externalBackend`, `NextVPermsConfig`, `NextVPerms`, `NextVPermsHandlers`, `NextRouteContext`, `BrowserConfig`, `ClientTransport`, `SubjectResolver`, `SubjectResolverResult`, `NextBackendOptions`, `ExternalBackendOptions`, `Backend`, plus the React server bindings. Client: `createNextVPerms`, `NextClientConfig` and the React client exports.
# React
> React Server Components and Client Components bindings.
`@vperms/react` re-exports the [client](/integrations/client/) factory plus React bindings. Importing `@vperms/react` resolves to the **server** entry under the `react-server` condition and to the **client** entry otherwise, so the same import works in both environments.
permissions.ts
```ts
import { createVPerms } from "@vperms/react";
export const vperms = createVPerms("https://api.example.com", {
subjectResolver: async () => currentUser,
});
```
## Server Components
[Section titled “Server Components”](#server-components)
`createVPerms(origin, config)` returns a `ServerVPerms` whose `getResolvedSubject` and `getAbility` are wrapped in a request/render-scoped cache (`React.cache` by default). Every Server Component in the same render reuses one resolved subject.
```tsx
import { vperms } from "@/permissions";
export default async function Page() {
const ability = await vperms.getAbility();
if (!ability.can("dashboard.read")) return null;
return (
}>
);
}
```
```ts
interface ServerVPerms {
client: VPermsClient;
getResolvedSubject(subjectId?): Promise;
getAbility(subjectId?): Promise;
Provider: (props: { children?: ReactNode }) => Promise;
Ability: (props: AbilityProps) => Promise;
}
```
The most recently created instance also becomes the module default used by the standalone exports `getAbility`, `getResolvedSubject`, `Provider` and `ServerAbility` (alias `Ability`). Calling them without an instance throws `MISSING_INSTANCE_MESSAGE`.
The server `Provider` resolves the subject once and hands the JSON-safe snapshot to the client `AbilityProvider` — the class instance never crosses the RSC boundary.
## Client Components
[Section titled “Client Components”](#client-components)
```tsx
"use client";
import { vperms } from "@/permissions";
function CreateButton() {
const ability = vperms.useAbility();
return ability.can("projects.create") ? : null;
}
```
`createVPerms` on the client returns a `ClientVPerms`:
```ts
interface ClientVPerms {
client: VPermsClient;
getResolvedSubject: VPermsClient["getResolvedSubject"];
getAbility: VPermsClient["getAbility"];
Provider: typeof AbilityProvider;
Ability: typeof ClientAbility;
useAbility: typeof useAbility;
}
```
`useAbility()` throws `MISSING_PROVIDER_MESSAGE` outside a provider. `AbilityProvider` takes a `ResolvedSubject` snapshot and hydrates the ability synchronously for the subtree.
## `` props
[Section titled “\ props”](#ability-props)
```ts
interface AbilityProps {
permission?: string;
permissions?: string[];
any?: boolean;
fallback?: ReactNode;
children?: ReactNode;
}
```
`permission` and `permissions` require **every** permission by default; set `any` to require at least one. Both short-circuit, and `fallback` renders when access is denied. `abilityAllows(ability, props)` is exported if you need the same logic without rendering.
```tsx
}>
```
## Exports
[Section titled “Exports”](#exports)
* Server: `createVPerms`, `getAbility`, `getResolvedSubject`, `Provider`, `ServerAbility` (alias `Ability`), `abilityAllows`, `MISSING_INSTANCE_MESSAGE`, plus the client’s `createAbility`, `fetchResolvedSubject`, `PermissionAbility`, `VPermsHttpError` and types.
* Client: `createVPerms`, `AbilityProvider`, `ClientAbility` (alias `Ability`), `getAbilityContext`, `useAbility`, `MISSING_PROVIDER_MESSAGE`, plus the same client re-exports.
`ServerAbility` and `ClientAbility` are exported explicitly for advanced use.
## Security
[Section titled “Security”](#security)
Client-side permissions are **for conditional rendering and UX only**. They are not authoritative authorization and client state must never be trusted. Every sensitive operation must still be authorized on the server with VeguiPerms.
# @vperms/client
> API reference for the framework-agnostic client.
`@vperms/client` loads resolved subjects over HTTP and evaluates them locally.
## Factory
[Section titled “Factory”](#factory)
```ts
interface VPermsConfig {
prefix?: string; // default "/vperms"
subjectResolver?: SubjectResolver;
fetch?: FetchLike;
fetchOptions?: RequestInit;
}
interface VPermsClient {
readonly origin: string;
readonly prefix: string;
getResolvedSubject(
subjectId?: SubjectId | Principal,
): Promise;
getAbility(subjectId?: SubjectId | Principal): Promise;
}
function createVPerms(origin: string, config?: VPermsConfig): VPermsClient;
```
* `origin` may be absolute (`https://api.example.com`), relative (`/api`) or empty (`""`).
* With an explicit subject, the client requests `/subject/:subjectId`; with no argument it uses `subjectResolver()`, falling back to `/subject/me` when it returns `null` or `undefined`.
## Ability
[Section titled “Ability”](#ability)
```ts
class PermissionAbility {
readonly subject: ResolvedSubject;
readonly permissions: ResolvedPermission[];
can(permission: string): boolean;
}
const Ability = PermissionAbility;
function createAbility(subject: ResolvedSubject): PermissionAbility;
```
`PermissionAbility` is an immutable wrapper over one `ResolvedSubject` snapshot. `can()` is synchronous and only evaluates the resolved permissions using `canResolved` precedence — no inheritance, adapters or network.
## HTTP
[Section titled “HTTP”](#http)
```ts
class VPermsHttpError extends Error {
readonly status: number;
readonly url: string;
}
interface FetchResolvedSubjectOptions {
fetch?: FetchLike;
requestInit?: RequestInit;
}
function fetchResolvedSubject(
url: string,
options?: FetchResolvedSubjectOptions,
): Promise;
```
Validates the response with `ResolvedSubjectSchema` and throws `VPermsHttpError` on non-2xx responses.
## Other exports
[Section titled “Other exports”](#other-exports)
`DEFAULT_PREFIX` (`"/vperms"`), `FetchLike`, `SubjectResolver`, `resolveSubjectId`, and the re-exported `SubjectType`, `Subject`, `SubjectId`, `Principal`, `ResolvedSubject`, `ResolvedPermission`, `ResolvedPermissionSchema`, `ResolvedSubjectSchema`.
# @vperms/core
> API reference for the pure permission engine.
`@vperms/core` is the adapter-independent engine: types, pattern matching, reconciliation and resolution. It has no runtime dependencies.
## Adapter
[Section titled “Adapter”](#adapter)
### `VeguiPermsAdapter`
[Section titled “VeguiPermsAdapter”](#veguipermsadapter)
Abstract persistence contract. Adapters perform **no** validation, resolution, inheritance or evaluation.
```ts
abstract class VeguiPermsAdapter {
abstract findSubject(
workspaceId: string,
subjectId: SubjectId,
): Promise;
abstract saveSubject(workspaceId: string, subject: Subject): Promise;
abstract deleteSubject(
workspaceId: string,
subjectId: SubjectId,
): Promise;
abstract findSubjectGrants(
workspaceId: string,
subjectId: SubjectId,
): Promise;
abstract grantPermission(
workspaceId: string,
subjectId: SubjectId,
permission: string,
value: boolean,
): Promise;
abstract ungrantPermission(
workspaceId: string,
subjectId: SubjectId,
permission: string,
): Promise;
}
```
* `saveSubject` upserts the subject record.
* `grantPermission` **must** upsert: setting the same permission twice replaces the value.
* Unknown subjects return `null` / `false` rather than throwing.
### `VeguiPermsMemoryAdapter`
[Section titled “VeguiPermsMemoryAdapter”](#veguipermsmemoryadapter)
Map-backed reference implementation. No options, no `migrate()`. Data is lost when the process restarts.
## Types
[Section titled “Types”](#types)
```ts
type SubjectId = string;
enum SubjectType {
User = "user",
Service = "service",
Group = "group",
Anon = "anon",
}
interface Subject {
id: SubjectId;
type: SubjectType;
parents: SubjectId[]; // "!id" negates a default parent
}
interface Principal {
getSubjectId(): SubjectId;
}
interface DefaultParents {
global?: SubjectId[];
byType?: Partial>;
}
interface PermissionGrant {
permission: string;
value: boolean;
subjectId: SubjectId;
workspaceId: string;
}
interface ResolvedPermissionGrant extends PermissionGrant {
depth: number;
layer: number;
}
```
## Matching
[Section titled “Matching”](#matching)
```ts
function matchesPattern(pattern: string, permission: string): boolean;
function matchPermission(
grants: PermissionGrant[],
permission: string,
): boolean | null;
function permissionSpecificity(pattern: string): number;
function exactSegments(pattern: string): number;
```
* `matchesPattern` compares dot-separated segments; `*` matches a single inner segment and a trailing `*` matches any remaining segments, including none.
* `matchPermission` sorts candidates with `compareGrants` and returns the first match, or `null` when nothing matches.
* `permissionSpecificity`: exact segment `+2`, inner `*` `+1`, trailing `*` `+0`.
## Reconciliation
[Section titled “Reconciliation”](#reconciliation)
```ts
function compareGrants(
a: ResolvedPermissionGrant,
b: ResolvedPermissionGrant,
): number;
function depthOf(grant: PermissionGrant): number;
function layerOf(grant: PermissionGrant): number;
```
`compareGrants` is the single source of precedence, in order:
1. layer ascending
2. depth ascending
3. specificity descending
4. exact segments descending
5. deny before allow
6. pattern ascending
7. subject id ascending
The last two make ordering deterministic and independent of insertion order. `depthOf`/`layerOf` return `0` for grants without resolution metadata.
## Layers
[Section titled “Layers”](#layers)
```ts
const EXPLICIT_PARENT_LAYER = 0;
const TYPE_DEFAULT_PARENT_LAYER = 1;
const GLOBAL_DEFAULT_PARENT_LAYER = 2;
const BUILTIN_PERMISSION_LAYER = 3;
```
## Inheritance rules
[Section titled “Inheritance rules”](#inheritance-rules)
```ts
const VIRTUAL_PARENT_NEGATION = "!";
function splitParents(parents: SubjectId[]): {
explicit: SubjectId[];
excluded: Set;
};
function effectiveParentLayers(
subject: Subject,
defaults?: DefaultParents,
): { explicit: SubjectId[]; byType: SubjectId[]; global: SubjectId[] };
```
`effectiveParentLayers` merges the explicit `subject.parents` with the type and global defaults, removing duplicates across layers. Negation only excludes virtual (default) parents; explicit parents are always kept.
## Inheritance resolution
[Section titled “Inheritance resolution”](#inheritance-resolution)
```ts
interface ResolveInheritedPermissionsOptions {
defaultParents?: DefaultParents;
}
function resolveInheritedPermissions(
adapter: VeguiPermsAdapter,
workspaceId: string,
subject: Subject,
options?: ResolveInheritedPermissionsOptions,
): Promise;
```
Walks parents highest-priority-first (explicit, then type defaults, then global defaults; closer depth first). A visited set prevents infinite cycles. A negation directive on the root subject propagates through the whole walk. Default-reached parents contribute their explicit grants plus the applicable defaults, tagged with `max(layer, defaultLayer)`. The result is sorted with `compareGrants`.
## Subject resolution
[Section titled “Subject resolution”](#subject-resolution)
```ts
interface ResolvedPermission {
permission: string;
value: boolean;
weight: number;
}
interface ResolvedSubject {
id: SubjectId;
type: SubjectType;
parents: SubjectId[];
permissions: ResolvedPermission[];
}
function resolveSubjectPermissions(
adapter: VeguiPermsAdapter,
workspaceId: string,
subject: Subject,
options?: ResolveInheritedPermissionsOptions,
): Promise;
function canResolved(
permissions: ResolvedPermission[],
permission: string,
): boolean;
```
Merges direct grants (depth `0`, layer `0`) with inherited grants and a synthesized built-in grant for `SELF_PERMISSIONS_PERMISSION` (value `true`, depth `0`, layer `3`). It sorts, dedupes by permission keeping the first (highest-priority) occurrence and assigns descending `weight`s (`total - index`). `canResolved` returns the value of the highest-weight matching candidate, or `false`.
## Permissions about permissions
[Section titled “Permissions about permissions”](#permissions-about-permissions)
```ts
const SELF_PERMISSIONS_PERMISSION = "vperms.subject.me.permissions";
function subjectPermissionsPermission(subjectId: SubjectId): string;
// "vperms.subject..permissions"
```
These gate the [permissions export endpoint](/guides/exporting/). The built-in self grant has the lowest priority and can be overridden by an explicit deny.
# @vperms/drizzle-adapter
> API reference for the Drizzle SQLite, MySQL and Postgres adapters.
`@vperms/drizzle-adapter` provides Drizzle-backed adapters for SQLite, MySQL and Postgres. Each subpath ships its schema, a driver and the migration files.
## SQLite
[Section titled “SQLite”](#sqlite)
```ts
import {
VeguiPermsSqliteAdapter,
sqliteSchema,
vpermsGrants,
vpermsSubjects,
} from "@vperms/drizzle-adapter/sqlite";
const adapter = new VeguiPermsSqliteAdapter({
db, // BetterSQLite3Database
migrationsFolder: "node_modules/@vperms/drizzle-adapter/migrations/sqlite",
});
await adapter.migrate();
```
## MySQL
[Section titled “MySQL”](#mysql)
```ts
import {
VeguiPermsMysqlAdapter,
mysqlSchema,
vpermsGrants,
vpermsSubjects,
} from "@vperms/drizzle-adapter/mysql";
const adapter = new VeguiPermsMysqlAdapter({ db /* MySql2Database */ });
await adapter.migrate();
```
## Postgres
[Section titled “Postgres”](#postgres)
```ts
import {
VeguiPermsPostgresAdapter,
postgresSchema,
vpermsGrants,
vpermsSubjects,
} from "@vperms/drizzle-adapter/postgres";
const adapter = new VeguiPermsPostgresAdapter({ db /* NodePgDatabase */ });
await adapter.migrate();
```
## Options
[Section titled “Options”](#options)
Every adapter accepts the Drizzle database plus driver options:
```ts
interface DriverOptions {
migrationsFolder?: string;
}
```
`migrate()` calls Drizzle’s `migrate` with the package’s bundled migrations folder. Pass `migrationsFolder` to point at your own copy, for example when bundling.
## Schema
[Section titled “Schema”](#schema)
The SQLite schema (mirrored, with native types, for MySQL and Postgres):
```ts
const vpermsSubjects = sqliteTable("vperms_subjects", {
workspaceId: text("workspace_id").notNull(),
id: text("id").notNull(),
type: text("type").notNull(),
parents: text("parents", { mode: "json" }).$type().notNull(),
});
const vpermsGrants = sqliteTable("vperms_grants", {
workspaceId: text("workspace_id").notNull(),
subjectId: text("subject_id").notNull(),
permission: text("permission").notNull(),
value: integer("value", { mode: "boolean" }).notNull(),
});
```
Subjects are keyed by `(workspace_id, id)` and grants by `(workspace_id, subject_id, permission)`.
## Root exports
[Section titled “Root exports”](#root-exports)
The package root re-exports the SQL base adapter and its record types: `VeguiPermsSqlAdapter`, `GrantRecord`, `SubjectRecord`, `SqlAdapterDriver`.
Regenerate migration files in this repository with `bun run db:generate`.
# Errors
> Error types and their HTTP mappings across integrations.
The `vperms` package defines three errors. Integrations map them to HTTP status codes consistently.
## `PermissionDeniedError`
[Section titled “PermissionDeniedError”](#permissiondeniederror)
```ts
class PermissionDeniedError extends Error {
readonly permission: string;
readonly status: 403;
}
```
Thrown when a caller is not allowed to perform an operation — most notably by [`exportResolvedSubject`](/guides/exporting/) when the caller cannot read the target subject’s permissions.
## `SubjectNotFoundError`
[Section titled “SubjectNotFoundError”](#subjectnotfounderror)
```ts
class SubjectNotFoundError extends Error {
readonly subjectId: string;
readonly status: 404;
}
```
Thrown when a subject has no record, for example from `resolvePermissions` or when exporting permissions for a non-existent subject.
## `InvalidSubjectIdError`
[Section titled “InvalidSubjectIdError”](#invalidsubjectiderror)
```ts
class InvalidSubjectIdError extends Error {
readonly subjectId: string;
readonly status: 400;
}
```
Thrown when a subject id fails schema validation, for example when a malformed id is requested from the export endpoint.
## HTTP mapping
[Section titled “HTTP mapping”](#http-mapping)
| Error | Status | Export/served by |
| ----------------------- | ------ | ------------------------------------ |
| `InvalidSubjectIdError` | `400` | Express/Hono/Nest/Next export routes |
| `PermissionDeniedError` | `403` | All request integrations |
| `SubjectNotFoundError` | `404` | All request integrations |
* **Express** — `403`/`404` via `res.sendStatus`, `400` as JSON `{ error }`.
* **Hono** — `403`/`404` via `c.body(null, ...)`, `400` as `c.json({ error })`.
* **Nest** — guard denials become Nest’s own `403`; the export controller maps the errors above.
* **Next.js** — `NextResponse.json` for success, the same status codes for failures.
Client-side, `VPermsHttpError` wraps any non-2xx response from [`fetchResolvedSubject`](/reference/client/), exposing `status` and `url`.
# @vperms/express
> API reference for the Express integration.
## Middleware
[Section titled “Middleware”](#middleware)
```ts
interface VpermsMiddlewareOptions {
adapter: VeguiPermsAdapter;
resolver: SubjectResolver;
workspace: string | WorkspaceResolver;
defaultParents?: DefaultParents;
permissionsExport?: PermissionsExportOptions;
}
type SubjectResolverResult = SubjectId | Principal | null;
type SubjectResolver =
(req: Request) => SubjectResolverResult | Promise;
type WorkspaceResolver = (req: Request) => string | Promise;
interface PermissionsExportOptions {
path: string;
}
function vpermsMiddleware(
options: VpermsMiddlewareOptions,
): RequestHandler;
```
Hydrates `req.ability`, `req.subject` and `req.kind` on each request, serves the export endpoint when configured, and forwards errors with `next(error)`.
## Request augmentation
[Section titled “Request augmentation”](#request-augmentation)
```ts
interface RequestAbility {
can(permission: string): Promise;
}
declare global {
namespace Express {
interface Request {
ability: RequestAbility;
subject?: Subject;
kind?: SubjectType;
}
}
}
```
## Guards
[Section titled “Guards”](#guards)
```ts
type PermissionBuilder = (
req: Request,
) => string | Promise;
type PermissionInput = string | PermissionBuilder;
function hasPermission(...inputs: PermissionInput[]): RequestHandler; // ALL
function hasAnyPermission(...inputs: PermissionInput[]): RequestHandler; // ANY
```
Both respond `403` via `res.sendStatus(403)` when denied, and throw if `vpermsMiddleware` was not registered first.
## Export
[Section titled “Export”](#export)
`ANONYMOUS_SUBJECT_ID`.
# @vperms/hono
> API reference for the Hono integration.
## Middleware
[Section titled “Middleware”](#middleware)
```ts
interface VPermsVariables {
ability: RequestAbility;
subject: Subject | undefined;
kind: SubjectType | undefined;
}
interface VPermsEnv {
Variables: VPermsVariables;
}
type VPermsContext = Context;
interface VpermsMiddlewareOptions {
adapter: VeguiPermsAdapter;
resolver: SubjectResolver;
workspace: string | WorkspaceResolver;
defaultParents?: DefaultParents;
permissionsExport?: PermissionsExportOptions;
}
type SubjectResolverResult = SubjectId | Principal | null;
type SubjectResolver =
(c: VPermsContext) => SubjectResolverResult | Promise;
type WorkspaceResolver =
(c: VPermsContext) => string | Promise;
interface PermissionsExportOptions {
path: string;
}
function vpermsMiddleware(
options: VpermsMiddlewareOptions,
): MiddlewareHandler;
```
Sets `ability`, `subject` and `kind` on the context, serves the export endpoint when configured, otherwise calls `await next()`.
## Guards
[Section titled “Guards”](#guards)
```ts
type PermissionBuilder = (
c: VPermsContext,
) => string | Promise;
type PermissionInput = string | PermissionBuilder;
function hasPermission(...inputs: PermissionInput[]): MiddlewareHandler; // ALL
function hasAnyPermission(...inputs: PermissionInput[]): MiddlewareHandler; // ANY
```
Both return `c.body(null, 403)` when denied.
## Other exports
[Section titled “Other exports”](#other-exports)
`RequestAbility` (alias of the vperms request `Ability`), `ANONYMOUS_SUBJECT_ID`.
# @vperms/mongodb-adapter
> API reference for the MongoDB adapter.
## Adapter
[Section titled “Adapter”](#adapter)
```ts
import { MongoClient } from "mongodb";
import { VeguiPermsMongoDBAdapter } from "@vperms/mongodb-adapter";
const client = new MongoClient(process.env.MONGODB_URI!);
await client.connect();
const adapter = new VeguiPermsMongoDBAdapter({ db: client.db("app") });
await adapter.migrate();
```
```ts
interface VeguiPermsMongoDBAdapterOptions {
db: Db;
subjectsCollection?: string; // default "vperms_subjects"
grantsCollection?: string; // default "vperms_grants"
}
class VeguiPermsMongoDBAdapter extends VeguiPermsAdapter {
constructor(options: VeguiPermsMongoDBAdapterOptions);
migrate(): Promise;
}
```
`migrate()` idempotently creates both collections and their unique indexes:
* subjects: `{ workspaceId: 1, id: 1 }`
* grants: `{ workspaceId: 1, subjectId: 1, permission: 1 }`
The adapter never opens or closes the MongoDB connection; pass a `Db` from a client you manage.
## Constants
[Section titled “Constants”](#constants)
```ts
const SUBJECTS_COLLECTION = "vperms_subjects";
const GRANTS_COLLECTION = "vperms_grants";
```
## Documents
[Section titled “Documents”](#documents)
```ts
interface SubjectDocument {
workspaceId: string;
id: string;
type: SubjectType;
parents: string[];
}
interface GrantDocument {
workspaceId: string;
subjectId: string;
permission: string;
value: boolean;
}
```
# @vperms/nest
> API reference for the NestJS integration.
## Module
[Section titled “Module”](#module)
```ts
interface VPermsModuleOptions {
adapter: VeguiPermsAdapter;
resolver: SubjectResolver;
workspace: string;
defaultParents?: DefaultParents;
permissionsExport?: PermissionsExportOptions;
}
interface PermissionsExportOptions {
path: string;
}
type SubjectResolverResult = SubjectId | Principal | null;
type SubjectResolver =
(req: Request) => SubjectResolverResult | Promise;
class VPermsModule {
static forRoot(options: VPermsModuleOptions): DynamicModule;
}
```
Global module. Registers the service and `VPermsGuard` (as `APP_GUARD`) and, when `permissionsExport` is set, a controller that serves the export endpoint.
## Tokens
[Section titled “Tokens”](#tokens)
`VPERMS_OPTIONS`, `VPERMS_SERVICE`.
## Decorators
[Section titled “Decorators”](#decorators)
```ts
function Permission(...inputs: PermissionInput[]): MethodDecorator & ClassDecorator;
function AnyPermission(...inputs: PermissionInput[]): MethodDecorator & ClassDecorator;
function Ability(): ParameterDecorator;
function Subject(): ParameterDecorator;
function Kind(): ParameterDecorator;
const PERMISSION_METADATA: symbol;
const ANY_PERMISSION_METADATA: symbol;
type PermissionBuilder = (
req: Request,
) => string | Promise;
type PermissionInput = string | PermissionBuilder;
```
`Permission`/`AnyPermission` only attach metadata. `Ability`/`Subject`/`Kind` read the hydrated request context; `Ability` throws when it is absent.
## Guards
[Section titled “Guards”](#guards)
```ts
class VPermsGuard implements CanActivate {}
abstract class AbilityGuard implements CanActivate {
protected abstract check(
ability: Ability,
context: ExecutionContext,
): boolean | Promise;
protected getAbility(...): Ability;
protected getSubject(...): Subject | undefined;
protected getKind(...): SubjectType | undefined;
}
```
`VPermsGuard` hydrates once per request and evaluates `@Permission` / `@AnyPermission` metadata. Routes without metadata are untouched.
## Request
[Section titled “Request”](#request)
```ts
interface VpermsRequest extends Request {
ability: Ability;
subject?: Subject;
kind?: SubjectType;
}
```
`VPERMS_STATE` is the symbol used to store the hydrated `RequestContext` on the request.
# @vperms/next
> API reference for the Next.js App Router integration.
## Factory
[Section titled “Factory”](#factory)
```ts
interface NextVPermsConfig {
backend: Backend;
prefix?: string; // default "/vperms"
client?: ClientTransport; // default "proxy"
cache?: CacheWrapper;
browserFetchOptions?: RequestInit;
}
type ClientTransport = "direct" | "proxy";
interface NextVPerms {
backend: Backend;
prefix: string;
clientTransport: ClientTransport;
browserConfig: BrowserConfig;
client?: VPermsClient;
getResolvedSubject(subjectId?): Promise;
getAbility(subjectId?): Promise;
Provider: (props: { children?: ReactNode }) => Promise;
Ability: (props: AbilityProps) => Promise;
handlers: NextVPermsHandlers;
}
function createNextVPerms(config: NextVPermsConfig): NextVPerms;
```
## Backends
[Section titled “Backends”](#backends)
```ts
interface NextBackendOptions {
adapter: VeguiPermsAdapter;
workspace: string;
subjectResolver: SubjectResolver;
defaultParents?: DefaultParents;
}
interface NextBackend extends NextBackendOptions {
kind: "next";
}
function nextBackend(options: NextBackendOptions): NextBackend;
interface ExternalBackendOptions {
origin: string;
prefix?: string; // default "/vperms"
subjectResolver?: SubjectResolver;
fetch?: FetchLike;
fetchOptions?: RequestInit;
forwardHeaders?: string[]; // default ["cookie", "authorization"]
}
interface ExternalBackend extends ExternalBackendOptions {
kind: "external";
}
function externalBackend(options: ExternalBackendOptions): ExternalBackend;
type Backend = NextBackend | ExternalBackend;
```
## Shared types
[Section titled “Shared types”](#shared-types)
```ts
type SubjectResolverResult = SubjectId | Principal | null;
type SubjectResolver =
(request?: Request) => SubjectResolverResult | Promise;
interface BrowserConfig {
origin: string;
prefix: string;
fetchOptions?: RequestInit;
}
```
## Handlers
[Section titled “Handlers”](#handlers)
```ts
interface NextRouteContext {
params?:
| Promise<{ path?: string[] }>
| { path?: string[] };
}
interface NextVPermsHandlers {
GET(request: Request, context?: NextRouteContext): Promise;
}
```
Export them from a catch-all route:
```ts
export const { GET } = vperms.handlers;
```
## Client
[Section titled “Client”](#client)
```ts
interface NextClientConfig extends BrowserConfig {
fetch?: FetchLike;
}
function createNextVPerms(config: NextClientConfig): ClientVPerms;
```
`@vperms/next/client` also re-exports everything from `@vperms/react/client`.
# @vperms/react
> API reference for the React server and client bindings.
`@vperms/react` exposes two entry points — `@vperms/react` (server, resolved under the `react-server` condition) and `@vperms/react/client`.
## Server
[Section titled “Server”](#server)
```ts
interface ServerVPermsConfig extends VPermsConfig {
cache?: CacheWrapper;
}
interface ServerVPerms {
client: VPermsClient;
getResolvedSubject(subjectId?): Promise;
getAbility(subjectId?): Promise;
Provider: (props: { children?: ReactNode }) => Promise;
Ability: (props: AbilityProps) => Promise;
}
function createVPerms(
origin: string,
config?: ServerVPermsConfig,
): ServerVPerms;
```
`getResolvedSubject` and `getAbility` are wrapped in `React.cache` by default, so a subject is resolved once per render.
Standalone exports `getResolvedSubject`, `getAbility`, `Provider` and `ServerAbility` (alias `Ability`) use the most recently created instance, or throw `MISSING_INSTANCE_MESSAGE`.
## Client
[Section titled “Client”](#client)
```ts
interface ClientVPerms {
client: VPermsClient;
getResolvedSubject: VPermsClient["getResolvedSubject"];
getAbility: VPermsClient["getAbility"];
Provider: typeof AbilityProvider;
Ability: typeof ClientAbility;
useAbility: typeof useAbility;
}
function createVPerms(origin: string, config?: VPermsConfig): ClientVPerms;
function useAbility(): PermissionAbility;
```
`useAbility` throws `MISSING_PROVIDER_MESSAGE` when used outside `AbilityProvider`.
## Components
[Section titled “Components”](#components)
```ts
interface AbilityProps {
permission?: string;
permissions?: string[];
any?: boolean;
fallback?: ReactNode;
children?: ReactNode;
}
interface AbilityProviderProps {
subject: ResolvedSubject;
children?: ReactNode;
}
function AbilityProvider(props: AbilityProviderProps): JSX.Element;
function ClientAbility(props: AbilityProps): JSX.Element;
const Ability = ClientAbility;
function ServerAbility(props: AbilityProps): Promise;
```
`permission` and `permissions` require **all** permissions by default; set `any` to require at least one. Denied renders `fallback` (or nothing).
## Helpers
[Section titled “Helpers”](#helpers)
```ts
interface AbilityCheck {
permission?: string;
permissions?: string[];
any?: boolean;
}
function abilityAllows(
ability: PermissionAbility,
check: AbilityCheck,
): boolean;
function getAbilityContext(): Context;
```
`abilityAllows` combines `permission` and `permissions`, requires everything by default and short-circuits on `any`.
## Constants
[Section titled “Constants”](#constants)
`MISSING_INSTANCE_MESSAGE` (server), `MISSING_PROVIDER_MESSAGE` (client).
# @vperms/sql-adapter
> API reference for the dialect-agnostic SQL base adapter.
`@vperms/sql-adapter` is a dialect-agnostic base for SQL-backed adapters. It implements `VeguiPermsAdapter` on top of a small `SqlAdapterDriver` that each dialect provides.
## `VeguiPermsSqlAdapter`
[Section titled “VeguiPermsSqlAdapter”](#veguipermssqladapter)
```ts
abstract class VeguiPermsSqlAdapter extends VeguiPermsAdapter {
protected constructor(driver: SqlAdapterDriver);
migrate(): Promise;
}
```
`migrate()` delegates to the driver and must be idempotent. Adapters built on this class (such as the Drizzle adapters) accept a `migrationsFolder` option to override where migration files are read from.
## Driver contract
[Section titled “Driver contract”](#driver-contract)
```ts
interface SqlAdapterDriver {
migrate(): Promise;
findSubject(
workspaceId: string,
subjectId: string,
): Promise;
upsertSubject(record: SubjectRecord): Promise;
deleteSubject(workspaceId: string, subjectId: string): Promise;
findGrants(workspaceId: string, subjectId: string): Promise;
upsertGrant(record: GrantRecord): Promise;
deleteGrant(
workspaceId: string,
subjectId: string,
permission: string,
): Promise;
}
```
## Record types
[Section titled “Record types”](#record-types)
```ts
interface SubjectRecord {
workspaceId: string;
id: string;
type: SubjectType;
parents: string[];
}
interface GrantRecord {
workspaceId: string;
subjectId: string;
permission: string;
value: boolean;
}
```
These records are the storage representation; they are mapped to and from the core `Subject` and `PermissionGrant` types. See [drizzle-adapter](/reference/drizzle-adapter/) for concrete implementations.
# vperms
> API reference for the public service, schemas and errors.
`vperms` is the main package. It re-exports the core engine and adds the validated service, request runtime and export helpers.
## Re-exports
[Section titled “Re-exports”](#re-exports)
Values: `VeguiPermsAdapter`, `VeguiPermsMemoryAdapter`, `SubjectType`, `SELF_PERMISSIONS_PERMISSION`, `subjectPermissionsPermission`, `canResolved`, `resolveSubjectPermissions`, `BUILTIN_PERMISSION_LAYER`.
Types: `Subject`, `SubjectId`, `Principal`, `DefaultParents`, `PermissionGrant`, `ResolvedPermission`, `ResolvedPermissionGrant`, `ResolvedSubject`.
## `VeguiPermsService`
[Section titled “VeguiPermsService”](#veguipermsservice)
```ts
interface VeguiPermsServiceOptions {
adapter: VeguiPermsAdapter;
defaultParents?: DefaultParents;
}
class VeguiPermsService {
constructor(options: VeguiPermsServiceOptions);
can(
workspaceId: string,
subject: SubjectId | Principal,
permission: string,
): Promise;
resolvePermissions(
workspaceId: string,
subject: SubjectId | Principal,
): Promise;
saveSubject(workspaceId: string, subject: Subject): Promise;
deleteSubject(workspaceId: string, subject: Subject): Promise;
setPermission(
workspaceId: string,
subject: SubjectId | Principal,
permission: string,
value: boolean,
): Promise;
unsetPermission(
workspaceId: string,
subject: SubjectId | Principal,
permission: string,
): Promise;
}
```
`can` returns `false` when the subject has no record. It checks direct grants first, then inherited grants, and finally matches `SELF_PERMISSIONS_PERMISSION`. `resolvePermissions` throws `SubjectNotFoundError` when the subject has no record. Every input is validated with Zod; a `Principal` is normalized with `getSubjectId()`.
## Schemas
[Section titled “Schemas”](#schemas)
```ts
const WorkspaceIdSchema; // non-empty string
const SubjectIdSchema; // non-empty string
const PermissionSchema; // dot-separated non-empty segments
const SubjectTypeSchema;
const SubjectSchema;
const PermissionGrantSchema;
const DefaultParentIdSchema; // rejects a leading "!"
const DefaultParentsSchema;
const ResolvedPermissionSchema;
const ResolvedSubjectSchema;
```
Each schema infers a `Validated*` type.
## Request runtime
[Section titled “Request runtime”](#request-runtime)
```ts
interface Ability {
can(permission: string): Promise;
}
const ANONYMOUS_SUBJECT_ID = "anonymous";
function resolveSubjectId(subject: SubjectId | Principal): SubjectId;
function createAbility(
service: VeguiPermsService,
workspaceId: string,
subject: SubjectId | Principal,
): Ability;
function ensureAnonymousSubject(
adapter: VeguiPermsAdapter,
service: VeguiPermsService,
workspaceId: string,
): Promise;
interface RequestContextInput {
adapter: VeguiPermsAdapter;
service: VeguiPermsService;
workspaceId: string;
subject: SubjectId | Principal | null | undefined;
}
interface RequestContext {
id: SubjectId;
subject: Subject | undefined;
kind: SubjectType | undefined;
ability: Ability;
}
function resolveRequestContext(
input: RequestContextInput,
): Promise;
```
`createAbility` memoizes `can()` per permission. `resolveRequestContext` creates the anonymous subject on demand when `subject` is `null` or `undefined`, otherwise loads the subject record.
## Export
[Section titled “Export”](#export)
```ts
function parsePermissionsExportPath(path: string): PermissionsExportPath;
interface PermissionsExportPath {
path: string;
base: string;
routePattern: string;
param: string;
match(pathname: string): PermissionsExportMatch | null;
}
function exportResolvedSubject(
input: ExportResolvedSubjectInput,
): Promise;
```
`exportResolvedSubject` authorizes the caller before returning anything: self requests require `SELF_PERMISSIONS_PERMISSION`, other targets require `subjectPermissionsPermission(target)`. It then loads the subject and resolves its permissions. Throws `PermissionDeniedError`, `SubjectNotFoundError` or `InvalidSubjectIdError`.
## Errors
[Section titled “Errors”](#errors)
### `PermissionDeniedError`
[Section titled “PermissionDeniedError”](#permissiondeniederror)
`status = 403`. Thrown when the caller may not read the requested permissions. Holds `.permission`.
### `SubjectNotFoundError`
[Section titled “SubjectNotFoundError”](#subjectnotfounderror)
`status = 404`. Thrown when the subject has no record. Holds `.subjectId`.
### `InvalidSubjectIdError`
[Section titled “InvalidSubjectIdError”](#invalidsubjectiderror)
`status = 400`. Thrown when a subject id fails validation. Holds `.subjectId`.
See [Errors](/reference/errors/) for the full reference.