Skip to main content

adapters overview

Storage layer for duck-iam — pick a built-in adapter (Memory, Prisma, Drizzle, Redis, HTTP) or implement the Adapter interface for custom backends.

What is an adapter?

Adapters are duck-iam's storage layer. They implement the Adapter interface, which combines three stores:

Loading diagram...

  • PolicyStore — CRUD for access policies (rules, conditions, combining algorithms)
  • RoleStore — CRUD for roles (permissions, inheritance, scopes)
  • SubjectStore — role assignments, scoped roles, and subject attributes

Every adapter is fully typed against your app's action, resource, role, and scope unions.

import { Engine } from '@gentleduck/iam'
 
const engine = new Engine({
  adapter: yourAdapter,
  defaultEffect: 'deny',
  cacheTTL: 60,
})
import { Engine } from '@gentleduck/iam'
 
const engine = new Engine({
  adapter: yourAdapter,
  defaultEffect: 'deny',
  cacheTTL: 60,
})

Built-in adapters

AdapterUse casePersistencePeer dep
MemoryDev, testing, prototypingNone (in-process)None
PrismaProduction w/ PrismaAny Prisma-supported DB@prisma/client
DrizzleProduction w/ DrizzlePG, MySQL, SQLitedrizzle-orm
RedisDistributed deploysRedis-compatible KVioredis or redis
HTTPMicroservices splitRemote APINone (uses fetch)
CustomAny other backendYour choiceYour choice

All adapters are interchangeable. Start with MemoryAdapter during development and switch to a database adapter in production without changing engine, builder, or middleware code.


How adapters fit in

import { Engine } from '@gentleduck/iam'
import { MemoryAdapter } from '@gentleduck/iam/adapters/memory'
 
// 1. Pick a storage backend
const adapter = new MemoryAdapter({ /* seed */ })
 
// 2. Wrap it in an engine
const engine = new Engine({ adapter, defaultEffect: 'deny' })
 
// 3. Use the engine — same API regardless of adapter
const allowed = await engine.can('user-1', 'read', { type: 'post', attributes: {} })
import { Engine } from '@gentleduck/iam'
import { MemoryAdapter } from '@gentleduck/iam/adapters/memory'
 
// 1. Pick a storage backend
const adapter = new MemoryAdapter({ /* seed */ })
 
// 2. Wrap it in an engine
const engine = new Engine({ adapter, defaultEffect: 'deny' })
 
// 3. Use the engine — same API regardless of adapter
const allowed = await engine.can('user-1', 'read', { type: 'post', attributes: {} })

The engine reads from the adapter on cache miss, then caches policies, roles, and resolved subjects in an LRU with the configured cacheTTL. Adapter writes via engine.admin.* automatically invalidate the relevant caches.

See Comparison & FAQ for selection guidance and answers to common adapter questions.