| name | polizy-storage |
| description | Storage adapter setup for polizy authorization. Use when configuring InMemory, Prisma, or custom storage adapters, database setup, or performance optimization. |
| license | MIT |
| metadata | {"author":"bratsos","version":"0.5.0","repository":"https://github.com/bratsos/polizy"} |
Polizy Storage
Storage adapters handle persistence of authorization tuples.
When to Apply
- User asks "set up database storage"
- User asks "use Prisma with polizy"
- User asks "create custom storage adapter"
- User asks about "production storage"
- User has performance concerns with authorization
Adapter Comparison
| Feature | InMemoryStorageAdapter | PrismaAdapter |
|---|
| Persistence | No (RAM only) | Yes (database) |
| Multi-instance | No | Yes |
| Setup | Zero config | Prisma model + @@unique required |
| Idempotent writes | Yes | Yes (upsert in a transaction) |
| Pagination | Yes (limit/offset) | Yes (take/skip) |
| Performance | Fastest | Good with indexes |
| Use case | Testing, dev | Production |
Quick Setup
InMemory (Development/Testing)
import { AuthSystem, InMemoryStorageAdapter } from "polizy";
const storage = new InMemoryStorageAdapter();
const authz = new AuthSystem({ storage, schema });
Prisma (Production)
import { AuthSystem } from "polizy";
import { PrismaAdapter } from "polizy/prisma-storage";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const storage = PrismaAdapter(prisma);
const authz = new AuthSystem({ storage, schema });
Requires Prisma model - see PRISMA-ADAPTER.md
InMemoryStorageAdapter
When to Use
- Unit tests
- Development environment
- Single-process applications
- Prototyping
Behavior
- Data stored in JavaScript
Map
- Lost on process restart
- No network latency
- Fastest possible reads
Testing Example
import { describe, it, beforeEach } from "node:test";
import { AuthSystem, InMemoryStorageAdapter, defineSchema } from "polizy";
describe("authorization", () => {
let authz: AuthSystem<typeof schema>;
beforeEach(() => {
const storage = new InMemoryStorageAdapter();
authz = new AuthSystem({ storage, schema });
});
it("grants access correctly", async () => {
await authz.allow({
who: { type: "user", id: "alice" },
toBe: "owner",
onWhat: { type: "doc", id: "doc1" }
});
const result = await authz.check({
who: { type: "user", id: "alice" },
canThey: "edit",
onWhat: { type: "doc", id: "doc1" }
});
assert.strictEqual(result, true);
});
});
PrismaAdapter
When to Use
- Production environments
- Multi-instance deployments
- Need audit trail
- Data must survive restarts
Setup Steps
-
Install dependencies
npm install @prisma/client
npm install -D prisma
-
Add Prisma model (see PRISMA-ADAPTER.md)
-
Run migrations
npx prisma migrate dev --name add_polizy
-
Use adapter
import { PrismaAdapter } from "polizy/prisma-storage";
const storage = PrismaAdapter(prisma);
Storage Interface
All adapters implement the same contract. The bundled InMemoryStorageAdapter
and PrismaAdapter are both held to it by a shared cross-adapter test suite, so
they behave identically:
interface StorageAdapter<S, O> {
write(tuples: InputTuple<S, O>[]): Promise<StoredTuple<S, O>[]>;
delete(filter: {
who?: Subject<S> | AnyObject<O>;
was?: Relation;
onWhat?: AnyObject<O>;
}): Promise<number>;
findTuples(
filter: Partial<InputTuple<S, O>>,
options?: { limit?: number; offset?: number },
): Promise<StoredTuple<S, O>[]>;
findSubjects(object, relation, options?: { subjectType?: S }): Promise<Subject<S>[]>;
findObjects(subject, relation, options?: { objectType?: O }): Promise<AnyObject<O>[]>;
}
See CUSTOM-ADAPTERS.md for the exact
idempotent-write, delete, and pagination semantics every adapter must honor.
Role catalog (runtime roles)
When you use RoleRegistry for runtime custom roles, an optional
RoleCatalogStore tracks role existence + labels. This is metadata only —
the engine never reads the catalog. Capabilities (cap_<action> tuples) and
assignments (assignee membership tuples) live as ordinary tuples in the
StorageAdapter, so a role is fully functional even with no catalog at all. The
catalog exists so permission-less roles stay listable (e.g. a freshly-created
role with no caps yet still shows up in listRoles/permissionMatrix).
import { AuthSystem, InMemoryStorageAdapter, InMemoryRoleCatalog, RoleRegistry } from "polizy";
const authz = new AuthSystem({ storage: new InMemoryStorageAdapter(), schema });
const roles = new RoleRegistry(authz, schema, { catalog: new InMemoryRoleCatalog() });
import { AuthSystem, RoleRegistry } from "polizy";
import { PrismaAdapter, PrismaRoleCatalog } from "polizy/prisma-storage";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const authz = new AuthSystem({ storage: PrismaAdapter(prisma), schema });
const roles = new RoleRegistry(authz, schema, { catalog: PrismaRoleCatalog(prisma) });
PrismaRoleCatalog needs the new optional PolizyRole Prisma model — see
PRISMA-ADAPTER.md. The catalog is independent of
the tuple StorageAdapter: you can pair an in-memory catalog with a Prisma
adapter or vice versa, since the engine resolves roles purely from tuples.
Common Patterns
Shared Storage Instance
import { InMemoryStorageAdapter } from "polizy";
export const storage = new InMemoryStorageAdapter();
import { AuthSystem } from "polizy";
import { storage } from "./storage";
import { schema } from "./schema";
export const authz = new AuthSystem({ storage, schema });
Environment-Based Selection
import { AuthSystem, InMemoryStorageAdapter } from "polizy";
import { PrismaAdapter } from "polizy/prisma-storage";
import { PrismaClient } from "@prisma/client";
function createStorage() {
if (process.env.NODE_ENV === "test") {
return new InMemoryStorageAdapter();
}
const prisma = new PrismaClient();
return PrismaAdapter(prisma);
}
const storage = createStorage();
export const authz = new AuthSystem({ storage, schema });
Performance Considerations
| Concern | Solution |
|---|
| Slow reads | Add indexes on (subjectType, subjectId, relation) and (objectType, objectId, relation) |
| Too many queries | Reduce group nesting depth |
| Large tuple counts | Periodic cleanup of expired tuples |
Large listTuples/listAccessibleObjects results | Page with { limit, offset } |
| Bulk grants | Use allowMany(grants[]) or storage.write(tuples[]) |
check() is memoized per call (no exponential blow-up on deep/wide group or
hierarchy graphs; cycles terminate), and listAccessibleObjects scales with the
subject's reachable set rather than scanning the whole tuple table. See
PERFORMANCE.md.
Common Issues
| Issue | Solution |
|---|
| Data lost on restart | Switch from InMemory to Prisma |
| "Table doesn't exist" | Run npx prisma migrate deploy |
new PrismaStorageAdapter(...) fails | It's a factory from polizy/prisma-storage — call it, don't new it |
| Upsert/idempotent write errors | Add @@unique([subjectType, subjectId, relation, objectType, objectId]) and migrate |
Migration fails on the new @@unique | Dedupe 0.2.x and earlier duplicate rows first (see migration guide) |
| Time-based grants threw on Prisma | Fixed in 0.3.0 — conditions revive validSince/validUntil to Date on read |
| Revocation removed too much | Fixed in 0.3.0 — both adapters keep an explicit who and no longer over-delete |
| Slow checks | Reduce group/hierarchy depth; index the two hot paths |
| Memory growing | Clean up expired conditional tuples |
References
Related Skills