Writing a custom adapter
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”import type { PermissionGrant, Subject } from "vperms";import { VeguiPermsAdapter } from "vperms";
export class MyAdapter extends VeguiPermsAdapter { async findSubject(workspaceId: string, subjectId: string): Promise<Subject | null> { // load, or return null }
async saveSubject(workspaceId: string, subject: Subject): Promise<Subject> { // create or update, then return the stored subject }
async deleteSubject(workspaceId: string, subjectId: string): Promise<boolean> { // return true when a record was removed }
async findSubjectGrants(workspaceId: string, subjectId: string): Promise<PermissionGrant[]> { // return [] when the subject has no grants }
async grantPermission( workspaceId: string, subjectId: string, permission: string, value: boolean, ): Promise<PermissionGrant> { // MUST upsert and return the stored grant }
async ungrantPermission( workspaceId: string, subjectId: string, permission: string, ): Promise<boolean> { // return true when a grant was removed }}- Upsert grants. For a given
(workspaceId, subjectId, permission)there is at most one grant; a newvaluereplaces the previous one. - Do no validation. Every value has already been validated by
VeguiPermsServicewith Zod.PermissionGrantincludessubjectIdandworkspaceId. - 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.
deleteSubjectandungrantPermissionreturnfalsewhen there was nothing to remove.
Migrations
Section titled “Migrations”VeguiPerms has no implicit schema management. Database adapters expose an explicit
migrate() that the application calls once at startup:
const adapter = new VeguiPermsSqliteAdapter({ db });await adapter.migrate(); // explicit, idempotentFollow 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”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 are built on it.
Testing your adapter
Section titled “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).