Adapters: Persistence adapters and the custom adapter guide.
# 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.
# 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`).