Skip to main content

server middleware overview

Server-side integrations for Express, NestJS, Hono, and Next.js — extract identity, infer action and resource, call engine.can().

How it works

duck-iam ships server integrations for Express, NestJS, Hono, and Next.js. Every one follows the same pattern: extract the user identity, map the HTTP method to an action, infer the resource from the route, and call engine.can().

Loading diagram...

Every integration is a thin adapter over engine.can(). No runtime framework dependency — duck-iam defines its own minimal type interfaces, so no extra packages sneak in.


Pick a framework

FrameworkDocSubpath
Expressserver/express@gentleduck/iam/server/express
Honoserver/hono@gentleduck/iam/server/hono
NestJSserver/nest@gentleduck/iam/server/nest
Next.js App Routerserver/next@gentleduck/iam/server/next
Generic helpersserver/generic@gentleduck/iam/server/generic

All five share the same building blocks: identity extraction, action mapping, resource inference, scope resolution, and decision callbacks. The framework-specific docs cover each one's idioms.


HTTP method mapping

The default method-to-action map shared by every integration:

const METHOD_ACTION_MAP = {
  GET: 'read',
  HEAD: 'read',
  OPTIONS: 'read',
  POST: 'create',
  PUT: 'update',
  PATCH: 'update',
  DELETE: 'delete',
}
const METHOD_ACTION_MAP = {
  GET: 'read',
  HEAD: 'read',
  OPTIONS: 'read',
  POST: 'create',
  PUT: 'update',
  PATCH: 'update',
  DELETE: 'delete',
}

Override per-route or globally with the getAction option.


Common patterns

Combine global + per-route protection

Use global middleware for broad protection, then layer per-route guards for endpoints with different rules.

// Express example
app.use('/api', accessMiddleware(engine, { getUserId: (req) => req.user?.id }))
 
// Override for a specific route that needs a different scope
app.delete('/api/admin/users/:id',
  guard(engine, 'manage', 'user', { scope: 'admin' }),
  deleteUserHandler
)
// Express example
app.use('/api', accessMiddleware(engine, { getUserId: (req) => req.user?.id }))
 
// Override for a specific route that needs a different scope
app.delete('/api/admin/users/:id',
  guard(engine, 'manage', 'user', { scope: 'admin' }),
  deleteUserHandler
)

Custom action mapping

Override the default HTTP method mapping for non-standard action patterns.

const middleware = accessMiddleware(engine, {
  getUserId: (req) => req.user?.id,
  getAction: (req) => {
    // POST /api/posts/:id/publish -> "publish" action
    if (req.method === 'POST' && req.path.endsWith('/publish')) {
      return 'publish'
    }
    return METHOD_ACTION_MAP[req.method] ?? 'read'
  },
})
const middleware = accessMiddleware(engine, {
  getUserId: (req) => req.user?.id,
  getAction: (req) => {
    // POST /api/posts/:id/publish -> "publish" action
    if (req.method === 'POST' && req.path.endsWith('/publish')) {
      return 'publish'
    }
    return METHOD_ACTION_MAP[req.method] ?? 'read'
  },
})

Server-driven client permissions

  1. Generate a PermissionMap on the server using engine.permissions() or getPermissions() (Next.js helper)
  2. Pass the map to the client via props, response body, or server-rendered HTML
  3. Hydrate AccessProvider (React), plugin (Vue), or AccessClient (vanilla) on the client
  4. Client-side checks become instant lookups with no extra network requests
const permissions = await engine.permissions(userId, [
  { action: 'create', resource: 'post' },
  { action: 'delete', resource: 'post' },
  { action: 'manage', resource: 'team' },
])
const permissions = await engine.permissions(userId, [
  { action: 'create', resource: 'post' },
  { action: 'delete', resource: 'post' },
  { action: 'manage', resource: 'team' },
])

See client integrations for hydration on the consumer side.


Default failure responses

ConditionStatusOverride
Missing user identity401None — fail closed
Engine returns deny403onDenied
Engine throws500onError

The engine itself never throws from authorize(). It catches internal errors, calls the onError engine hook, and returns a deny decision. The server integration try/catch is an extra safety net for adapter connection failures during subject resolution.


FAQ