ADR-0016: Two-Tier Feature Flags — Global Kill-Switch + Per-Org Toggle

Status

Accepted

Tags

fieldforce, feature-flags, multi-tenancy, admin

Decision

Fieldforce route access is gated by two independent flag checks in sequence: (1) the global admin_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_flags is a general-purpose table reusable by future modules. Rejected.

How it works

Schema:
The middleware is registered on the fieldforce route group in 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_flags is general-purpose — future modules add their own key rows without schema changes; do not fieldforce-specific-ify the table

Bad pattern (do not generate)

Good pattern