Skip to content

feat: add database-driven feature flag library (OHE-3101) - #217

Draft
tofarr wants to merge 3 commits into
mainfrom
feat/db-driven-feature-flags
Draft

feat: add database-driven feature flag library (OHE-3101)#217
tofarr wants to merge 3 commits into
mainfrom
feat/db-driven-feature-flags

Conversation

@tofarr

@tofarr tofarr commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

HUMAN:

  • A human has tested these changes.

AGENT:


Why

There is no first-class feature flag mechanism in the application today; gating is done with env-var toggles and ad-hoc config. OHE-3101 asks for a database-driven, no-external-service flag library with user/org/email targeting, rule-based includes/excludes, and REST administration.

Surveying the Python ecosystem, the mature flag libraries (Unleash, Flagsmith, GrowthBook) all assume a separate flag-delivery service, which conflicts with the "no external service" requirement. This PR implements a bespoke library instead, modeled directly on the existing user_authorizations whitelist/blacklist pattern — so it's a natural generalization of a pattern the team already maintains.

Summary

  • Add FeatureFlag + FeatureFlagRule storage models with targeting by user_id, org_id, email_pattern (SQL LIKE), and percentage rollout, plus an async store mirroring user_authorization_store's _internal(session)/public(session=None) overload pattern.
  • Add FeatureFlagService evaluator with exclude-before-include precedence (generalizes whitelist-beats-blacklist), deterministic percentage bucketing (sha256(flag+user)), and a short-TTL in-memory cache.
  • Add an admin REST API at /api/admin/feature-flags (CRUD for flags + rules, plus an evaluate endpoint) gated by a new MANAGE_FEATURE_FLAGS permission granted only to the superadmin super role.
  • Add Alembic migration 150 (chained off 149, the current origin head) and 46 unit tests across store/service/routes.

Issue Number

OHE-3101

How to Test

  1. cd enterprise && poetry install --with dev,test
  2. PYTHONPATH=".:$PYTHONPATH" poetry run pytest enterprise/tests/unit/storage/test_feature_flag_store.py enterprise/tests/unit/server/services/test_feature_flag_service.py enterprise/tests/unit/server/routes/test_feature_flags.py
  3. Apply migration 150 against a dev DB (alembic upgrade head), then exercise the REST endpoints (requires a superadmin caller):
    • POST /api/admin/feature-flags → create a flag
    • POST /api/admin/feature-flags/{key}/rules → add an include/exclude rule
    • POST /api/admin/feature-flags/{key}/evaluate → preview a context
  4. Verify evaluation precedence: exclude beats include; percentage rollout is stable across calls for the same user.

Video/Screenshots

N/A — backend-only change; behavior verified via the unit test suite (46 passing).

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

  • Migration is numbered 150 (not 148) because 148 and 149 already landed on origin/main while this was in progress.
  • The MANAGE_FEATURE_FLAGS permission is granted only to the superadmin super role (parallel to MANAGE_SUPER_ADMINS); no org-scoped role can reach these routes.
  • Rule cascade on flag delete is done explicitly in the store so it works on SQLite (test DB) as well as Postgres, which doesn't enforce FK ondelete=CASCADE by default.
  • This PR was created by an AI agent (OpenHands) on behalf of the repository owner.

Enterprise server image for this PR:

ghcr.io/openhands/enterprise-server:sha-2334d35

@linear

linear Bot commented Aug 20, 2026

Copy link
Copy Markdown
OHE-3101 [Hardening Week] Add a first-class feature flag library for safer releases

Proposed solutions: adopt before building

Prefer an established open-source library/control plane rather than implementing feature flags from scratch.

  1. OpenFeature — vendor-neutral standard with JavaScript and Python SDKs. Likely the best application-facing abstraction because OpenHands can switch providers without coupling product code to one vendor.
  2. Unleash — mature self-hosted control plane with gradual rollout, targeting, auditability, and SDKs.
  3. GrowthBook — self-hosted feature flags and experimentation with JavaScript/Python support.
  4. Flagsmith — self-hosted flags and remote configuration with targeting and SDKs.
  5. PostHog — already used by OpenHands and supports feature flags. It may minimize integration cost, but product code should still use a provider-neutral wrapper and self-hosted/offline deployments must have deterministic local defaults.

Recommended starting point: use OpenFeature as the typed application API, then evaluate PostHog, Unleash, GrowthBook, or Flagsmith as the runtime provider/control plane. The Hardening Week deliverable should include a short build-vs-adopt decision and a small proof of concept with the leading option.

Summary

OpenHands does not have a shared release-safety feature flag contract across Agent Canvas, backend services, SaaS, and self-hosted deployments.

Flags currently appear as hardcoded web-client booleans, VITE_* build variables, deployment environment variables, settings fields, direct Cloud-backend assumptions, and one-off PostHog calls. This makes features harder to stage, disable, test, observe, and retire. It also allows frontend and backend behavior to disagree.

This is a good Hardening Week candidate because an initial library integration, safe provider contract, tests, release runbook, and two representative migrations are bounded work that improves every later release.

Context

Audit performed against OpenHands/main at 2965aca5.

  • src/api/option-service/option.types.ts exposes only hide_llm_settings and hide_users_page; the local adapter hardcodes their values.
  • Build-time gates such as VITE_ENABLE_BROWSER_TOOLS require rebuild/redeploy and cannot provide an emergency runtime kill switch.
  • Some feature availability is inferred from backend.kind === "cloud", conflating backend capability, release state, entitlement, and deployment identity.
  • Backend experiment code has called PostHog directly. ALL-4224 showed that remote flag failure can return None and reach downstream storage without a consistent fallback contract.
  • Existing Linear issues create and remove individual flags, but none defines reusable release infrastructure.

Related but not duplicate:

  • OHE-3044 covers OHE web-client environment wiring.
  • OHE-2642 explores backend-declared Canvas capabilities. Capability discovery should integrate with release flags but is a separate concern.

Required properties

Whichever library/provider is selected must support:

  • typed, centrally registered boolean and variant flags;
  • deterministic safe defaults on timeout, outage, missing flag, or unknown variant;
  • stable percentage rollout and targeted organization/user deployment;
  • backend-authoritative evaluation when frontend and API behavior must agree;
  • self-hosted and offline operation without mandatory SaaS calls;
  • test/in-memory overrides that never contact a remote provider;
  • auditability for production changes;
  • flag owner, purpose, creation date, expiry/review date, and removal issue;
  • CI or reporting that identifies expired flags;
  • a clear separation between feature flags, backend capabilities, entitlements, and authorization.

A client-visible flag must never be treated as authorization. Server endpoints must continue enforcing authentication, permission, entitlement, and data access independently.

Potential implementation from scratch

The following is context and a fallback design if no existing library satisfies the requirements. It is not the preferred first choice.

Typed registry

const flags = defineFeatureFlags({
  hostedVscode: {
    type: "boolean",
    default: false,
    owner: "agent-canvas",
    failurePolicy: "use-default",
    expiresAt: "2026-10-01",
  },
  newConversationFlow: {
    type: "variant",
    variants: ["control", "candidate"],
    default: "control",
    owner: "conversation-platform",
    failurePolicy: "use-default",
    expiresAt: "2026-10-01",
  },
});

Product code would consume a provider-neutral API:

evaluate(flag, context) -> value + evaluation metadata
subscribe(flag, context) -> updates where supported

Provider adapters could include static defaults, test overrides, deployment configuration, backend-evaluated snapshots, and PostHog or another remote provider.

Evaluation rules

  • Each flag has a safe default.
  • Remote evaluation cannot block rendering indefinitely.
  • Unknown flags fail in development/test.
  • Missing flags and provider errors are observable but not fatal.
  • Percentage assignment is deterministic for a stable subject.
  • Secrets, prompts, and repository content are prohibited from evaluation context.
  • Cross-layer features are evaluated authoritatively by the backend; the frontend does not independently guess.

Feature availability should compose separate concerns:

available = backend capability
          AND operational release flag
          AND authorization/entitlement

Lifecycle

  1. Merge with the flag default off.
  2. Deploy dark.
  3. Enable for internal subjects.
  4. Increase targeted/percentage rollout while observing health metrics.
  5. Use a tested kill switch if regressions appear.
  6. Roll out fully.
  7. Remove the flag and dead branch by the review date.

Bounded Hardening Week scope

  1. Compare the open-source options above and record the decision.
  2. Add the selected typed application API and static/test providers.
  3. Add one runtime provider or backend-evaluated snapshot.
  4. Migrate hide_llm_settings as a simple frontend flag.
  5. Migrate one cross-layer flag requiring frontend/backend agreement.
  6. Add tests for enabled, disabled, variant, timeout, outage, missing flag, and unknown variant states.
  7. Document offline/self-hosted behavior and the release/rollback process.
  8. Generate follow-up issues for broader migration; do not convert every environment or backend.kind check in the first PR.

Acceptance criteria

  • An open-source library/provider decision is documented, with adoption preferred over custom code.
  • Product code uses a typed, provider-neutral feature flag API.
  • Static/default, test, and one runtime provider are available.
  • Provider failures have deterministic, tested behavior.
  • Self-hosted/offline deployments do not require a SaaS flag service.
  • Backend-gated features cannot disagree with the frontend.
  • Flags cannot replace authorization checks.
  • Every flag has an owner, safe default, failure policy, and expiry/review date.
  • Expired flags are surfaced automatically.
  • Two representative existing gates are migrated.
  • A runbook covers dark launch, targeted rollout, health checks, kill switch, full rollout, and removal.

This issue was updated by an AI agent (OpenHands) on behalf of the user.

Review in Linear

@github-actions github-actions Bot added the type: feat A new feature label Aug 20, 2026
@tofarr
tofarr marked this pull request as draft August 20, 2026 17:17
Add a first-class feature flag system modeled on the existing
user_authorizations whitelist/blacklist pattern:

- FeatureFlag + FeatureFlagRule storage models with targeting by
  user_id, org_id, email_pattern (SQL LIKE), and percentage rollout
- FeatureFlagStore async store mirroring user_authorization_store's
  _internal(session)/public(session=None) overload pattern
- FeatureFlagService evaluator with exclude-before-include precedence
  (generalizes whitelist-beats-blacklist), deterministic percentage
  bucketing, and a short-TTL in-memory cache
- Admin REST API at /api/admin/feature-flags gated by a new
  MANAGE_FEATURE_FLAGS permission granted only to the superadmin role
- Alembic migration 150 (chained off 149, the current origin head)
- 46 unit tests (store, service, routes)

Co-authored-by: openhands <openhands@all-hands.dev>
… conv limit)

origin/main landed migration 150_add_daily_conversation_limit after this
branch was cut, so the feature-flags migration collided on revision 150.
Renumber to 151 and chain off the new 150 head.

Co-authored-by: openhands <openhands@all-hands.dev>
@tofarr
tofarr force-pushed the feat/db-driven-feature-flags branch from b74084e to 4a335f8 Compare August 24, 2026 16:22
@github-actions

Copy link
Copy Markdown

⚠️ This PR contains migrations. Please synchronize before merging to prevent conflicts.

Resolve merge conflict in enterprise/server/auth/authorization.py by
keeping both the new MANAGE_ORG_QUOTA permission (from main) and the
MANAGE_FEATURE_FLAGS permission (from this branch), and adding both to
the super-role permissions set.

Renumber the feature-flags migration 151 -> 154 to chain after the new
main migrations (151 org daily conv limit, 152 quota increase request,
153 kimi->deepseek settings migration). Single alembic head is now 154.

Co-authored-by: openhands <openhands@all-hands.dev>
@tofarr
tofarr force-pushed the feat/db-driven-feature-flags branch from d6eae8b to 2334d35 Compare August 26, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants