Skip to content

Instantly share code, notes, and snippets.

@jordotech
Created February 12, 2026 21:44
Show Gist options
  • Select an option

  • Save jordotech/53e5cc04f0e05c004e30a08b54f364d9 to your computer and use it in GitHub Desktop.

Select an option

Save jordotech/53e5cc04f0e05c004e30a08b54f364d9 to your computer and use it in GitHub Desktop.

Feature Flags Architecture — Response

Good doc, Masha. The direction is right. Here's my take with some additional considerations.

Where I Agree

  1. Registry as source of truth for what flags exist — yes, absolutely
  2. Validate flag keys on write — reject keys not in registry. This prevents drift
  3. Merge on read — registry defaults + per-org overrides. This is the core change
  4. Show ALL registry flags in the admin UI — this is actually the highest-impact change

One Important Design Decision: "Override-Only" vs "Explicit State"

Your proposal says DynamoDB should only store overrides from default state. There's a subtle but critical distinction here:

Option A: Override-Only Storage

Registry: composer.default_enabled = true
DynamoDB: (no record for Org A) → Org A gets true (from default)

If we later change registry to default_enabled = false:
DynamoDB: (still no record for Org A) → Org A suddenly gets false

Risk: Changing a registry default silently flips ALL orgs that haven't been explicitly set. In production, this could disable a feature for hundreds of orgs.

Option B: Explicit State Storage (Recommended)

Registry: composer.default_enabled = true
When Org A is created → DynamoDB: Org A, composer = true (explicit copy of default)

If we later change registry to default_enabled = false:
DynamoDB: Org A still has composer = true → Org A keeps the feature
Only NEW orgs get the new default

default_enabled only applies to newly onboarded orgs, not retroactively. This is safer.

Recommendation

Go with Option B (explicit state) for safety. The default_enabled in the registry means: "when a new org is created, pre-populate their flags with these defaults." It does NOT mean "dynamically compute the value at read time."

This matches how LaunchDarkly and most feature flag systems work — defaults are for initialization, not runtime computation.

On the Proposed API Changes

GET /org-feature-flags/{org_id} — Merge with registry

Agree. The response should include ALL registry flags, not just what's in DynamoDB. For flags without a DynamoDB record, use the registry default_enabled value. This gives the frontend a complete picture.

# Pseudocode for the merge
def get_flags_for_org(org_id):
    registry_flags = get_all_registry_flags()
    org_overrides = get_org_flags_from_dynamo(org_id)  # dict keyed by flag_key
    
    result = []
    for flag_def in registry_flags:
        if flag_def.key in org_overrides:
            # Use the explicit per-org value
            result.append({**org_overrides[flag_def.key], "from_registry": True})
        else:
            # No per-org record — use registry default
            result.append({
                "key": flag_def.key,
                "feature_flag_value": 1 if flag_def.default_enabled else 0,
                "user_enabled": flag_def.default_enabled,
                "internal_only": flag_def.internal_only,  # Always from registry
                "from_registry": True,
                "is_default": True  # Frontend can show this differently
            })
    return result

POST /org-feature-flags — Validate against registry

Agree. Return 400 if the flag key doesn't exist in the registry:

if flag_key not in FEATURE_FLAG_REGISTRY:
    raise HTTPException(400, f"Unknown feature flag: {flag_key}. Must be one of: {list(FEATURE_FLAG_REGISTRY.keys())}")

GET /org-feature-flags/by-flag/{flag_key} — Feature-centric view

Nice to have. Would need pagination for large numbers of orgs. Not critical for the first iteration — I'd defer this to a separate ticket unless the admin panel specifically needs it.

What I'd Change About the Framing

The current system isn't "wrong" — it works. The issue is that it lacks guardrails (no validation on write, no merge on read). The data model in DynamoDB doesn't need to fundamentally change. The changes are:

  1. Add validation on write (check key exists in registry)
  2. Add merge on read (fill in missing flags from registry defaults)
  3. Move internal_only to registry (already done in your PR)
  4. Frontend: show all registry flags (biggest user-facing change)

Suggested Implementation Order

  1. PR #357 (current): Add composer-dashboard + chat flags, add internal_only to registry, clean up node flags ✅
  2. Next ticket: Backend — validate flag keys on write, merge on read, move internal_only source of truth to registry
  3. Next ticket: Frontend — show all registry flags, remove "Add flag" freeform input, show is_default indicator
  4. Future: Feature-centric view endpoint, orphaned flag cleanup migration

On Orphaned Flags

You'll need a migration plan for flags that exist in DynamoDB but not in the registry. Options:

  • Ignore on read: The merge logic already handles this — unknown flags just don't appear in the merged result
  • Cleanup migration: One-time script to remove DynamoDB records where the key isn't in the registry
  • Soft deprecation: Log a warning when orphaned flags are encountered, clean up later

I'd go with "ignore on read" first, then a cleanup migration once we're confident the registry is complete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment