Skip to main content
Search...

installation

Install duck-iam, configure a storage adapter, and run your first permission check in under five minutes.

Prerequisites

Before installing, make sure you have:

  • Node.js 18 or newer -- check with node --version.
  • TypeScript 5.0+ -- duck-iam relies on const type parameters and satisfies operator.
  • A package manager: npm, bun, or pnpm.

Install duck-iam

duck-iam is a single package. The core engine, memory adapter, type builders, and all integration entry points are included.

Install the package

# npm
npm install @gentleduck/iam
 
# bun
bun add @gentleduck/iam
 
# pnpm
pnpm add @gentleduck/iam
# npm
npm install @gentleduck/iam
 
# bun
bun add @gentleduck/iam
 
# pnpm
pnpm add @gentleduck/iam

Basic setup

Create your access control configuration with roles and an engine:

src/lib/access.ts
import { createAccessConfig, MemoryAdapter } from "@gentleduck/iam";
 
// 1. Define your application's actions, resources, and scopes
const access = createAccessConfig({
  actions: ["create", "read", "update", "delete", "manage"],
  resources: ["post", "comment", "user", "team"],
  scopes: ["org"],
} as const);
 
// 2. Define roles using typed builders
const viewer = access
  .defineRole("viewer")
  .grantRead("post", "comment")
  .build();
 
const editor = access
  .defineRole("editor")
  .inherits("viewer")
  .grant("create", "post")
  .grant("update", "post")
  .grant("create", "comment")
  .build();
 
const admin = access
  .defineRole("admin")
  .inherits("editor")
  .grant("delete", "post")
  .grant("delete", "comment")
  .grantCRUD("user")
  .grant("manage", "team")
  .build();
 
// 3. Create an adapter
const adapter = new MemoryAdapter({
  roles: [viewer, editor, admin],
  assignments: {
    "user-1": ["admin"],
    "user-2": ["editor"],
    "user-3": ["viewer"],
  },
});
 
// 4. Create the engine
export const engine = access.createEngine({ adapter });
src/lib/access.ts
import { createAccessConfig, MemoryAdapter } from "@gentleduck/iam";
 
// 1. Define your application's actions, resources, and scopes
const access = createAccessConfig({
  actions: ["create", "read", "update", "delete", "manage"],
  resources: ["post", "comment", "user", "team"],
  scopes: ["org"],
} as const);
 
// 2. Define roles using typed builders
const viewer = access
  .defineRole("viewer")
  .grantRead("post", "comment")
  .build();
 
const editor = access
  .defineRole("editor")
  .inherits("viewer")
  .grant("create", "post")
  .grant("update", "post")
  .grant("create", "comment")
  .build();
 
const admin = access
  .defineRole("admin")
  .inherits("editor")
  .grant("delete", "post")
  .grant("delete", "comment")
  .grantCRUD("user")
  .grant("manage", "team")
  .build();
 
// 3. Create an adapter
const adapter = new MemoryAdapter({
  roles: [viewer, editor, admin],
  assignments: {
    "user-1": ["admin"],
    "user-2": ["editor"],
    "user-3": ["viewer"],
  },
});
 
// 4. Create the engine
export const engine = access.createEngine({ adapter });

Check permissions

// Simple boolean check
const canCreate = await engine.can(
  "user-2",
  "create",
  { type: "post", attributes: {} }
);
// -> true (editor can create posts)
 
const canDelete = await engine.can(
  "user-2",
  "delete",
  { type: "post", attributes: {} }
);
// -> false (editor cannot delete posts)
 
// Full decision with metadata
const decision = await engine.check(
  "user-2",
  "delete",
  { type: "post", attributes: {} }
);
// -> { allowed: false, effect: "deny", reason: "...", duration: 0.12, ... }
 
// Debug why a permission was granted or denied
const trace = await engine.explain(
  "user-2",
  "delete",
  { type: "post", attributes: {} }
);
// -> detailed trace of every policy, rule, and condition evaluated
// Simple boolean check
const canCreate = await engine.can(
  "user-2",
  "create",
  { type: "post", attributes: {} }
);
// -> true (editor can create posts)
 
const canDelete = await engine.can(
  "user-2",
  "delete",
  { type: "post", attributes: {} }
);
// -> false (editor cannot delete posts)
 
// Full decision with metadata
const decision = await engine.check(
  "user-2",
  "delete",
  { type: "post", attributes: {} }
);
// -> { allowed: false, effect: "deny", reason: "...", duration: 0.12, ... }
 
// Debug why a permission was granted or denied
const trace = await engine.explain(
  "user-2",
  "delete",
  { type: "post", attributes: {} }
);
// -> detailed trace of every policy, rule, and condition evaluated

Batch permission checks

Load many permissions at once for a user -- useful for hydrating client-side UI:

const permissions = await engine.permissions("user-2", [
  { action: "create", resource: "post" },
  { action: "update", resource: "post" },
  { action: "delete", resource: "post" },
  { action: "manage", resource: "team" },
]);
// -> { "create:post": true, "update:post": true, "delete:post": false, "manage:team": false }
const permissions = await engine.permissions("user-2", [
  { action: "create", resource: "post" },
  { action: "update", resource: "post" },
  { action: "delete", resource: "post" },
  { action: "manage", resource: "team" },
]);
// -> { "create:post": true, "update:post": true, "delete:post": false, "manage:team": false }

Optional Peer Dependencies

duck-iam has optional peer dependencies for framework integrations. Install only what you use:

IntegrationPeer dependencyInstall
React clientreact >= 18.0.0npm install react
Vue clientvue >= 3.3.0npm install vue
Prisma adapter@prisma/client >= 5.0.0npm install @prisma/client

The core engine, memory adapter, Drizzle adapter, HTTP adapter, and all server integrations (Express, Hono, NestJS, Next.js) have zero peer dependencies -- they use minimal type interfaces internally instead of importing framework packages.

Import Paths

duck-iam uses subpath exports to keep your bundle lean. Import only what you need:

Loading diagram...

// Core engine, builders, memory adapter, types
import { Engine, defineRole, policy, when, MemoryAdapter } from "@gentleduck/iam";
 
// Server integrations
import { accessMiddleware, guard } from "@gentleduck/iam/server/express";
import { accessMiddleware, guard } from "@gentleduck/iam/server/hono";
import { Authorize, nestAccessGuard } from "@gentleduck/iam/server/nest";
import { withAccess, checkAccess, getPermissions } from "@gentleduck/iam/server/next";
 
// Client libraries
import { createAccessControl } from "@gentleduck/iam/client/react";
import { createVueAccess } from "@gentleduck/iam/client/vue";
import { AccessClient } from "@gentleduck/iam/client/vanilla";
 
// Storage adapters
import { MemoryAdapter } from "@gentleduck/iam/adapters/memory";
import { PrismaAdapter } from "@gentleduck/iam/adapters/prisma";
import { DrizzleAdapter } from "@gentleduck/iam/adapters/drizzle";
import { HttpAdapter } from "@gentleduck/iam/adapters/http";
// Core engine, builders, memory adapter, types
import { Engine, defineRole, policy, when, MemoryAdapter } from "@gentleduck/iam";
 
// Server integrations
import { accessMiddleware, guard } from "@gentleduck/iam/server/express";
import { accessMiddleware, guard } from "@gentleduck/iam/server/hono";
import { Authorize, nestAccessGuard } from "@gentleduck/iam/server/nest";
import { withAccess, checkAccess, getPermissions } from "@gentleduck/iam/server/next";
 
// Client libraries
import { createAccessControl } from "@gentleduck/iam/client/react";
import { createVueAccess } from "@gentleduck/iam/client/vue";
import { AccessClient } from "@gentleduck/iam/client/vanilla";
 
// Storage adapters
import { MemoryAdapter } from "@gentleduck/iam/adapters/memory";
import { PrismaAdapter } from "@gentleduck/iam/adapters/prisma";
import { DrizzleAdapter } from "@gentleduck/iam/adapters/drizzle";
import { HttpAdapter } from "@gentleduck/iam/adapters/http";

TypeScript Configuration

duck-iam works out of the box with standard TypeScript settings. For the best experience, make sure your tsconfig.json includes:

tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "moduleResolution": "bundler"
  }
}
tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "moduleResolution": "bundler"
  }
}

The strict flag enables the const type parameter inference that powers type-safe role and policy builders. The bundler module resolution is required for subpath exports to resolve correctly. Alternatively, "moduleResolution": "nodenext" also works.

Next Steps

  • Quick Start -- end-to-end walkthrough with roles, policies, server middleware, and client hooks.
  • Core Concepts -- deep dive into the RBAC + ABAC evaluation model.
  • Integrations -- set up Express, Hono, NestJS, or Next.js middleware.