{
  "source": "samples/apps/use-all-bundled APP_FEATURES (composeFeatures includeBundled + signup)",
  "featureCount": 74,
  "features": [
    {
      "name": "admin-shell",
      "description": "Registers tenant-admin and platform-admin workspaces with provider nav into owner-feature screens (`tenant:screen:members`, `audit:screen:audit-log`, `tenant:screen:tenant-list`, `jobs:screen:job-runs`, optional `tier-engine:screen:tier-admin`, optional `cap-overview:screen:my-caps`/`cap-overview:screen:tenant-cap-list`). Mount after user, tenant, audit, and jobs; pass `workspaceIds` to match app URL conventions (e.g. Studio `d`/`s`, PublicStatus `admin`/`sysadmin`). Client: `adminShellClient()`, `tenantClient()`, `auditClient()`, `jobsClient()`, optional `tierEngineClient()`.",
      "toggleableDefault": null,
      "requires": [
        "user",
        "tenant",
        "audit",
        "jobs",
        "tier-engine",
        "cap-overview"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Admin Shell",
        "category": "operations",
        "recommended": false
      }
    },
    {
      "name": "agent-tools",
      "description": "Builds a tool catalog and an agent manifest from the mounted registry so an LLM agent can call handlers and understand the app's shape. A handler or entity without a `description` stays invisible to the agent by construction (see `resolveAgentExposure`), while a screen stays visible unless it opts out with `agent: { expose: false }` (see `isAgentVisibleScreen`) — this feature surfaces the resulting gaps at boot and via `kumiko agent lint` so an app author notices before an agent silently can't see a feature.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "AI Agent Tools",
        "category": "ai",
        "recommended": false
      }
    },
    {
      "name": "audit",
      "description": "Exposes the framework's event store as a paginated, filterable audit log via the `audit:query:list` handler (accessible to `Admin` and `SystemAdmin` roles). No separate table or projection — the event store is the audit trail by construction: every entity write already records who, when, what entity, and the event payload with PII stripped. Filter by `aggregateType`, `aggregateId`, `eventType`, `userId`, or time range. Also records `audit:event:escape-hatch-used` whenever a handler uses one of the framework's escape hatches (unsafeRaw, acknowledgeCrossTenant, db.global() writes, or a granted identity switch).",
      "toggleableDefault": null,
      "requires": [
        "tenant",
        "user"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Audit Log",
        "category": "compliance",
        "recommended": false
      }
    },
    {
      "name": "auth-email-password",
      "description": "Provides email+password authentication: the always-on handlers are `login`, `changePassword`, and `logout`; optional flows — password reset, email verification, magic-link self-signup, tenant invite, and account-unlock — are registered only when you pass their respective option objects (`passwordReset`, `emailVerification`, `signup`, `invite`, `accountUnlock`) to `createAuthEmailPasswordFeature(opts)`. All five magic-link flows dispatch their mail through the `delivery` feature via `ctx.notify`, so mounting any of them additionally requires `delivery`. Tokens are HMAC-signed (reset/verify/unlock) or opaque-random in Redis (signup/invite). `accountUnlock` is the self-service escape hatch for `accountLockout`'s monotonic failure-counter (#1266): confirming the mailed token clears the Redis lockout state without touching the user entity. Requires the `user` and `tenant` features, and declares `JWT_SECRET` (≥ 32 chars) in `authEmailPasswordEnvSchema` so a missing secret surfaces at boot validation rather than on the first login attempt.",
      "toggleableDefault": null,
      "requires": [
        "user",
        "tenant",
        "delivery"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "auth-email-password:write:change-password",
        "auth-email-password:write:confirm-account-unlock",
        "auth-email-password:write:invite-accept",
        "auth-email-password:write:invite-accept-with-login",
        "auth-email-password:write:invite-create",
        "auth-email-password:write:invite-signup-complete",
        "auth-email-password:write:login",
        "auth-email-password:write:logout",
        "auth-email-password:write:request-account-unlock",
        "auth-email-password:write:request-email-verification",
        "auth-email-password:write:request-password-reset",
        "auth-email-password:write:reset-password",
        "auth-email-password:write:signup-confirm",
        "auth-email-password:write:signup-request",
        "auth-email-password:write:verify-email"
      ],
      "uiHints": {
        "displayLabel": "Auth · Email + Password",
        "category": "identity",
        "recommended": true,
        "configurableOptions": [
          {
            "key": "passwordReset",
            "label": "Password-Reset-Flow",
            "type": "boolean",
            "default": true
          },
          {
            "key": "emailVerification",
            "label": "Email-Verification-Flow",
            "type": "boolean",
            "default": true
          },
          {
            "key": "signup",
            "label": "Self-Signup-Flow",
            "type": "boolean",
            "default": false
          },
          {
            "key": "invite",
            "label": "Tenant-Invite-Flow",
            "type": "boolean",
            "default": false
          },
          {
            "key": "accountUnlock",
            "label": "Account-Unlock-Flow",
            "type": "boolean",
            "default": false
          }
        ]
      }
    },
    {
      "name": "auth-foundation",
      "description": "Declares auth-middleware extension points: `tokenVerifier` (Bearer), `sessionStore` (revocable JWT sid), optional `tenantResolver` + `tenantExistence` for anonymous multi-tenant. Provider-features register via r.useExtension; mount auth-foundation with the matching provider features.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Auth Provider Foundation",
        "category": "identity",
        "recommended": false
      }
    },
    {
      "name": "auth-mfa",
      "description": "TOTP-based two-factor authentication: enable/disable flow with QR-code setup, 8 single-use recovery codes, and (once wired into a login flow) a second login step after password verification. Secrets are envelope-encrypted at rest via the same MasterKeyProvider as `secrets`/`config`.",
      "toggleableDefault": null,
      "requires": [
        "user",
        "config",
        "tenant"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [
        {
          "key": "required",
          "qualifiedName": "auth-mfa:config:required",
          "type": "select",
          "scope": "tenant",
          "default": "optional",
          "encrypted": false,
          "computed": false,
          "options": [
            "optional",
            "admins",
            "all"
          ],
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        }
      ],
      "secrets": [],
      "writeHandlers": [
        "auth-mfa:write:disable",
        "auth-mfa:write:enable-confirm",
        "auth-mfa:write:enable-confirm-preauth",
        "auth-mfa:write:enable-start",
        "auth-mfa:write:enable-start-preauth",
        "auth-mfa:write:regenerate-recovery",
        "auth-mfa:write:verify"
      ],
      "uiHints": {
        "displayLabel": "2FA / TOTP",
        "category": "identity",
        "recommended": true
      }
    },
    {
      "name": "auth-mfa-user-data",
      "description": "GDPR (Art. 20 export / Art. 17 erasure) coverage for the `auth-mfa` feature's `user-mfa` entity. Mounts the EXT_USER_DATA export + delete hooks so 2FA enrollment status is included in the user-data export bundle and a user's TOTP secret + recovery codes are hard-deleted (via executor.forget, rebuild-safe) on a data-subject erasure request. Kept separate from `auth-mfa` so consumers without the user-data-rights pipeline don't pull a hard dependency — requires `user-data-rights`, optionalRequires `auth-mfa`.",
      "toggleableDefault": null,
      "requires": [
        "user-data-rights"
      ],
      "optionalRequires": [
        "auth-mfa"
      ],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "userData",
          "entityName": "user-mfa"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": []
    },
    {
      "name": "auth-self-registration",
      "description": "Runtime on/off switch for the auth-email-password self-signup flow. Handler-less: composing it registers \"auth-self-registration\" as a toggleable feature (default ON). auth-email-password's signup-request handler and its `signupRegistrationStatus` query both read `ctx.hasFeature(\"auth-self-registration\")` — the query stays reachable when the toggle is off (deliberately not gated itself) so the public signup page can hide its own link/form. Only meaningful when `signup` is configured on `createAuthEmailPasswordFeature` — compose alongside it, then flip via the feature-toggles admin screen.",
      "toggleableDefault": true,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": []
    },
    {
      "name": "billing-foundation",
      "description": "Plugin host for subscription billing — manages the `read_subscriptions` projection table and exposes 5 domain events (subscription created/updated/canceled, invoice paid/failed) appended by the foundation's own `billing-foundation:write:process-event` write-handler after provider plugins verify and normalize each webhook. Also manages a separate `read_payments` projection table (one row per one-off-payment) fed by its own `payment-received` event and `billing-foundation:write:process-payment-event` write-handler. Also ships `billing-foundation:write:create-checkout-session` and `billing-foundation:write:create-portal-session` write-handlers, a `billing-foundation:query:subscription:list` query handler, and a `createSubscriptionWebhookRoute` factory for the `/api/subscription/webhook/:providerName` extraRoute. Low-level building block — use `subscription-stripe` or `subscription-mollie` unless you are writing a new payment provider.",
      "toggleableDefault": null,
      "requires": [
        "tenant-lifecycle",
        "compliance-profiles"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "tenantData",
          "entityName": "subscription"
        },
        {
          "extensionName": "tenantData",
          "entityName": "payment"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "billing-foundation:write:create-checkout-session",
        "billing-foundation:write:create-portal-session",
        "billing-foundation:write:process-event",
        "billing-foundation:write:process-payment-event"
      ],
      "uiHints": {
        "displayLabel": "Billing · Foundation",
        "category": "billing",
        "recommended": false
      }
    },
    {
      "name": "cap-counter",
      "description": "Tracks per-tenant usage against configurable limits using two complementary storage models: calendar-period counters (one projection row per tenant/capName/period, reset implicitly by period rollover) and rolling-window counters (append-only event stream, no projection). Use `enforceCap` / `enforceRollingCap` (or the `withCapEnforcement` / `withRollingCapEnforcement` handler wrappers) in your write-handlers to check limits with soft-warn and hard-block tolerances; call `enforceCapAndMaybeNotify` when you also want to trigger a delivery notification on soft-threshold hits.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "cap-counter:write:increment",
        "cap-counter:write:increment-rolling",
        "cap-counter:write:mark-soft-warned"
      ],
      "uiHints": {
        "displayLabel": "Cap Counter · Usage Limits",
        "category": "operations",
        "recommended": false
      }
    },
    {
      "name": "cap-overview",
      "description": "Read-only visibility into per-tenant tier assignment and cap usage. SystemAdmin gets a platform-wide tenant list with usage bars; every member of a tenant gets their own tenant's usage as dashboard cards. Reads tier-engine, billing-foundation, and tenant data plus app-owned usage tables via caller-supplied CapSpec callbacks — never writes.",
      "toggleableDefault": null,
      "requires": [
        "tenant",
        "tier-engine",
        "billing-foundation"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Cap Overview · Tier & Usage Visibility",
        "category": "operations",
        "recommended": false
      }
    },
    {
      "name": "cap-overview-labels",
      "description": "Labels for the example caps this sample app mounts into cap-overview.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": []
    },
    {
      "name": "channel-email",
      "description": "Wires an `EmailTransport` (typically `mail-transport-smtp` in production, `createInMemoryTransport()` in tests) into the delivery system as the `email` channel. Requires `delivery`; pass an `EmailChannelOptions` with a `transport`, a `renderer: NotificationRenderer` (e.g. backed by `renderer-simple`), and a `resolveEmail` function that maps a user ID to their email address.",
      "toggleableDefault": null,
      "requires": [
        "delivery"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "deliveryChannel",
          "entityName": "email"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Email Channel",
        "category": "notifications",
        "recommended": false
      }
    },
    {
      "name": "channel-in-app",
      "description": "Persists notifications to an in-app inbox table so users can retrieve them via `handlers.inbox` and track unread state with `handlers.markRead` / `handlers.markAllRead` and `queries.unreadCount`. Requires `delivery`; no external service needed — messages are stored in the app's own database.",
      "toggleableDefault": null,
      "requires": [
        "delivery"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "deliveryChannel",
          "entityName": "inApp"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "channel-in-app:write:mark-all-read",
        "channel-in-app:write:mark-read"
      ],
      "uiHints": {
        "displayLabel": "In-App Inbox",
        "category": "notifications",
        "recommended": false
      }
    },
    {
      "name": "channel-push",
      "description": "Delivers push notifications through a `PushTransport` (bring your own FCM/APNs adapter or use `createInMemoryPushTransport()` for tests) registered as the `push` channel in the delivery system. Requires `delivery`; supply a `PushChannelOptions` with a transport and a resolver that maps a user ID to their device token.",
      "toggleableDefault": null,
      "requires": [
        "delivery"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "deliveryChannel",
          "entityName": "push"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Push Channel",
        "category": "notifications",
        "recommended": false
      }
    },
    {
      "name": "collection-labels",
      "description": "Nav labels for the content collections mounted by this sample app.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": []
    },
    {
      "name": "compliance-profiles",
      "description": "Lets each tenant select a compliance regime (e.g. `eu-dsgvo`, `swiss-dsg`, `de-hr-dsgvo-hgb`) that bundles user-rights grace periods, breach-disclosure deadlines, sub-processor requirements, and audit-retention rules into a single named profile. Tenant admins call `compliance-profiles:write:set-profile` to choose a profile (with optional JSON override for edge cases); other features resolve the effective profile via the `compliance.forTenant` cross-feature API. Required by `user-data-rights` — mount this feature before it.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [
        "compliance.forTenant"
      ],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "compliance-profiles:write:set-profile"
      ],
      "uiHints": {
        "displayLabel": "Compliance Profiles",
        "category": "compliance",
        "recommended": false
      }
    },
    {
      "name": "compliance-profiles-ops",
      "description": "Platform-wide SystemAdmin counterpart to `compliance-profiles`' `needs-profile` query (#2089): `tenants-missing-profile` lists every enabled tenant with no row in `tenantComplianceProfile` at all, tenant-wide instead of scoped to the caller's own tenant. Mount alongside `compliance-profiles` and `tenant` when an operator UI needs to see which tenants still silently run on `minimal-no-region`.",
      "toggleableDefault": null,
      "requires": [
        "compliance-profiles",
        "tenant"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Compliance Profiles · Operator Visibility",
        "category": "compliance",
        "recommended": false
      }
    },
    {
      "name": "config",
      "description": "Stores per-tenant (and optionally per-user) configuration values with a multi-layer cascade: user-row → tenant-row → system-row → app-override (deploy-time `AppConfigOverrides`) → computed → feature default. Access a value in handlers via `ctx.config(handle)`, declare keys with `r.config({ keys: { ... } })` inside a feature's registry callback, and optionally mark them `encrypted: true` to route storage through the envelope cipher (versioned master key). Use this feature whenever a tenant admin needs to customise behaviour at runtime without a code deploy.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "config:write:reset",
        "config:write:set"
      ],
      "uiHints": {
        "displayLabel": "Tenant Config Store",
        "category": "infrastructure",
        "recommended": true
      }
    },
    {
      "name": "crypto-shredding",
      "description": "Operator-level crypto-shredding trigger. `forget-subject` erases a user or tenant subject key in the configured KMS adapter, making every PII field encrypted under it permanently unreadable (reads render the `[[erased]]` sentinel), and appends a `subject-forgotten` audit event. Requires a KMS adapter (`runProdApp({ kms })`). The automated Art.-17 deletion pipeline in `user-data-rights` erases keys itself; this command covers manual forgets (authority requests, operator recovery, tenant destroy).",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "crypto-shredding:write:forget-subject"
      ],
      "uiHints": {
        "displayLabel": "Crypto-Shredding",
        "category": "compliance"
      }
    },
    {
      "name": "custom-fields",
      "description": "Tenant- and system-scoped custom field definitions with generic value storage on any host entity. Registers the `field-definition` entity (event-sourced CRUD via `define-tenant-field`, `define-system-field`, `update-tenant-field`, `delete-tenant-field`, `delete-system-field`) and two value write-handlers (`set-custom-field`, `clear-custom-field`) that emit `custom-fields:event:custom-field-set` / `custom-fields:event:custom-field-cleared` events on the host aggregate's stream. To attach custom fields to your own entity, call `wireCustomFieldsFor(r, entityName, entityTable)` in the host feature — this wires the JSONB projection, `postQuery` flattening hook, and search-payload extension. The host entity must declare a `customFieldsField()` JSONB column.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "custom-fields:write:clear-custom-field",
        "custom-fields:write:define-system-field",
        "custom-fields:write:define-tenant-field",
        "custom-fields:write:delete-system-field",
        "custom-fields:write:delete-tenant-field",
        "custom-fields:write:set-custom-field",
        "custom-fields:write:update-tenant-field"
      ],
      "uiHints": {
        "displayLabel": "Custom Fields",
        "category": "data",
        "recommended": false
      }
    },
    {
      "name": "data-retention",
      "description": "Resolves the effective retention policy for any entity using a 3-layer stack: entity-level default → tenant preset (`dsgvo-basic`, `dsgvo-hgb`, `swiss-dsg`) → per-tenant override stored in `tenantRetentionOverride`. Other features query the resolved policy via the `retention.policyFor` cross-feature API — most notably `user-data-rights`, which uses it to decide whether to anonymize instead of hard-delete a record that is still within a mandatory retention window.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [
        "retention.policyFor"
      ],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Data Retention Policy",
        "category": "compliance",
        "recommended": false
      }
    },
    {
      "name": "delivery",
      "description": "The notification dispatch core: call `ctx.notify(notificationType, { to, route, data, priority, idempotencyKey })` from any handler to fan out a notification across all registered channels (email, in-app, push). It stores per-user channel preferences in the `notification-preference` entity, opt-outs for no-account recipient addresses in `notification-address-opt-out` (keyed by a blind-index hash, never the plaintext address), logs every attempt to `store_delivery_attempts`, and enforces idempotency and rate-limiting — add `channel-email`, `channel-in-app`, or `channel-push` on top to actually send anything. Unsubscribe links are served by `createUnsubscribeRoute({ secret })` mounted via the app's `extraRoutes` at `GET /api/delivery/unsubscribe?token=`; sign links with `signUnsubscribeToken` / `signAddressUnsubscribeToken` using the same secret.",
      "toggleableDefault": null,
      "requires": [
        "tenant"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "delivery:write:set-preference",
        "delivery:write:unsubscribe-address",
        "delivery:write:unsubscribe-user"
      ],
      "uiHints": {
        "displayLabel": "Notifications · Dispatch Core",
        "category": "notifications",
        "recommended": true
      }
    },
    {
      "name": "derivatives-sharp",
      "description": "Registers an `image/*` renderer against the `derivativeRenderer` extension point declared by `file-derivatives`, backed by sharp. Supports resize/fit (cover, inside, contain), format conversion with quality (webp, avif, jpeg), whole-image blur, and blurring individual regions burned in before resize (see BlurRegion). EXIF orientation is applied and then normalized away, and all other EXIF (including GPS) is dropped. Server-only: sharp is a native binding — never import this from client code.",
      "toggleableDefault": null,
      "requires": [
        "file-derivatives"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "derivativeRenderer",
          "entityName": "image/*"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "File Derivatives · Sharp",
        "category": "storage",
        "recommended": false
      }
    },
    {
      "name": "document-ingest-foundation",
      "description": "Shared PDF/Scan/Image → normalized-text ingest primitive. Owns the `documentExtract` entity (fileRefId, storageKey, per-page text + metadata) as an implicit entity-projection, the per-tenant `ocrLanguage`/`maxPagesPerFile` config keys, the `documentIngestProvider` extension-point providers register accepted mimeTypes/size caps under, and a fileRef.created/fileRef.restored trigger that resolves the provider for a file's mimeType and requests ingest via `documentIngest.requested` tagged with the winning provider — a delete→restore round-trip re-requests ingest the same way a fresh upload does. An orphan-extract guard forgets any `documentExtract` whose fileRef is no longer live at processing time, regardless of how a provider's own write races the fileRef's delete/forget — providers should write through `writeDocumentExtractForLiveFileRef`.",
      "toggleableDefault": null,
      "requires": [
        "config",
        "tenant-lifecycle"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "tenantData",
          "entityName": "documentExtract"
        }
      ],
      "configKeys": [
        {
          "key": "max-pages-per-file",
          "qualifiedName": "document-ingest-foundation:config:max-pages-per-file",
          "type": "number",
          "scope": "tenant",
          "default": 50,
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": {
            "min": 1,
            "max": 500
          },
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ],
          "readRoles": [
            "TenantAdmin",
            "SystemAdmin",
            "User"
          ]
        },
        {
          "key": "ocr-language",
          "qualifiedName": "document-ingest-foundation:config:ocr-language",
          "type": "text",
          "scope": "tenant",
          "default": "deu+eng",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": {
            "regex": "^[a-z]{3}(\\+[a-z]{3})*$"
          },
          "writeRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ],
          "readRoles": [
            "TenantAdmin",
            "SystemAdmin",
            "User"
          ]
        }
      ],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Document Ingest Foundation",
        "category": "storage",
        "recommended": false
      }
    },
    {
      "name": "feature-toggles",
      "description": "Persists per-feature enabled/disabled state in the `store_global_feature_state` table and exposes a `set` write-handler plus `list`/`registered` query-handlers so operators can flip features at runtime without redeploying. Each API instance keeps an in-memory `GlobalFeatureToggleRuntime` snapshot (initialize it via `createFeatureToggleRuntime`, pass a `() => runtime` accessor to `createFeatureTogglesFeature`) that the dispatcher gate reads on every request; a `toggle-cache-sync` multi-stream projection with `delivery: \"shared\"` syncs the snapshot across instances whenever a `toggle-set` event is appended.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "feature-toggles:write:set"
      ],
      "uiHints": {
        "displayLabel": "Feature Toggles · Operator Switches",
        "category": "operations",
        "recommended": false
      }
    },
    {
      "name": "file-derivatives",
      "description": "Declares the `derivativeRenderer` extension point. `ctx.derivatives.variant(fileRefId, spec, name)` derives a variant of a tracked FileRef the first time it's requested and reuses the stored result afterwards (derive-on-first-use, keyed by a hash of the spec). Mount at least one `derivatives-*` renderer feature alongside this one — without a registered renderer for the FileRef's MIME type, every `variant(...)` call throws. Also declares the `derivativePublicPredicate` extension point (`r.useExtension(EXT_DERIVATIVE_PUBLIC_PREDICATE, '<entityType>', { isPublic })`) and, when `createFileDerivativesFeature({resolveApexTenant})` is passed a host-resolver, mounts an anonymous `GET {basePath}/:fileRefId/:variant` route that serves any variant name the FileRef's field declared in its `variants` for a FileRef whose entityType has a registered predicate returning true — default-deny (404) otherwise, same as an unknown FileRef or an undeclared variant name. The route's only rate-limit (`per: \"ip\"`) trusts the first `x-forwarded-for` hop — deployers must ensure their ingress overwrites rather than appends to that header, or the throttle is bypassable by rotating it. `publicTenantResolution: \"fileRef\"` keeps resolveApexTenant as the host gate but reads the variant from the FileRef row's own tenant instead of the host's, so one shared platform host serves every tenant's public variants — each FileRef-tenant's own `isPublic` predicate still default-denies. Also declares the `derivativeOverlayResolver` extension point (`r.useExtension(EXT_DERIVATIVE_OVERLAY_RESOLVER, '<entityType>', { resolve })`), used to turn a variant's `overlays[].dataToken` into the actual QR payload for that FileRef's entityType before the variant is rendered — a variant declaring a `qr` overlay throws at request-time if no resolver is registered for the FileRef's entityType.",
      "toggleableDefault": null,
      "requires": [
        "file-foundation",
        "files"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "File Derivatives",
        "category": "storage",
        "recommended": false
      }
    },
    {
      "name": "file-foundation",
      "description": "Defines the `fileProvider` extension point and a per-tenant `provider` config key that selects which registered storage plugin to use at runtime. Call `createFileProviderForTenant(ctx, tenantId)` to get a `FileStorageProvider` — use this feature together with at least one `file-provider-*` feature; the `files` feature builds on top of it for tracked `FileRef` entities with GDPR hooks.",
      "toggleableDefault": null,
      "requires": [
        "config"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [
        {
          "key": "provider",
          "qualifiedName": "file-foundation:config:provider",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ],
          "readRoles": [
            "TenantAdmin",
            "SystemAdmin",
            "User"
          ]
        }
      ],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "File Provider Foundation",
        "category": "storage",
        "recommended": false
      }
    },
    {
      "name": "file-provider-inmemory",
      "description": "Registers an in-process `\"inmemory\"` provider for `file-foundation` that stores file bytes per tenant in a module-level Map. Use `listKeys(tenantId)` and `clearStorage(tenantId)` in demo apps and tests; not for production (data is lost on restart and grows without bound).",
      "toggleableDefault": null,
      "requires": [
        "file-foundation"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "fileProvider",
          "entityName": "inmemory"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "File Provider · In-Memory",
        "category": "storage",
        "recommended": false
      }
    },
    {
      "name": "file-provider-s3",
      "description": "Registers itself as the `\"s3\"` provider for `file-foundation` and owns the per-tenant config keys (`bucket`, `region`, `endpoint`, `forcePathStyle`, `accessKeyId`) and the encrypted `s3.secretAccessKey` secret. Compatible with any S3-compatible object store (AWS S3, Hetzner Object Storage); set credentials via the admin UI or a seed handler before the first file operation.",
      "toggleableDefault": null,
      "requires": [
        "config",
        "secrets",
        "file-foundation"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "fileProvider",
          "entityName": "s3"
        }
      ],
      "configKeys": [
        {
          "key": "access-key-id",
          "qualifiedName": "file-provider-s3:config:access-key-id",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ],
          "readRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ]
        },
        {
          "key": "bucket",
          "qualifiedName": "file-provider-s3:config:bucket",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ],
          "readRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ]
        },
        {
          "key": "endpoint",
          "qualifiedName": "file-provider-s3:config:endpoint",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ],
          "readRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ]
        },
        {
          "key": "force-path-style",
          "qualifiedName": "file-provider-s3:config:force-path-style",
          "type": "boolean",
          "scope": "tenant",
          "default": false,
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        },
        {
          "key": "region",
          "qualifiedName": "file-provider-s3:config:region",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ],
          "readRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ]
        }
      ],
      "secrets": [
        {
          "qualifiedName": "file-provider-s3:secret:s3-secret-access-key",
          "scope": "tenant",
          "label": "S3 Secret Access Key",
          "hint": "Private half of the S3 key pair. Hetzner calls it 'Secret Key', AWS calls it 'Secret Access Key'."
        }
      ],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "File Provider · S3",
        "category": "storage",
        "recommended": false
      }
    },
    {
      "name": "file-provider-s3-env",
      "description": "Registers an `\"s3-env\"` provider for `file-foundation` that reads one S3 credential set from `process.env` (`S3_BUCKET`/`S3_REGION`/`S3_ACCESS_KEY`/`S3_SECRET_KEY`, optional `S3_ENDPOINT`/`S3_FORCE_PATH_STYLE`) and serves every tenant from one shared bucket — no per-tenant config or secret seeding. Use this for single-bucket S3-compatible deploys (e.g. Hetzner Object Storage); use `file-provider-s3` instead when each tenant needs its own bucket/credentials.",
      "toggleableDefault": null,
      "requires": [
        "file-foundation"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "fileProvider",
          "entityName": "s3-env"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "File Provider · S3 (env)",
        "category": "storage",
        "recommended": false
      }
    },
    {
      "name": "files",
      "description": "Exposes the `fileRef` entity from the framework core so that uploaded files — tracked in the `file_refs` table by `createFileRoutes` — participate in cross-feature hooks: `user-data-rights-defaults` automatically includes file blobs in GDPR exports and forget flows, and tenant-lifecycle cleanup deletes all refs on tenant destroy. Upload/download routes are registered by the server bootstrap when a `file-provider-*` feature is mounted; this feature carries their access/size policy via `createFilesFeature(opts?)`.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Files · Metadata",
        "category": "storage",
        "recommended": false
      }
    },
    {
      "name": "files-tenant-data",
      "description": "GDPR coverage for the `files` feature's `fileRef` entity: tenant-destroy row purge (EXT_TENANT_DATA) and full storage-prefix binary wipe (EXT_STORAGE_PROVIDER), plus a manual backfill/GC job that sweeps derivatives orphaned by a forget/tenant-destroy that ran before #2461 wired binary cleanup into those flows. Requires `tenant-lifecycle`, optionalRequires `files`.",
      "toggleableDefault": null,
      "requires": [
        "tenant-lifecycle"
      ],
      "optionalRequires": [
        "files"
      ],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "tenantData",
          "entityName": "fileRef"
        },
        {
          "extensionName": "storageProvider",
          "entityName": "fileRef"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Files · Tenant-Destroy & Derivative GC",
        "category": "compliance",
        "recommended": false
      }
    },
    {
      "name": "folders",
      "description": "Generic, host-agnostic hierarchical folders for any entity. Owns two event-sourced entities — the per-tenant `folder` tree (`read_folders`, self-referential via parentId) and SINGLE-membership `folder-assignment` rows keyed by (entityType, entityId) (`read_folder_assignments`) — so filing an entity adds NO column to the host and needs no relational pivot or JOIN. The folder catalog uses the generic entity handlers (create, update [= rename, optimistic-locked], delete, list, detail); set-folder puts/moves an entity into a folder (one folder per entity) and clear-folder unfiles it (both idempotent). Read which folder an entity is in, or which entities a folder holds, by listing `folder-assignment` filtered on `entityId` or `folderId`. Every path uses one access rule — adopt the host's model with createFoldersFeature({ access: { openToAll: { reason } } }) or pin roles. Pass { toggleable: { default: false } } to make the whole feature tier-gatable via the tier-engine (no host hook).",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "folders:write:clear-folder",
        "folders:write:folder:create",
        "folders:write:folder:delete",
        "folders:write:folder:update",
        "folders:write:set-folder"
      ],
      "uiHints": {
        "displayLabel": "Folders",
        "category": "data",
        "recommended": false
      }
    },
    {
      "name": "folders-user-data",
      "description": "GDPR (Art. 20 export / Art. 17 erasure) coverage for the `folders` feature's `folder` + `folder-assignment` entities. Mounts the EXT_USER_DATA export + delete hooks so a tenant's folder tree and its entity-to-folder assignments are included in the user-data export bundle and erased on a tenant-scoped forget (single-user tenants only; multi-user + anonymize are no-ops since folder rows carry no per-user PII). Kept separate from `folders` so folder consumers without the user-data-rights pipeline don't pull a hard dependency — requires `user-data-rights`, optionalRequires `folders`.",
      "toggleableDefault": null,
      "requires": [
        "user-data-rights"
      ],
      "optionalRequires": [
        "folders"
      ],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "userData",
          "entityName": "folder"
        },
        {
          "extensionName": "userData",
          "entityName": "folder-assignment"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": []
    },
    {
      "name": "form-draft",
      "description": "Per-user, per-tenant working copy of an in-progress form, saved BEFORE the real domain entity exists. Owns one event-sourced entity, `form-draft` (`read_form_drafts`), keyed by a caller-assigned draftKey (typically screenId + optional hostEntityId), unique per (tenant, owner, draftKey). `save` upserts the draft blob ({ values, stepIndex, savedAt } — savedAt stamped server-side), `discard` deletes it (called once the real submit succeeds), `get` resumes it, `list` finds a user's open drafts for a given screenId (draftKey prefix match) — the fallback when a client-generated draftId is lost. Ownership is enforced by a per-row owner filter in every handler, not by roles — a foreign user's save/discard/get/list for someone else's draftKey never sees or touches that row. Never holds anything that already lives in a domain stream; the consuming app is responsible for discarding once the domain write succeeds. A daily cron job hard-deletes drafts past a configurable retention window (`form-draft:config:retention-days`, SystemAdmin-writable, default 30 days).",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [
        "config"
      ],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [
        {
          "key": "retention-days",
          "qualifiedName": "form-draft:config:retention-days",
          "type": "number",
          "scope": "system",
          "default": 30,
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": {
            "min": 1
          },
          "pattern": null,
          "writeRoles": [
            "SystemAdmin"
          ],
          "readRoles": [
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ]
        }
      ],
      "secrets": [],
      "writeHandlers": [
        "form-draft:write:discard",
        "form-draft:write:save"
      ],
      "uiHints": {
        "displayLabel": "Form Drafts",
        "category": "data",
        "recommended": false
      }
    },
    {
      "name": "form-draft-user-data",
      "description": "GDPR (Art. 20 export / Art. 17 erasure) coverage for the `form-draft` feature's `form-draft` entity. Mounts the EXT_USER_DATA export hook so a user's saved drafts are included in the user-data export bundle, and a delete hook that physically removes the user's own draft rows on forget. Kept separate from `form-draft` so form-draft consumers without the user-data-rights pipeline don't pull a hard dependency — requires `user-data-rights`, optionalRequires `form-draft`.",
      "toggleableDefault": null,
      "requires": [
        "user-data-rights"
      ],
      "optionalRequires": [
        "form-draft"
      ],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "userData",
          "entityName": "form-draft"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": []
    },
    {
      "name": "inbound-mail-foundation",
      "description": "Plugin host for inbound e-mail (inbox federation) — provider plugins (inbound-provider-imap, later M365 Graph / Gmail REST) register at the `inboundMailProvider` extension point with verify/fetch and optional oauth/watch (live push). The foundation owns three event-sourced streams (mail-account lifecycle, inbound-message with exactly-once ingest via deterministic aggregate ids, mail-thread rollup) and their projections `read_mail_accounts`/`read_inbound_messages`/`read_mail_threads`; PII fields are envelope-encrypted per tenant subject key before every append (crypto-shredding on tenant destroy). Wire `createInboundMailConnectRoutes` (OAuth connect + anonymous callback outside /api) and `createInboundMailSupervisor` (IDLE watch with backoff + reconciliation poll) in bin/server.ts. Consume `inbound-mail-foundation:event:inbound-message-received` from app features to attach business processes.",
      "toggleableDefault": null,
      "requires": [
        "tenant-lifecycle"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "tenantData",
          "entityName": "mail-account"
        },
        {
          "extensionName": "tenantData",
          "entityName": "inbound-message"
        },
        {
          "extensionName": "tenantData",
          "entityName": "mail-thread"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "inbound-mail-foundation:write:connect-account",
        "inbound-mail-foundation:write:disconnect-account",
        "inbound-mail-foundation:write:ingest-message",
        "inbound-mail-foundation:write:update-account"
      ],
      "uiHints": {
        "displayLabel": "Inbound Mail · Foundation",
        "category": "notifications",
        "recommended": false
      }
    },
    {
      "name": "inbound-provider-imap",
      "description": "Registers the `\"imap\"` provider for `inbound-mail-foundation` using imapflow — password/app-password auth covers classic IMAP hosts without any OAuth (plus an XOAUTH2 fallback consuming an access token from the secret slot). Incremental sync via a UIDVALIDITY:lastUid cursor (UIDVALIDITY change triggers a cursor-invalid full resync), initial backfill bounded by the foundation backfill window, and live push via IMAP IDLE for the watch supervisor. Store the per-account connection JSON (host/port/secure/user/password) in the account credential secret slot before the first sync.",
      "toggleableDefault": null,
      "requires": [
        "inbound-mail-foundation",
        "secrets"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "inboundMailProvider",
          "entityName": "imap"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Inbound Mail · IMAP",
        "category": "notifications",
        "recommended": false
      }
    },
    {
      "name": "inbound-provider-inmemory",
      "description": "Registers a scriptable in-process `\"inmemory\"` provider for `inbound-mail-foundation`. Seed messages with `seedInboundMessage(accountId, msg)` — an active watch pushes them immediately (IDLE simulation) and the cursor-based `fetch` re-delivers them on the reconciliation path, exercising the foundation dedup guarantee. Error injection via `failNextFetchWith`/`failNextVerifyWith`/`emitWatchError`; reset state with `resetInboundInMemory()`. For tests, demos and sample apps — not for production.",
      "toggleableDefault": null,
      "requires": [
        "inbound-mail-foundation"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "inboundMailProvider",
          "entityName": "inmemory"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Inbound Mail · In-Memory Provider",
        "category": "notifications",
        "recommended": false
      }
    },
    {
      "name": "jobs",
      "description": "Persistence and operator tooling for background jobs registered via `r.job(...)`. Every job execution writes directly into `store_job_runs` (current status + duration) and `store_job_run_logs` (per-line log rows) from the BullMQ callbacks — no event stream in between (#2243). A daily `retention-cleanup` job deletes runs (and their logs) older than `retentionDays`; an hourly `stale-run-sweep` job marks runs stuck at status `running` past `staleRunTimeoutHours` as `failed` (#2246 — a crashed worker never fires the completion callback, so nothing else ever revisits the row). Exposes `jobs:write:trigger` (manual run) and `jobs:write:retry` (operator retry of a failed run), plus `jobs:query:list`, `jobs:query:details`, and `jobs:query:catalog` (manual jobs) for the operator UI. A job that declares `tenantVisibleFailure` also records its last failed attempt per tenant and subject in `store_tenant_job_failures`, which the tenant itself reads through `jobs:query:failures` — a translation key only, never the provider's message (fw#3079).",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "jobs:write:retry",
        "jobs:write:trigger"
      ],
      "uiHints": {
        "displayLabel": "Jobs · Audit & Operator UI",
        "category": "operations",
        "recommended": false
      }
    },
    {
      "name": "ledger",
      "description": "Double-entry bookkeeping primitive. Owns two event-sourced entities — the per-tenant `account` chart of accounts (`read_ledger_accounts`, self-referential via parentId, typed asset/liability/equity/income/expense) and immutable `transaction` journal entries (`read_ledger_transactions`) whose balanced posting lines are embedded as jsonb (Σ amount = 0, signed integer minor units). The account catalog uses the generic entity handlers (create, update, list, detail); create-transaction books a balanced entry (Σ=0 and ≥2 distinct accounts enforced at the command boundary, referential integrity against accounts checked) and reverse-transaction books its Storno mirror — there is deliberately NO transaction update/delete, so a posted entry is an immutable fact and the audit trail stays intact. Recurring schedules (`read_ledger_schedules`) layer Dauerauftrag templates on top — a schedule names debit/credit accounts, an amount and a monthly interval, from which the Soll (forecast) is a pure projection (projectSchedule) needing no bookings, and confirm-schedule-period materialises one period as an idempotent, reversal-aware balanced entry referencing scheduleReference(id, period); only confirming writes. Balances and reports (balance sheet, P&L, cashflow) derive as pure queries over the postings. Everything financial — banking, accounting, rent cashflow, credits, invoices — models as accounts + balanced transactions on top. Pin roles with createLedgerFeature({ roles }) or adopt the host model with { access }; pass { toggleable: { default: false } } to tier-gate the whole feature.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "ledger:write:account:create",
        "ledger:write:account:update",
        "ledger:write:confirm-schedule-period",
        "ledger:write:create-transaction",
        "ledger:write:reverse-transaction",
        "ledger:write:schedule:create",
        "ledger:write:schedule:update"
      ],
      "uiHints": {
        "displayLabel": "Ledger",
        "category": "data",
        "recommended": false
      }
    },
    {
      "name": "legal-pages",
      "description": "Opt-in wrapper around `template-resolver` text-blocks that registers public HTML routes (default: `/legal/impressum`, `/legal/datenschutz`, `/legal/imprint`, `/legal/privacy`) with Markdown-to-HTML rendering and a boot-time job that hard-fails in production when the required blocks (default: `imprint/de`, `privacy/de`) are not seeded in `SYSTEM_TENANT`. Both are configurable via `routes`/`requiredBlocks` for apps with a different default language or additional pages. Requires `anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID }` and `extraContext.templateResolver` to be wired at app bootstrap; for per-tenant imprints or a custom layout call `template-resolver:query:by-slug` directly.",
      "toggleableDefault": null,
      "requires": [
        "template-resolver"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Legal Pages",
        "category": "content",
        "recommended": false
      }
    },
    {
      "name": "locale-de",
      "description": "German UI and mail copy for framework screens. Opt-in; not included by includeBundled.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": []
    },
    {
      "name": "mail-foundation",
      "description": "Defines the `mailTransport` extension point and a per-tenant `provider` config key that selects which registered transport plugin to use at runtime. Call `createTransportForTenant(ctx, tenantId)` to get an `EmailTransport` ready for sending — use this feature together with at least one `mail-transport-*` feature; use `delivery` + `channel-email` instead when you need the full notification pipeline with delivery attempts and user preferences.",
      "toggleableDefault": null,
      "requires": [
        "config"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [
        {
          "key": "provider",
          "qualifiedName": "mail-foundation:config:provider",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ],
          "readRoles": [
            "TenantAdmin",
            "SystemAdmin",
            "User"
          ]
        }
      ],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Mail Transport Foundation",
        "category": "notifications",
        "recommended": false
      }
    },
    {
      "name": "mail-transport-inmemory",
      "description": "Registers an in-process `\"inmemory\"` provider for `mail-foundation` that buffers sent mails per tenant instead of contacting an SMTP server. Use `getInbox(tenantId)` and `clearInbox(tenantId)` in demo apps and tests; not for production (buffer is process-memory, lost on restart).",
      "toggleableDefault": null,
      "requires": [
        "mail-foundation"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "mailTransport",
          "entityName": "inmemory"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": []
    },
    {
      "name": "mail-transport-smtp",
      "description": "Registers itself as the `\"smtp\"` provider for `mail-foundation` and owns the per-tenant config keys (`host`, `port`, `secure`, `from`, `authUser`) and the encrypted `smtp.password` secret. Tenants set `mail-foundation`'s `provider` config key to `\"smtp\"` to activate it; set the SMTP credentials via the admin UI or a seed handler before sending the first mail.",
      "toggleableDefault": null,
      "requires": [
        "config",
        "secrets",
        "mail-foundation"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "mailTransport",
          "entityName": "smtp"
        }
      ],
      "configKeys": [
        {
          "key": "auth-user",
          "qualifiedName": "mail-transport-smtp:config:auth-user",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ],
          "readRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ]
        },
        {
          "key": "from",
          "qualifiedName": "mail-transport-smtp:config:from",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ],
          "readRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ]
        },
        {
          "key": "host",
          "qualifiedName": "mail-transport-smtp:config:host",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ],
          "readRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ]
        },
        {
          "key": "port",
          "qualifiedName": "mail-transport-smtp:config:port",
          "type": "number",
          "scope": "tenant",
          "default": 587,
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": {
            "min": 1,
            "max": 65535
          },
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        },
        {
          "key": "secure",
          "qualifiedName": "mail-transport-smtp:config:secure",
          "type": "boolean",
          "scope": "tenant",
          "default": false,
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        }
      ],
      "secrets": [
        {
          "qualifiedName": "mail-transport-smtp:secret:smtp-password",
          "scope": "tenant",
          "label": "SMTP password",
          "hint": "Login password at the SMTP server. Brevo/Postmark/SES call it 'API key' or 'SMTP credentials'."
        }
      ],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Mail Transport · SMTP",
        "category": "notifications",
        "recommended": false
      }
    },
    {
      "name": "managed-pages",
      "description": "Tenant-editable, server-rendered public pages with per-tenant branding. Stores one Markdown `page` per `(tenantId, slug, lang)` in the `read_pages` entity table with a `published` gate plus `description`/`ogImage` SEO meta. Registers an anonymous `GET {basePath}/:slug` route that resolves the tenant from the request Host via the app-supplied `resolveApexTenant`, serves only published pages (drafts → 404), renders Markdown through the hardened `page-render` core, and isolates per-tenant content with `Vary: Host`. Ships TenantAdmin/SystemAdmin admin screens (`entityList` + `entityEdit`) backed by convention CRUD handlers (`managed-pages:write:page:{create,update,delete}`, `managed-pages:query:page:{list,detail}`); the app wires nav/workspace onto `managed-pages:screen:page-list`. Also exposes `managed-pages:query:by-tenant-published` (anonymous, SQL-filtered on `published`) for sitemap/discovery consumers such as the `seo` feature. Branding (via `config`, scope tenant): `branding-{title,description,site-url,accent-color,logo-url,layout-preset,custom-css}` keys with write-time validation (hex color, https URLs), a `configEdit` self-service screen (`managed-pages:screen:branding-settings`), and a `managed-pages:query:branding` read that the render path applies as scoped `:root` CSS vars + a logo/title header. Also exposes `managed-pages:write:set` (idempotent slug-keyed upsert, SystemAdmin cross-tenant via `tenantIdOverride`) as a provisioning API. Requires `config` + `anonymousAccess` wired at app bootstrap.",
      "toggleableDefault": null,
      "requires": [
        "config"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [
        {
          "key": "branding-accent-color",
          "qualifiedName": "managed-pages:config:branding-accent-color",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": {
            "regex": "^$|^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"
          },
          "writeRoles": [
            "system",
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        },
        {
          "key": "branding-custom-css",
          "qualifiedName": "managed-pages:config:branding-custom-css",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": {
            "regex": "^[\\s\\S]{0,8000}$"
          },
          "writeRoles": [
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        },
        {
          "key": "branding-description",
          "qualifiedName": "managed-pages:config:branding-description",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": {
            "regex": "^[\\s\\S]{0,500}$"
          },
          "writeRoles": [
            "system",
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        },
        {
          "key": "branding-layout-preset",
          "qualifiedName": "managed-pages:config:branding-layout-preset",
          "type": "select",
          "scope": "tenant",
          "default": "centered",
          "encrypted": false,
          "computed": false,
          "options": [
            "minimal",
            "centered",
            "wide"
          ],
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "system",
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        },
        {
          "key": "branding-logo-url",
          "qualifiedName": "managed-pages:config:branding-logo-url",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": {
            "regex": "^$|^https://[^\\s\"'<>]{1,2000}$"
          },
          "writeRoles": [
            "system",
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        },
        {
          "key": "branding-site-url",
          "qualifiedName": "managed-pages:config:branding-site-url",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": {
            "regex": "^$|^https://[^\\s\"'<>]{1,2000}$"
          },
          "writeRoles": [
            "system",
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        },
        {
          "key": "branding-title",
          "qualifiedName": "managed-pages:config:branding-title",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": {
            "regex": "^[\\s\\S]{0,200}$"
          },
          "writeRoles": [
            "system",
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        }
      ],
      "secrets": [],
      "writeHandlers": [
        "managed-pages:write:page:create",
        "managed-pages:write:page:delete",
        "managed-pages:write:page:update",
        "managed-pages:write:set"
      ],
      "uiHints": {
        "displayLabel": "Managed Pages · Public CMS",
        "category": "content",
        "recommended": false
      }
    },
    {
      "name": "notes-history",
      "description": "Generic, host-agnostic, append-only note history for any entity. Owns one event-sourced entity, `note-entry` (`read_note_entries`), keyed by (entityType, entityId) — so attaching notes adds NO column to the host entity and needs no relational pivot or JOIN. Provides a `create` write-handler (author stamped server-side from the caller, never client-supplied) and a `list` query filterable on entityId. Deliberately append-only: no update or delete handler is registered — a correction is a new entry, not an edit, so who-said-what-when stays reconstructable. Every path uses one access rule — adopt the host's model with createNotesHistoryFeature({ access: { openToAll: { reason } } }) or pin roles with createNotesHistoryFeature({ roles }).",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "notes-history:write:add-note"
      ],
      "uiHints": {
        "displayLabel": "Notes",
        "category": "data",
        "recommended": false
      }
    },
    {
      "name": "notes-history-user-data",
      "description": "GDPR (Art. 20 export / Art. 17 erasure) coverage for the `notes-history` feature's `note-entry` and `note-mention` entities. Mounts the export hook so a user's authored notes are included in the user-data export bundle; the note-entry delete hook looks up `note-mention` for notes that structurally @-mention the forgotten user and crypto-shreds each reached note's row-subject key (after consulting that note's host entity's retention strategy — blockDelete/anonymize win over erasure), keeping the append-only history intact for notes without such a mention. `note-mention`'s own hooks are a no-op — it is a plain FK pointer, not separately exportable content. Kept separate from `notes-history` so notes consumers without the user-data-rights pipeline don't pull a hard dependency — requires `user-data-rights`, optionalRequires `notes-history`.",
      "toggleableDefault": null,
      "requires": [
        "user-data-rights"
      ],
      "optionalRequires": [
        "notes-history"
      ],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "userData",
          "entityName": "note-entry"
        },
        {
          "extensionName": "userData",
          "entityName": "note-mention"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": []
    },
    {
      "name": "personal-access-tokens",
      "description": "Long-lived, revocable Personal Access Tokens for headless HTTP-API access. Stores SHA-256 token hashes in the `store_api_tokens` direct-write table; the plaintext is returned once at creation. `create`/`revoke`/`mine` manage a user's own tokens and `available-scopes` lists the app-declared scope catalog. Bearer tokens carrying the PAT prefix are resolved before jwt.verify (roles resolved live, granted scopes enforced fail-closed at the API boundary) — registered as an auth-foundation tokenVerifier provider, resolved generically by the middleware. Pass { toggleable: { default: false } } to tier-gate the whole feature.",
      "toggleableDefault": null,
      "requires": [
        "user",
        "tenant",
        "auth-foundation"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "tokenVerifier",
          "entityName": "pat"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "personal-access-tokens:write:create",
        "personal-access-tokens:write:revoke"
      ],
      "uiHints": {
        "displayLabel": "Personal Access Tokens",
        "category": "identity",
        "recommended": false
      }
    },
    {
      "name": "rate-limiting",
      "description": "Adds an ops-side `rate-limiting:query:status` query handler for inspecting current bucket state; the actual request throttling is wired automatically by the dispatcher when any handler declares a `rateLimit` option (e.g. `{ per: 'user', limit: 3, windowSeconds: 60 }`) or when you pass `context.rateLimit` to `buildServer`. Loading this feature is optional if you only need L3 per-handler rate limits and have no need for ops introspection.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Rate Limiting · Ops Query",
        "category": "operations",
        "recommended": false
      }
    },
    {
      "name": "readiness",
      "description": "One-call tenant-onboarding probe: `readiness:query:status` rolls up every config key and secret declared `required: true` across all mounted features and reports which still lack a usable value for the calling tenant, plus a single `ready` boolean. Provider-features under an `r.extensionSelector`-declared extension point count only while their provider is the selected one — a tenant on the inmemory mail transport is not blocked by unset SMTP keys. Mount it (together with `config` and `secrets`) when an admin UI needs a settings checklist before the first mail-send or file-write; the per-concern lists stay available via `config:query:readiness` and `secrets:query:list`.",
      "toggleableDefault": null,
      "requires": [
        "config",
        "secrets"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Readiness · Onboarding Probe",
        "category": "operations",
        "recommended": false
      }
    },
    {
      "name": "renderer-foundation",
      "description": "Plugin registry for content rendering (notification HTML, mail HTML, PDF, images): call `foundation.createRendererForTenant({ tenantId, kind })` at render time to get the right renderer plugin selected by kind, with tenant-level overrides via the `rendererPluginByKind` config key. Requires `template-resolver` (declared via `r.requires`). Low-level building block — add `renderer-simple` (or write a custom plugin via `r.useExtension(\"renderer\", name, { kinds, render })`) rather than using this feature alone.",
      "toggleableDefault": null,
      "requires": [
        "template-resolver"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Renderer Foundation",
        "category": "notifications",
        "recommended": false
      }
    },
    {
      "name": "renderer-simple",
      "description": "Default renderer plugin for `kind=\"notification\"`: takes a structured `EmailTemplateData` variable map (with `header`, `sections[]` of text/button objects, and optional `footer`; falls back to `title`/`body` if no structured fields are present) and returns rendered HTML with inline CSS. Requires `renderer-foundation`; sufficient for plain notification emails — swap it for `renderer-mail-html` if you need MJML/Markdown layouts.",
      "toggleableDefault": null,
      "requires": [
        "renderer-foundation"
      ],
      "optionalRequires": [
        "template-resolver"
      ],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "renderer",
          "entityName": "simple"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Renderer · Simple",
        "category": "notifications",
        "recommended": false
      }
    },
    {
      "name": "secrets",
      "description": "Stores arbitrary per-tenant secrets (API keys, tokens, credentials) encrypted at rest using AES-256 with a KEK loaded from `KUMIKO_SECRETS_MASTER_KEY_V1` (and successive versions for rotation). Read a secret in handlers via `ctx.secrets.get(tenantId, handle)`, which automatically appends a `tenantSecretRead` audit event so every access is traceable. A `rotate` job re-encrypts all envelopes after a KEK version bump. The `set`/`delete`/`list` handlers share one access rule — default { roles: [\"TenantAdmin\"] }; adopt the host's role vocabulary with createSecretsFeature({ roles }) or open it to every authenticated tenant user with { access: { openToAll: { reason } } } (larger blast radius: any tenant member can then read secret previews and write/delete secrets, not just admins).",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "secrets:write:delete",
        "secrets:write:set"
      ],
      "uiHints": {
        "displayLabel": "Tenant Secrets",
        "category": "infrastructure",
        "recommended": true
      }
    },
    {
      "name": "seo",
      "description": "Site-discovery + SEO/AEO/GEO surface for apex/content pages. Serves GET /sitemap.xml and GET /llms.txt (both anonymous, revalidate-cached), merging the app-supplied sitemapEntries() callback with legal-pages' fixed routes (includeLegalPages) and/or managed-pages' published slugs (managedPages.resolveApexTenant) when opted in. Serves GET /robots.txt only when robotsPolicy is supplied (default off — the static public/robots.txt already covers the common case). Tenant-scoped config keys (seo:config:seo-organization-{name,logo-url}, seo:config:seo-twitter-site, seo:config:seo-llms-summary, seo:config:seo-default-og-image) feed the Organization JSON-LD helper + llms.txt summary. Also exports pure schema.org JSON-LD builders (organizationSchema/webPageSchema/faqPageSchema) for apps to pass into ApexHead.schemaJson or wrapInLayout({ seo: { schemaJson } }) directly — this feature does not inject JSON-LD on its own routes.",
      "toggleableDefault": null,
      "requires": [
        "config"
      ],
      "optionalRequires": [
        "legal-pages",
        "managed-pages"
      ],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [
        {
          "key": "seo-default-og-image",
          "qualifiedName": "seo:config:seo-default-og-image",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": {
            "regex": "^$|^https://[^\\s\"'<>]{1,2000}$"
          },
          "writeRoles": [
            "system",
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        },
        {
          "key": "seo-llms-summary",
          "qualifiedName": "seo:config:seo-llms-summary",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": {
            "regex": "^[\\s\\S]{0,500}$"
          },
          "writeRoles": [
            "system",
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        },
        {
          "key": "seo-organization-logo-url",
          "qualifiedName": "seo:config:seo-organization-logo-url",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": {
            "regex": "^$|^https://[^\\s\"'<>]{1,2000}$"
          },
          "writeRoles": [
            "system",
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        },
        {
          "key": "seo-organization-name",
          "qualifiedName": "seo:config:seo-organization-name",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": {
            "regex": "^[\\s\\S]{0,500}$"
          },
          "writeRoles": [
            "system",
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        },
        {
          "key": "seo-twitter-site",
          "qualifiedName": "seo:config:seo-twitter-site",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": {
            "regex": "^[\\s\\S]{0,500}$"
          },
          "writeRoles": [
            "system",
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        }
      ],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "SEO / Site Discovery",
        "category": "content",
        "recommended": false
      }
    },
    {
      "name": "sessions",
      "description": "Tracks signed-in clients in the `store_user_sessions` table (one row per JWT, keyed by the `sid`/`jti` claim) and exposes handlers for `mine` (list your sessions), `revoke`, and `revokeAllOthers`. Session creation and revocation on the hot auth path are handled by `createSessionCallbacks()`, wired into `buildServer({ auth: { ... } })` outside the dispatcher; the same callbacks are also registered as an auth-foundation sessionStore provider, resolvable generically via `resolveSessionStore()`. The feature also ships a manual-trigger cleanup job for pruning expired rows and an optional `autoRevokeOnPasswordChange` hook that mass-revokes all sessions for a user whenever their `passwordHash` changes.",
      "toggleableDefault": null,
      "requires": [
        "user",
        "auth-foundation"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [
        "sessions.revokeAllForUser"
      ],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "sessionStore",
          "entityName": "default"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "sessions:write:user-session:revoke",
        "sessions:write:user-session:revoke-all-for-user",
        "sessions:write:user-session:revoke-all-others"
      ],
      "uiHints": {
        "displayLabel": "Sessions · Server-side Logout",
        "category": "identity",
        "recommended": false
      }
    },
    {
      "name": "step-dispatcher",
      "description": "Internal system feature that drains deferred Tier-2 side-effects (currently `webhook.send` and `mail.send`) after their originating transaction commits. Listens via `r.multiStreamProjection` on the `kumiko:system:step.dispatch-requested` system event, performs the actual HTTP or mail delivery, then appends `kumiko:system:step.dispatched` or `kumiko:system:step.dispatch-failed` back onto the same stream so the outcome is recorded in the event log without a separate status table. Mount this feature explicitly via `createStepDispatcherFeature()` in your app's feature list alongside any features that use `r.step.webhook.send` or `r.step.mail.send`.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Step Dispatcher · Deferred Side-Effects",
        "category": "infrastructure",
        "recommended": false
      }
    },
    {
      "name": "subscription-mollie",
      "description": "Mollie payment provider plugin for `billing-foundation`, covering the DACH/EU mid-market use case. Mount via `createSubscriptionMollieFeature({ apiKey, webhookUrl, priceToTier, priceToConfig })` — the factory validates that `priceToTier` and `priceToConfig` keys are identical at boot time. Implements `verifyAndParseWebhook` (lazy Mollie-API fetch + heuristic event-type mapping) and `createCheckoutSession` (customer + first-payment with `sequenceType=\"first\"`); `createPortalSession` and `cancelSubscription` are not available because Mollie has no customer portal and requires a `customerId` that the plugin contract does not carry.",
      "toggleableDefault": null,
      "requires": [
        "billing-foundation"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "subscriptionProvider",
          "entityName": "mollie"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Billing · Mollie",
        "category": "billing",
        "recommended": false
      }
    },
    {
      "name": "subscription-stripe",
      "description": "Stripe payment provider plugin for `billing-foundation`. Reads its Stripe API key + webhook secret from system config keys with `backing:\"secrets\"` (envelope-encrypted in the secrets store under the system tenant) and a `billingLive` **system config** flag — all at runtime, so keys rotate and prod goes live without a redeploy. The `mask` on each key derives the sysadmin settings screen + nav, so no app wires a hand-written config UI. Mount via `createSubscriptionStripeFeature({ priceToTier })`; the optional `apiKey`/`webhookSecret` options are env→secrets bridge fallbacks. The plugin always mounts — `createCheckoutSession` throws `feature_disabled` unless `billingLive` is true, so sk_test_ keys in prod never produce a live checkout. Implements all four provider methods (webhook verify, checkout, portal, cancel).",
      "toggleableDefault": null,
      "requires": [
        "billing-foundation",
        "config",
        "secrets"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "subscriptionProvider",
          "entityName": "stripe"
        }
      ],
      "configKeys": [
        {
          "key": "api-key",
          "qualifiedName": "subscription-stripe:config:api-key",
          "type": "text",
          "scope": "system",
          "default": null,
          "encrypted": true,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": {
            "regex": "^(sk|rk)_(test|live)_"
          },
          "writeRoles": [
            "SystemAdmin"
          ],
          "readRoles": [
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ]
        },
        {
          "key": "billing-live",
          "qualifiedName": "subscription-stripe:config:billing-live",
          "type": "boolean",
          "scope": "system",
          "default": false,
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "system",
            "SystemAdmin"
          ],
          "readRoles": [
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ]
        },
        {
          "key": "webhook-secret",
          "qualifiedName": "subscription-stripe:config:webhook-secret",
          "type": "text",
          "scope": "system",
          "default": null,
          "encrypted": true,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": {
            "regex": "^whsec_"
          },
          "writeRoles": [
            "SystemAdmin"
          ],
          "readRoles": [
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ]
        }
      ],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "Billing · Stripe",
        "category": "billing",
        "recommended": false
      }
    },
    {
      "name": "tags",
      "description": "Generic, host-agnostic tagging for any entity. Owns two event-sourced entities — the per-tenant `tag` catalog (`read_tags`, with optional `color` and `scope`) and `tag-assignment` join rows keyed by (entityType, entityId) (`read_tag_assignments`) — so tagging adds NO column to the host entity and needs no relational pivot or JOIN. Catalog screens are declarative (`entityList` + `entityEdit`) and use convention QNs `tag:{create,update,delete}`; TagManager/TagPicker keep `create-tag`/`update-tag`/`delete-tag`. Also: `assign-tag` (idempotent), `remove-tag` (idempotent) and list queries for the catalog and the assignments. Read which tags an entity has, or which entities carry a tag, by listing `tag-assignment` filtered on `entityId` or `tagId` and composing in the read-layer. A tag with empty `scope` is global; a `scope` of an entityType restricts it to that type in the picker. Every path uses one access rule — adopt the host's model with createTagsFeature({ access: { openToAll: { reason } } }) or pin roles with createTagsFeature({ roles }). Pass { toggleable: { default: false } } to make the whole feature tier-gatable via the tier-engine (no host hook).",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "tags:write:assign-tag",
        "tags:write:create-tag",
        "tags:write:delete-tag",
        "tags:write:remove-tag",
        "tags:write:tag:create",
        "tags:write:tag:delete",
        "tags:write:tag:update",
        "tags:write:update-tag"
      ],
      "uiHints": {
        "displayLabel": "Tags",
        "category": "data",
        "recommended": false
      }
    },
    {
      "name": "template-resolver",
      "description": "Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks. What a record is used for is the `kind` (`notification`, `mail-html`, `document-pdf`, `ai-prompt`, `text-block`, `image-snapshot`).\n\nReading is one call — `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })`. It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes. Writing programmatically goes through the `upsertSystem`, `upsertTenant`, `publish` and `archive` write handlers.\n\nText an editor should be able to change belongs in a collection, declared at mount: `createTemplateResolverFeature({ collections: [{ id, kind, access: { roles }, nav }] })`. It appears in the navigation, and `access` is part of the mount because a bundled feature cannot know the host's roles. Each collection gets its own `<id>-list` / `<id>-item` / `<id>-set` handlers, so the dispatcher enforces the separation.\n\nHow a collection is edited follows from `contentFormat`: `plain` gives a text area, `rich` a small WYSIWYG (bold, italic, headings, lists, links), `markdown` a text area that stores markdown text (same insertable chips as `plain`). All three offer the collection's `variableSchema` as insertable chips and a preview rendered with sample data — an editor sees what `{{firstName}}` becomes without sending a mail. An app can register its own editor for a format; the last `clientFeature` that registers a format wins (a conflict logs a warning).\n\nA collection is tenant-wide by default. With `ownership: \"user\"` every user keeps their own entries — mail signatures being the obvious case. Those rows live in the separate `user-content-entry` entity and count as user data, so mounting one also requires the `template-resolver-user-data` feature and a migration on the app side.\n\nReplaces the former `text-content` feature; its blocks now live here as kind `text-block`.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "template-resolver:write:archive",
        "template-resolver:write:publish",
        "template-resolver:write:reply-snippets-set",
        "template-resolver:write:set",
        "template-resolver:write:signatures-set",
        "template-resolver:write:upsert-system",
        "template-resolver:write:upsert-tenant"
      ],
      "uiHints": {
        "displayLabel": "Template Resolver",
        "category": "notifications",
        "recommended": false
      }
    },
    {
      "name": "template-resolver-user-data",
      "description": "GDPR (Art. 20 export / Art. 17 erasure) coverage for the `template-resolver` feature's `user-content-entry` entity — the per-user half of the content store (mail signatures, personal reply snippets). Mounts the EXT_USER_DATA export hook so a user's own entries land in the export bundle; the delete hook is a deliberate no-op because `content` is annotated `userOwned`, so erasure runs via crypto-shredding (destroying the owner's subject key) rather than a physical delete, which would not survive an event replay. Mount this whenever `createTemplateResolverFeature` declares a collection with `ownership: \"user\"` — the boot guard otherwise refuses the entity. Requires `user-data-rights`, optionalRequires `template-resolver`.",
      "toggleableDefault": null,
      "requires": [
        "user-data-rights"
      ],
      "optionalRequires": [
        "template-resolver"
      ],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "userData",
          "entityName": "user-content-entry"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": []
    },
    {
      "name": "tenant",
      "description": "Registers the three core multi-tenancy entities — `tenant`, `tenant-membership`, and `tenant-invitation` (DB tables `read_tenants`, `read_tenant_memberships`, and `read_tenant_invitations`) — along with write handlers for create/update/disable/enable/addMember/removeMember/updateMemberRoles and the matching queries. It also declares a set of per-tenant config keys (companyName, timezone, locale, SMTP credentials) and system-only keys (priceModel, maxUsers) via `r.config({ keys: { ... } })`. Use this feature in every multi-tenant app; membership resolution and invitation flows depend on it, and `auth-email-password` requires it.",
      "toggleableDefault": null,
      "requires": [
        "config"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [
        {
          "key": "company-name",
          "qualifiedName": "tenant:config:company-name",
          "type": "text",
          "scope": "tenant",
          "default": "",
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        },
        {
          "key": "locale",
          "qualifiedName": "tenant:config:locale",
          "type": "select",
          "scope": "tenant",
          "default": "de",
          "encrypted": false,
          "computed": false,
          "options": [
            "de",
            "en",
            "fr",
            "es"
          ],
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        },
        {
          "key": "max-users",
          "qualifiedName": "tenant:config:max-users",
          "type": "number",
          "scope": "system",
          "default": 50,
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "system"
          ],
          "readRoles": [
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ]
        },
        {
          "key": "price-model",
          "qualifiedName": "tenant:config:price-model",
          "type": "select",
          "scope": "system",
          "default": "basic",
          "encrypted": false,
          "computed": false,
          "options": [
            "basic",
            "pro",
            "enterprise"
          ],
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "system"
          ],
          "readRoles": [
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ]
        },
        {
          "key": "smtp-host",
          "qualifiedName": "tenant:config:smtp-host",
          "type": "text",
          "scope": "tenant",
          "default": null,
          "encrypted": false,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "SystemAdmin"
          ],
          "readRoles": [
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ]
        },
        {
          "key": "smtp-pass",
          "qualifiedName": "tenant:config:smtp-pass",
          "type": "text",
          "scope": "tenant",
          "default": null,
          "encrypted": true,
          "computed": false,
          "options": null,
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "SystemAdmin"
          ],
          "readRoles": [
            "SystemAdmin"
          ]
        },
        {
          "key": "timezone",
          "qualifiedName": "tenant:config:timezone",
          "type": "select",
          "scope": "tenant",
          "default": "Europe/Berlin",
          "encrypted": false,
          "computed": false,
          "options": [
            "UTC",
            "Europe/Berlin",
            "Europe/London",
            "Europe/Paris",
            "Europe/Madrid",
            "Europe/Rome",
            "America/New_York",
            "America/Los_Angeles",
            "America/Sao_Paulo",
            "Asia/Tokyo",
            "Asia/Singapore",
            "Australia/Sydney"
          ],
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        }
      ],
      "secrets": [],
      "writeHandlers": [
        "tenant:write:add-member",
        "tenant:write:cancel-invitation",
        "tenant:write:create",
        "tenant:write:disable",
        "tenant:write:enable",
        "tenant:write:remove-member",
        "tenant:write:tenant:update",
        "tenant:write:update",
        "tenant:write:update-member-roles"
      ],
      "uiHints": {
        "displayLabel": "Multi-Tenant Core",
        "category": "identity",
        "recommended": true
      }
    },
    {
      "name": "tenant-handover",
      "description": "Try-before-signup ownership handover: claims the rows of a declared-transferable entity graph (root plus its parentRef-linked children) from an anonymous/public tenant into the caller's own tenant, legitimized by a row-bound grant the anonymous flow minted. Idempotent, audited via a `<entityType>.tenantHandover` domain event.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "signupHandover",
          "entityName": "tenant-handover"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "tenant-handover:write:claim"
      ],
      "uiHints": {
        "displayLabel": "Tenant Handover",
        "category": "identity",
        "recommended": false
      }
    },
    {
      "name": "tenant-lifecycle",
      "description": "Tenant-destroy lifecycle: request/cancel destruction with compliance-profile grace, auth 410 gate for teardown states, cron trigger after grace, and staged destroy runner (extension fan-out, subject-key erase, tenant tombstone).",
      "toggleableDefault": null,
      "requires": [
        "tenant",
        "compliance-profiles"
      ],
      "optionalRequires": [
        "sessions"
      ],
      "configReads": [],
      "exposesApis": [
        "tenantLifecycle.runDestroySweep"
      ],
      "usesApis": [
        "compliance.forTenant"
      ],
      "extensionsUsed": [
        {
          "extensionName": "tenantLifecycleStatus",
          "entityName": "tenant-lifecycle"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "tenant-lifecycle:write:cancel-destruction",
        "tenant-lifecycle:write:request-destruction"
      ],
      "uiHints": {
        "displayLabel": "Tenant Lifecycle · Destroy",
        "category": "compliance",
        "recommended": false
      }
    },
    {
      "name": "tenant-settings",
      "description": "Per-tenant Currency + Locale defaults, exposed as two config keys (tenant-settings:config:currency, tenant-settings:config:locale) that surface in the self-populating Settings-Hub via `mask`. Pair with defineCreateWithTenantDefaults to auto-fill a money field's currency or a locale field on create instead of hard-coding a literal per entity.",
      "toggleableDefault": null,
      "requires": [
        "config"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [
        {
          "key": "currency",
          "qualifiedName": "tenant-settings:config:currency",
          "type": "select",
          "scope": "tenant",
          "default": "EUR",
          "encrypted": false,
          "computed": false,
          "options": [
            "EUR",
            "USD",
            "GBP",
            "CHF",
            "JPY",
            "SEK",
            "NOK",
            "DKK",
            "PLN",
            "CZK",
            "CAD",
            "AUD",
            "NZD",
            "CNY",
            "INR"
          ],
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        },
        {
          "key": "locale",
          "qualifiedName": "tenant-settings:config:locale",
          "type": "select",
          "scope": "tenant",
          "default": "en",
          "encrypted": false,
          "computed": false,
          "options": [
            "de",
            "en",
            "es"
          ],
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ],
          "readRoles": [
            "all"
          ]
        }
      ],
      "secrets": [],
      "writeHandlers": []
    },
    {
      "name": "tier-engine",
      "description": "Stores a `tier-assignment` entity per tenant (which pricing tier is active) and, when configured with a `TierMap`, registers itself as the `tenantTierResolver` extension so the dispatcher automatically gates `r.toggleable()` features per tenant based on their assigned tier. Call `createTierEngineFeature({ defaultTier, tierMap })` to get full tier composition — including an `inTransaction` entity hook that atomically writes the default tier when a new tenant is created — or use `createTierEngineFeature()` without options for storage-only mode when you manage tier assignment yourself via `composeApp`. A SystemAdmin-only `set-tenant-tier` write plus `get-tenant-tier`/`tier-options` reads let an operator assign a tier to ANY tenant manually — without a billing purchase — stamping `source: \"manual\"` so a future Stripe→tier sync won't overwrite the grant. Apps surface this via the `tier-admin` screen, which is always registered — in storage-only mode (no `tierMap`) it shows an honest \"no tiers configured\" message instead of a tier dropdown.",
      "toggleableDefault": null,
      "requires": [
        "config",
        "tenant"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "tier-engine:write:set-tenant-tier",
        "tier-engine:write:tier-assignment:create",
        "tier-engine:write:tier-assignment:update"
      ],
      "uiHints": {
        "displayLabel": "Tier Engine · Plan Composition",
        "category": "billing",
        "recommended": false
      }
    },
    {
      "name": "user",
      "description": "Manages the cross-tenant user identity: the `read_users` table holds each user's email, `displayName`, global `roles`, `emailVerified` flag, and lifecycle `status` (active / restricted / deletionRequested / deleted). Because users exist above any individual tenant, the feature runs with `r.systemScope()` — membership and tenant-specific roles live in the `tenant` feature instead. Add this feature whenever your app needs a persistent, tenant-agnostic user record that auth and GDPR pipelines can reference.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "principalStatus",
          "entityName": "user"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "user:write:user:create",
        "user:write:user:update"
      ],
      "uiHints": {
        "displayLabel": "User Identity",
        "category": "identity",
        "recommended": true
      }
    },
    {
      "name": "user-data-rights",
      "description": "Implements GDPR Art. 15 (access / `my-audit-log` query), Art. 17 (erasure / `request-deletion` + `cancel-deletion`, plus the anonymous email-verified `request-deletion-by-email` + `confirm-deletion-by-token` flow for lockout-safe self-service, + cron cleanup with grace period), Art. 18 (restriction / `restrict-account` + `lift-restriction`), and Art. 20 (portability / async `request-export` → ZIP via `file-foundation`, Magic-Link download) as first-class HTTP handlers and cron jobs. Each domain feature opts in by calling `r.useExtension(EXT_USER_DATA, \"<entity>\", { export, delete })` — the feature then orchestrates the export and forget pipelines across all registered hooks automatically. When `mail-foundation` and a `mail-transport-*` are mounted, it also sends the four GDPR notifications (export ready/failed, deletion requested/executed) itself with no app callback code, rendered in each recipient’s locale. Requires `user`, `data-retention`, `compliance-profiles`, and `sessions`.",
      "toggleableDefault": null,
      "requires": [
        "user",
        "data-retention",
        "compliance-profiles",
        "sessions"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [
        "userDataRights.runForget",
        "userDataRights.runExport"
      ],
      "usesApis": [
        "compliance.forTenant",
        "retention.policyFor",
        "sessions.revokeAllForUser"
      ],
      "extensionsUsed": [],
      "configKeys": [
        {
          "key": "tenant-model",
          "qualifiedName": "user-data-rights:config:tenant-model",
          "type": "select",
          "scope": "system",
          "default": "multi-user",
          "encrypted": false,
          "computed": false,
          "options": [
            "single-user",
            "multi-user"
          ],
          "bounds": null,
          "pattern": null,
          "writeRoles": [
            "system"
          ],
          "readRoles": [
            "TenantAdmin",
            "Admin",
            "SystemAdmin"
          ]
        }
      ],
      "secrets": [],
      "writeHandlers": [
        "user-data-rights:write:cancel-deletion",
        "user-data-rights:write:confirm-deletion-by-token",
        "user-data-rights:write:lift-restriction",
        "user-data-rights:write:request-deletion",
        "user-data-rights:write:request-deletion-by-email",
        "user-data-rights:write:request-export",
        "user-data-rights:write:restrict-account",
        "user-data-rights:write:run-forget-cleanup"
      ],
      "uiHints": {
        "displayLabel": "User Data Rights · GDPR",
        "category": "compliance",
        "recommended": false
      }
    },
    {
      "name": "user-data-rights-defaults",
      "description": "Registers ready-made `EXT_USER_DATA` export and delete hooks for the bundled entities that hold per-user data: `user` (delete strategy sets email to `deleted-<id>@anonymized.invalid`, nulls `passwordHash`, sets status to `Deleted`; anonymize strategy sets email to `anonymized-<id>@anonymized.invalid` without touching `passwordHash`), `fileRef` (delete removes both the DB row and the storage binary), plus — gated on the source feature being mounted — `user-session` (ip/userAgent, hard-delete), `api-token` (hard-delete = revoke), `in-app-message` (hard-delete), `tenant-invitation` (invitee email forgotten/pseudonymized, inviter link severed), `notification-preference` and user-scoped `config-value` (purged via the forget verb), plus export-only hooks for the events-only aggregates `delivery-attempt` (recipientAddress) and `job-run` (payload) whose erasure runs via crypto-shredding. Mount this alongside `user-data-rights` for standard GDPR compliance; omit it only if your app needs custom anonymization logic for these entities.",
      "toggleableDefault": null,
      "requires": [
        "user",
        "files",
        "user-data-rights"
      ],
      "optionalRequires": [
        "sessions",
        "personal-access-tokens",
        "channel-in-app",
        "tenant",
        "delivery",
        "config",
        "jobs"
      ],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [
        {
          "extensionName": "userData",
          "entityName": "user"
        },
        {
          "extensionName": "userData",
          "entityName": "fileRef"
        },
        {
          "extensionName": "userData",
          "entityName": "user-session"
        },
        {
          "extensionName": "userData",
          "entityName": "api-token"
        },
        {
          "extensionName": "userData",
          "entityName": "in-app-message"
        },
        {
          "extensionName": "userData",
          "entityName": "tenant-invitation"
        },
        {
          "extensionName": "userData",
          "entityName": "notification-preference"
        },
        {
          "extensionName": "userData",
          "entityName": "config-value"
        },
        {
          "extensionName": "userData",
          "entityName": "delivery-attempt"
        },
        {
          "extensionName": "userData",
          "entityName": "job-run"
        }
      ],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [],
      "uiHints": {
        "displayLabel": "User Data Rights · Default Hooks",
        "category": "compliance",
        "recommended": false
      }
    },
    {
      "name": "user-profile",
      "description": "Self-service account page (fw#2312: declarative `projectionDetail` bound to the signed-in user's own `me` row) plus its `change-email` write handler (re-auth via current password, uniqueness check, resets emailVerified and expects the app to trigger the verification flow). Change-password and change-email stay custom `EditExtensionSection` components (re-auth flows a declarative action can't express); account deletion (user-data-rights request/cancel with grace period) is declarative fields + actions. Apps place the screen (id \"profile\") in their logged-in area via r.nav — no `type: \"custom\"` / `__component` registration needed on the app side anymore. Requires `user`, `auth-email-password`, `user-data-rights`, and `user-data-rights-defaults` (so the GDPR boot-validator finds export/delete hooks for `user`'s PII fields once user-data-rights is mounted).",
      "toggleableDefault": null,
      "requires": [
        "user",
        "auth-email-password",
        "user-data-rights",
        "user-data-rights-defaults"
      ],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "user-profile:write:change-email"
      ],
      "uiHints": {
        "displayLabel": "User Profile · Self-Service",
        "category": "identity",
        "recommended": true
      }
    },
    {
      "name": "workflow-runner",
      "description": "Writes the run-envelope (`workflow.run-started` / `workflow.run-completed` / `workflow.run-failed`) for a workflow run onto its `workflow-run` aggregate stream. Provides `startAndRunWorkflow` and `registerEventTrigger` for consumer features to wire up `defineWorkflow`-based workflows against domain events. Registers no workflows itself. Runs the resume loop for suspended `wait`/`retry`/`waitForEvent` steps (`resume-due-runs` job + `resume-run` handler); `registerEventTrigger` wires an event-wakeup subscriber per workflow that declares `awaits` so a `waitForEvent` step resumes as soon as its awaited event arrives, not just on timeout.",
      "toggleableDefault": null,
      "requires": [],
      "optionalRequires": [],
      "configReads": [],
      "exposesApis": [],
      "usesApis": [],
      "extensionsUsed": [],
      "configKeys": [],
      "secrets": [],
      "writeHandlers": [
        "workflow-runner:write:resume-run"
      ],
      "uiHints": {
        "displayLabel": "Workflow Runner",
        "category": "infrastructure",
        "recommended": false
      }
    }
  ]
}
