PermDock
Concepts

Apps and profiles

defineApp binds the data sources, opt-in features and services an app wires once (memberships, custom roles, relations, hosted policies, approval policies, limits, sink, approval store) to a policy, so createPermDock and every adapter read them from one place; profile dev fills in in-process defaults and profile prod refuses to start when a grant needs a service the app lacks.

defineApp(policy, options) returns the policy with the app's sources, features and services bound to it. Pass the result wherever a policy goes: core createPermDock, every adapter and the CLI. Each one reads the app definition, and its own options win key by key.

// src/permdock.ts
import {
  approvalPolicies,
  defineApp,
  hostedPolicies,
  limits,
  memoryLimitStore,
} from "permdock";
import { memoryApprovalStore } from "permdock/approvals";
import { policy } from "./policy";

export const app = defineApp(policy, {
  memberships, // MembershipSource, or several
  customRoles, // CustomRoleSource
  relations, // RelationSource
  approvalPolicies: approvalPolicies(approvalPolicySource),
  policies: hostedPolicies(policySource),
  limits: limits(memoryLimitStore()),
  sink,
  store: memoryApprovalStore(),
  profile: process.env.NODE_ENV === "production" ? "prod" : "dev",
});
// src/server.ts
import { createPermDock } from "permdock/hono";
import { app } from "./permdock";

export const { protect, middleware } = createPermDock(app, { subject });

What an app holds

KeyWhatWithout it
memberships, customRoles, entitlements, relations, principals, settingsThe extension interfaces that read the application's own dataMemberships come from the principal; no custom roles, entitlements or relations
policieshostedPolicies(source): hosted grants on hostable permissionsThe code policy only
approvalPoliciesapprovalPolicies(source), or a memoryApprovalPolicySource([...]) as isApproval requirements from code only
limitslimits(store): counts limit grants in a LimitStoreA grant with limit denies with limit-unavailable
sinkA DecisionSinkNo decision log
storeThe ApprovalStore every adapter opens and resumes approvals inapproval-required cannot be resumed
profile"prod" (default) or "dev""prod"

hostedPolicies, approvalPolicies and limits are features, not bare sources. The code that merges hosted grants, tightens approvals and counts quotas ships only with an app that imports them, so a plain RBAC app does not carry it.

Profiles

  • dev fills in in-process defaults: one memory ApprovalStore that every adapter of the app shares, so a request opened through one adapter resumes through another, and limits(memoryLimitStore()) when a grant has a limit. The defaults come from defineApp, never from an adapter, so an adapter bundle carries no store. A new app runs before any infrastructure exists. Memory stores are per process: their state is lost on restart and is not shared between instances.
  • prod fills in nothing. defineApp throws at startup when a grant requires approval and there is no store, or a grant has a limit and there are no limits. The alternative is an app that starts and then denies every such call at run time.

No adapter falls back to a memory store on its own. An approval store exists because the app or the adapter call named one, or because the profile is dev.

Why

Before defineApp, each adapter took its own copy of the same sources and services, and the CLI and the runtime named the same tables twice. A team that added a second surface (an MCP server next to a Next.js app) wired everything again and could miss the sink or the store on one of them. One app definition removes that duplication without a new interface: the result is still a Policy, so no adapter signature changes and an adapter option still overrides the app.

The profile makes the trade-off between a quick start and a safe production setup explicit. A memory store is right for the first hour and wrong in production. The dev profile gives it, prod refuses to start without the real one, and the choice is one line the reader can see.

Last updated on

On this page