# Apps and profiles

Source: https://permdock.com/docs/concepts/apps

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.

```ts
// 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",
});
```

```ts
// src/server.ts
import { createPermDock } from "permdock/hono";
import { app } from "./permdock";

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

## What an app holds [#what-an-app-holds]

| Key | What | Without it |
| --- | --- | --- |
| `memberships`, `customRoles`, `entitlements`, `relations`, `principals`, `settings` | The [extension interfaces](/docs/concepts/extension-interfaces) that read the application's own data | Memberships come from the principal; no custom roles, entitlements or relations |
| `policies` | `hostedPolicies(source)`: hosted grants on `hostable` permissions | The code policy only |
| `approvalPolicies` | `approvalPolicies(source)`, or a `memoryApprovalPolicySource([...])` as is | Approval requirements from code only |
| `limits` | `limits(store)`: counts `limit` grants in a `LimitStore` | A grant with `limit` denies with `limit-unavailable` |
| `sink` | A `DecisionSink` | No decision log |
| `store` | The `ApprovalStore` every adapter opens and resumes approvals in | `approval-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 [#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 [#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.
