Skip to content

Capability Gating

Daniel Hokanson edited this page Aug 30, 2026 · 3 revisions

Capability Gating

Canonical reference: docs/functional-reference/capabilities.md. The authoritative list is the code: forge.api/Capabilities/CapabilityCatalog.cs in forge-api.

What it is

Capability gating is a per-install feature-flag substrate that lets one codebase ship identical binaries to a two-person trade shop, a 25-person job shop, an ISO-13485 medical-device manufacturer and a 500-person enterprise. Each install turns on a different set of features; the code is the same everywhere.

Every capability has a stable code in the form CAP-{AREA}-{NAME}CAP-MD-CUSTOMERS, CAP-INV-LOTS, CAP-EXT-AI-ASSISTANT. Its enabled state lives in the capabilities table, and controllers, MediatR commands, HTTP routes and UI surfaces all read that state to decide whether a feature exists for this install.

Roughly 170 capabilities are registered at the time of writing; count the catalog rather than trusting a number in any doc, this one included.

Why it exists

The canonical example is the accounting boundary. An install can run with built-in lightweight invoicing and payments (CAP-ACCT-BUILTIN) or with QuickBooks/Xero/Sage as the source of truth (CAP-ACCT-EXTERNAL) — never both. Capabilities make that mutual exclusion declarative and enforced at a single gate layer, instead of scattering if (isStandalone) branches through the codebase. See Accounting Modes.

Functional areas

Code Area
IDEN Identity, auth, users
MD Master data — customers, parts, BOM, vendors, work centers, UOM
P2P Procure-to-pay — POs, RFQs, receiving, bills, payments
O2C Order-to-cash — quotes, sales orders, pick/pack/ship, invoicing, cash
MFG Manufacturing — work order release, material issue, labour, completion, shop floor
PLAN Planning — MRP, MPS, forecasting, capacity
INV Inventory — core stock, lots, serials, cycle counts
QC Quality — inspection, NCR, SPC, recall
MAINT Maintenance — PM, breakdown, asset lifecycle
ACCT Accounting — external provider, built-in books, full GL, expenses
COSTING Standard costing and overhead
HR Human resources — hiring, leave, payroll, training
RPT Reports — operational, financial, OEE, dashboards
CROSS Cross-cutting — notifications, documents, EDI
EXT Extensions — kanban, chat, AI assistant
MOBILE Mobile app surfaces
PS Professional-services shapes

Areas drive the grouping in the admin UI. Each catalog row carries its code, area, display name, description, a default-on flag, and an optional role gate for who may manage it.

Where the gate actually fires

Three layers, all reading the same snapshot:

  1. CapabilityGateMiddleware — HTTP request level, keyed off [RequiresCapability] attributes on controllers and actions.
  2. CapabilityGateBehavior — a MediatR pipeline behaviour, so a command is refused even if it is reached from somewhere other than its controller.
  3. The UI — surfaces are hidden rather than shown-then-denied.

A disabled capability yields a clean refusal rather than a 500 or a half-rendered screen. CapabilityDependencyResolver enforces the relationships between capabilities, so enabling one that depends on another can't leave an install in an incoherent state, and CapabilitySnapshotProvider caches the resolved state per request.

Two rules worth knowing before you add a gate:

  • Controllers marked with a class-level bootstrap attribute override action-level gates — the middleware checks bootstrap first. Mixed controllers must therefore be attributed action by action.
  • Every controller is expected to carry a gate. An architecture test enforces this by reflection, with a shrink-only legacy register for exemptions.

Presets and the discovery wizard

Nobody wants to answer 170 yes/no questions. Forge ships nine named presets plus an explicit Custom opt-out, and a discovery wizard that recommends one from a short interview about how the shop actually works.

The recommendation engine is stateless — it maps answers to a preset. Applying a preset is a single bulk toggle of the capability set: start from the catalog's default-on baseline, remove what the preset explicitly disables, add what it explicitly enables. PRESET-CUSTOM is empty by design; applying it substitutes the catalog defaults.

Modules (ModuleCatalog.cs) are the coarser, user-facing grouping the wizard speaks in — inventory, purchasing, sales and quoting, production, shipping, invoicing, quality, planning and scheduling, people.

Terminology is configuration, not a fork

A related mechanism worth knowing: TerminologyService and TerminologyPipe remap entity labels as admin configuration. Whether a shop calls the central object a Job, a Work Order or a Project is a settings row, not a code change.

Clone this wiki locally