StorageAdapter Reference
This page provides the formal API specification and operational contracts for polizy storage adapters.
Interface Specification
The StorageAdapter interface defines the boundary between the polizy execution engine and the persistence layer. Any storage adapter passed to the AuthSystem must satisfy these methods.
| Method | Type Signature | Purpose |
|---|---|---|
| write | write(tuples: InputTuple<S, O>[]): Promise<StoredTuple<S, O>[]> | Writes a list of tuples to storage, enforcing unique compound constraints. Returns the saved tuples with IDs in the original order. |
| delete | delete(filter: { who?: Subject<S> or AnyObject<O>; was?: Relation; onWhat?: AnyObject<O> }): Promise<number> | Deletes all stored tuples matching the active filter properties. Returns the number of deleted records. |
| findTuples | findTuples(filter: Partial<InputTuple<S, O>>, options?: { limit?: number; offset?: number }): Promise<StoredTuple<S, O>[]> | Returns tuples matching the exact filter properties. Supports stable pagination order. |
| findSubjects | findSubjects(object: AnyObject<O>, relation: Relation, options?: { subjectType?: S }): Promise<Subject<S>[]> | Returns all unique subjects that have a direct relationship to the target object. |
| findObjects | findObjects(subject: Subject<S>, relation: Relation, options?: { objectType?: O }): Promise<AnyObject<O>[]> | Returns all unique objects that the subject has a direct relationship to. |
| withSnapshot | withSnapshot?<T>(fn: (reader: ReadOnlyStorage<S, O>) => Promise<T>): Promise<T> | Optional. Executes a callback within a consistent read-only transaction (snapshot). |
In the signatures above, S represents the union of valid subject types, and O represents the union of valid object types, as defined in your schema.
Core Types
Subject
Represents the actor initiating an action.
type Subject<S extends string = string> = {
type: S;
id: string;
};
AnyObject
Represents the target resource.
type AnyObject<O extends string = string> = {
type: O;
id: string;
};
TupleSubject
A union type allowing a subject or another object to act in the subject position of a tuple (e.g., for nested groups or folders).
type TupleSubject<S extends string, O extends string> = Subject<S> | AnyObject<O>;
StoredTuple
The complete database representation of a stored relationship fact.
type StoredTuple<S extends string, O extends string> = {
id: string;
subject: TupleSubject<S, O>;
relation: string;
object: AnyObject<O>;
condition?: Condition;
};
InputTuple
Used when writing new tuples. It is identical to StoredTuple but lacks the unique database-generated id.
type InputTuple<S extends string, O extends string> = Omit<StoredTuple<S, O>, "id">;
ReadOnlyStorage
The slice of StorageAdapter containing only the query methods. This is passed to the callback function in withSnapshot.
type ReadOnlyStorage<S extends string, O extends string> = Pick<
StorageAdapter<S, O>,
"findTuples" | "findSubjects" | "findObjects"
>;
Database Schema Specification
For SQL-backed engines, your tuples table must satisfy the following constraints. The Prisma schema representation is the canonical specification:
model PolizyTuple {
id String @id @default(cuid())
subjectType String
subjectId String
relation String
objectType String
objectId String
condition Json?
@@unique([subjectType, subjectId, relation, objectType, objectId])
@@index([subjectType, subjectId, relation])
@@index([objectType, objectId, relation])
}
Key Specifications
- Compound Unique Index
A unique constraint on[subjectType, subjectId, relation, objectType, objectId]is required to make grants idempotent and prevent duplicate facts. - Subject Index
An index on[subjectType, subjectId, relation]is required to optimize outbound subject lookups (resolving direct permission checks and listing objects). - Object Index
An index on[objectType, objectId, relation]is required to optimize outbound object lookups (resolving inheritance checks and listing subjects). - Condition Store
Theconditioncolumn is optional and stores JSON context rules (such as time-based validity or environmental checks).
Operational Contracts
Write Idempotency Contract
When the engine requests a write of input tuples:
- The store must check if a tuple matching the combination of
(subject, relation, object)already exists. - If it exists, the adapter must not insert a new row. If a new
conditionis specified in the input, the adapter must update the existing record's condition block. Ifconditionis undefined in the input, the adapter must leave the existing condition untouched. - The return value must be a list of
StoredTupleobjects containing the generated database IDs, preserving the exact order of the input array.
Delete Matching Contract
The deletion logic uses a logical AND between all provided fields:
who: Maps to the subject (subjectTypeandsubjectId).was: Maps to the relation (relation).onWhat: Maps to either the object (objectTypeandobjectId) OR the subject (subjectTypeandsubjectId).
The matching logic must evaluate to:
(who == null OR (subject.type == who.type AND subject.id == who.id)) AND
(was == null OR relation == was) AND
(onWhat == null OR (object.type == onWhat.type AND object.id == onWhat.id) OR (subject.type == onWhat.type AND subject.id == onWhat.id))
Snapshot Consistency Contract
- When
withSnapshotis called, the adapter must open a transaction/snapshot block at the database level. - All queries called on the provided
readerparameter duringwithSnapshotmust execute within that isolation level. - For relational databases, this should map to
RepeatableRead(PostgreSQL) or similar read-only snapshot transaction levels.