Skip to main content

prisma adapter

Persist policies, roles, assignments, and subject attributes through Prisma Client. Ships a reference schema for any Prisma-supported database.

Install

import { IamPrismaAdapter } 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 prisma

Required 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")
}

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 generate

Usage

import { PrismaClient } from '@prisma/client'
import { IamPrismaAdapter } from '@gentleduck/iam/adapters/prisma'
import { IamEngine } from '@gentleduck/iam'

const prisma = new PrismaClient()
const adapter = new IamPrismaAdapter(prisma)

const engine = new IamEngine({ 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 methodPrisma 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

  • assignRole is not idempotent - it uses create, so a duplicate (subjectId, roleId, scope) will throw on the unique constraint. Catch the conflict in your application or check getSubjectRoles first.
  • setSubjectAttributes is read-merge-write - concurrent writes can lose updates. For high-contention attribute writes, wrap in a Prisma transaction with SELECT ... FOR UPDATE semantics.
  • JSON columns - Prisma handles Json natively. 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.