Skip to main content

Integrations

Where duck-iam plugs into the rest of your stack - adapters, server middleware, client SDKs, cross-instance invalidation, and metrics

Everything outside the policy engine lives here. @gentleduck/iam ships five families of integration, each on its own npm subpath and each replaceable: the adapter decides where policies live, the server helpers turn HTTP requests into decisions, the client SDKs render a pre-computed snapshot, the invalidator keeps a fleet's caches in step, and the metrics aggregator turns decision events into percentiles.

The five families

Integrations are sliced by direction: which way the data moves relative to the engine.

Loading diagram...

adapters/* is the only family the engine cannot run without. server/* sits in front of the engine and translates a framework's request object into a can / authorize call; client/* sits behind it and consumes a snapshot the server already computed. observability/metrics is a pure sink on the onMetrics hook. invalidators/redis is the only one that flows both ways: it publishes local admin writes and applies remote ones.

SubpathDirectionWhat it doesPages
@gentleduck/iam/adapters/*inboundWhere policies, roles, assignments, and subject attributes are storedAdapters overview
@gentleduck/iam/server/*request edgeExpress / Hono / Next / Nest middleware and admin routersServer overview
@gentleduck/iam/client/*outboundReact / Vue / vanilla SDKs over a serialised permission mapClient overview
@gentleduck/iam/invalidators/redissidewaysSigned pub/sub cache invalidation across instancesRedis invalidator
@gentleduck/iam/observability/metricsoutboundRolling p50 / p95 / p99 and allow / deny / fail-open countersMetrics aggregator

Every integration and its page

Adapters

ExportSubpathPage
IamMemoryAdapter, iamMemoryAdapteradapters/memoryMemory adapter
IamFileAdapter, iamFileAdapteradapters/fileFile adapter
IamPrismaAdapter, iamPrismaAdapteradapters/prismaPrisma adapter
IamDrizzleAdapter, iamDrizzleAdapteradapters/drizzle, adapters/drizzle/pg, adapters/drizzle/mysql, adapters/drizzle/sqliteDrizzle adapter
IamRedisAdapter, iamRedisAdapteradapters/redisRedis adapter
IamHttpAdapter, iamHttpAdapteradapters/httpHTTP adapter
IamAdapter.IAdapter (the interface)root entry, type-onlyCustom adapter

Not sure which one? Choosing an adapter has the feature matrix and the decision tree.

Server

FrameworkSubpathPage
Framework-agnostic helpersserverGeneric helpers
Next.js app routerserver/nextNext.js
Expressserver/expressExpress
Honoserver/honoHono
NestJSserver/nestNestJS

Client

TargetSubpathPage
The wire shape itselftype-onlyPermissionMap reference
Vanilla JSclientVanilla JS
Reactclient/reactReact
Vueclient/vueVue

Invalidation and observability

ExportSubpathPage
createIamRedisInvalidatorinvalidators/redisRedis invalidator
iamCreateMetricsAggregatorobservability/metricsMetrics aggregator

Wiring all five at once

A production deployment that uses one of each. Every symbol below is a real export; check each linked page for its full option table.

import { and, eq, isNull, or } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/node-postgres'
import { IamEngine } from '@gentleduck/iam/core'
import { IamDrizzleAdapter } from '@gentleduck/iam/adapters/drizzle'
import { iamAssignments, iamPolicies, iamRoles, iamSubjectAttrs } from '@gentleduck/iam/adapters/drizzle/pg'
import { iamAdminRouter, iamGuard } from '@gentleduck/iam/server/express'
import { createIamRedisInvalidator } from '@gentleduck/iam/invalidators/redis'
import { iamCreateMetricsAggregator } from '@gentleduck/iam/observability/metrics'

const db = drizzle(pool)

// 1. inbound: where the authorization data lives
const adapter = new IamDrizzleAdapter({
  db,
  tables: { policies: iamPolicies, roles: iamRoles, assignments: iamAssignments, attrs: iamSubjectAttrs },
  ops: { and, eq, isNull, or }, // isNull and or are optional; omitting either warns and takes a slower path
})

// 2. sideways: every node drops its caches when any node writes
const invalidator = createIamRedisInvalidator({
  client: redisPubSub,
  secret: process.env.IAM_INVALIDATE_SECRET,
})

// 3. outbound: rolling latency + allow/deny counters
const metrics = iamCreateMetricsAggregator({ sampleSize: 2000 })

const engine = new IamEngine({
  adapter,
  invalidator,
  defaultEffect: 'deny',
  mode: 'production',
  hooks: { onMetrics: metrics.record },
})

await engine.preload() // warm the policy and role caches before first request

// 4. request edge
app.delete('/posts/:id', iamGuard(engine, 'delete', 'post'), handler)
app.use('/admin/iam', iamAdminRouter(engine, { authorize: (req) => req.user?.role === 'admin' })(Router))

app.get('/healthz', async (_, res) => res.json(await engine.healthCheck()))
app.get('/metrics', (_, res) => res.json(metrics.snapshot()))

How the pieces sit in a fleet

Two app instances sharing one database and one Redis. The adapter is the source of truth; Redis carries only invalidation traffic.

Loading diagram...

Only the cache-miss edges reach the adapter: with cacheTTL at its default of 60 seconds, a warm node answers from its own LRU. The publish edge exists so Node B does not serve a stale decision for up to a full TTL after Node A revokes a role. Each node aggregates its own metrics, so scrape both.

What is not here

See also