ADR-0016: Two-Tier Feature Flags — Global Kill-Switch + Per-Org Toggle
Status
AcceptedTags
fieldforce, feature-flags, multi-tenancy, adminDecision
Fieldforce route access is gated by two independent flag checks in sequence: (1) the globaladmin_feature_flags row with key = 'fieldforce' (managed by platform admins), and (2) the per-org org_feature_flags row with key = 'fieldforce' for the requesting org (managed by platform support). Effective access = global.enabled AND org.enabled. A missing row in either table defaults to enabled.
Why
Platform admins need to disable fieldforce for all orgs simultaneously (billing suspension, security incident, maintenance). Support teams need to disable fieldforce for a specific org without affecting others. Neither table alone satisfies both requirements. Rejected alternatives:- Single global flag only: Cannot disable fieldforce for one org without touching unrelated orgs. Rejected.
- Single per-org flag only: Disabling globally requires updating every org’s flag row — a multi-row write with no atomic guarantee. Rejected.
- Hard-coded per-org feature in org settings:
org_feature_flagsis a general-purpose table reusable by future modules. Rejected.
How it works
fieldforce/module.go. Missing rows in both tables default to enabled — no row means the feature is on.
Known limitations
- Fail-open on DB error means a database outage does not take fieldforce offline, but a bad flag row cannot be enforced during the outage window.
- Disabling and re-enabling fieldforce mid-request is not atomic — in-flight requests complete with the old flag state.
Rules for agents
- The middleware MUST check global flag first, then org flag — never only one
- DB errors in flag checks MUST be logged but MUST NOT return 5xx — fail open to allow the request
- Missing flag rows MUST be treated as enabled (default on) — never treat missing as disabled
org_feature_flagsis general-purpose — future modules add their ownkeyrows without schema changes; do not fieldforce-specific-ify the table