prisma adapter
Persist policies, roles, assignments, and subject attributes through Prisma Client. Ships a reference schema for any Prisma-supported database.
Install
import { PrismaAdapter } from '@gentleduck/iam/adapters/prisma'import { PrismaAdapter } from '@gentleduck/iam/adapters/prisma'Stores duck-iam state in your database through Prisma Client. Requires four models in your schema.prisma.
bun add @prisma/client
bun add -D prismabun add @prisma/client
bun add -D prismaRequired schema
Add these models to your schema.prisma:
model AccessPolicy {
id String @id
name String
description String?
version Int @default(1)
algorithm String
rules Json
targets Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("access_policies")
}
model AccessRole {
id String @id
name String
description String?
permissions Json
inherits String[]
scope String?
metadata Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
assignments AccessAssignment[]
@@map("access_roles")
}
model AccessAssignment {
id String @id @default(cuid())
subjectId String
roleId String
scope String?
role AccessRole @relation(fields: [roleId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([subjectId, roleId, scope])
@@index([subjectId])
@@map("access_assignments")
}
model AccessSubjectAttr {
subjectId String @id
data Json
updatedAt DateTime @updatedAt
@@map("access_subject_attrs")
}model AccessPolicy {
id String @id
name String
description String?
version Int @default(1)
algorithm String
rules Json
targets Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("access_policies")
}
model AccessRole {
id String @id
name String
description String?
permissions Json
inherits String[]
scope String?
metadata Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
assignments AccessAssignment[]
@@map("access_roles")
}
model AccessAssignment {
id String @id @default(cuid())
subjectId String
roleId String
scope String?
role AccessRole @relation(fields: [roleId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([subjectId, roleId, scope])
@@index([subjectId])
@@map("access_assignments")
}
model AccessSubjectAttr {
subjectId String @id
data Json
updatedAt DateTime @updatedAt
@@map("access_subject_attrs")
}A copy lives at node_modules/@gentleduck/iam/src/adapters/prisma/schema.prisma for reference.
Run after editing:
bunx prisma migrate dev --name add-iam-models
bunx prisma generatebunx prisma migrate dev --name add-iam-models
bunx prisma generateUsage
import { PrismaClient } from '@prisma/client'
import { PrismaAdapter } from '@gentleduck/iam/adapters/prisma'
import { Engine } from '@gentleduck/iam'
const prisma = new PrismaClient()
const adapter = new PrismaAdapter(prisma)
const engine = new Engine({ adapter, cacheTTL: 60 })import { PrismaClient } from '@prisma/client'
import { PrismaAdapter } from '@gentleduck/iam/adapters/prisma'
import { Engine } from '@gentleduck/iam'
const prisma = new PrismaClient()
const adapter = new PrismaAdapter(prisma)
const engine = new Engine({ adapter, cacheTTL: 60 })The adapter uses upsert for save operations, so calling savePolicy or saveRole with an existing ID updates the record rather than throwing a conflict error.
How it maps
| Adapter method | Prisma operation |
|---|---|
listPolicies() | accessPolicy.findMany() |
getPolicy(id) | accessPolicy.findUnique({ where: { id } }) |
savePolicy(p) | accessPolicy.upsert(...) |
deletePolicy(id) | accessPolicy.delete({ where: { id } }) |
listRoles() | accessRole.findMany() |
getRole(id) | accessRole.findUnique({ where: { id } }) |
saveRole(r) | accessRole.upsert(...) |
deleteRole(id) | accessRole.delete({ where: { id } }) |
getSubjectRoles(id) | accessAssignment.findMany({ where: { subjectId } }) |
getSubjectScopedRoles(id) | Same query, filtered for non-null scope |
assignRole(id, role, scope?) | accessAssignment.create(...) |
revokeRole(id, role, scope?) | accessAssignment.deleteMany(...) |
getSubjectAttributes(id) | accessSubjectAttr.findUnique(...) |
setSubjectAttributes(id, attrs) | accessSubjectAttr.upsert(...) (merges with existing) |
Notes & caveats
assignRoleis not idempotent — it usescreate, so a duplicate(subjectId, roleId, scope)will throw on the unique constraint. Catch the conflict in your application or checkgetSubjectRolesfirst.setSubjectAttributesis read-merge-write — concurrent writes can lose updates. For high-contention attribute writes, wrap in a Prisma transaction withSELECT ... FOR UPDATEsemantics.- JSON columns — Prisma handles
Jsonnatively. No manual stringify needed.
When to use
- Production apps already on Prisma
- Multi-database support (Postgres, MySQL, SQLite, MongoDB, SQL Server, CockroachDB)
- Want type-safe SQL via Prisma Client
For pure SQL with stronger types and zero runtime, see Drizzle.