Skip to content

Subjects

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.

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 for how parents are resolved and how the ! negation prefix works.

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.
import { SubjectType } from "vperms";
const group = {
id: "developers",
type: SubjectType.Group,
parents: [],
};

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:

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.

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:

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.

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:

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 for the request context.

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).