PURPOSE

Feature flags client. Bulk-hydrates the feature_flags table at boot and exposes a synchronous lookup for callers. Default-on policy: missing flags (whether un-hydrated or never seeded) return true. Kill switches must be explicitly flipped off in the Supabase dashboard to take effect. This biases toward “feature works” over “feature silently disabled because someone forgot to seed a row.”

OWNS

  • Module-private _cache: Map<string, boolean> of flag name to enabled boolean.
  • Module-private _hydrated: boolean indicating whether hydrateFeatureFlags has completed (success or failure).
  • Default-on lookup semantics for any unknown or missing flag.

READS FROM

  • Supabase feature_flags table, columns flag_name and enabled, via the shared supabase client from ./supabase.

PUSHES TO

  • The internal _cache map (writes only happen during hydrateFeatureFlags).
  • console.warn on hydrate failure.

DOES NOT

  • Does not write to the feature_flags table.
  • Does not subscribe to realtime updates — flags are snapshotted at boot.
  • Does not re-hydrate on demand; one hydration per session.
  • Does not throw on lookup or hydration; hydration failures are swallowed and marked hydrated to avoid a permanent fallback path.
  • Does not provide async lookups; isFlagEnabled is synchronous.
  • Does not differentiate between “missing row” and “row with enabled = true” — both return true.

Signals

  • Hydration failure logs [featureFlags] hydrate failed to the console with the underlying error.

Entry points

  • hydrateFeatureFlags(): Promise<void> — fetches all rows from feature_flags and populates _cache. Sets _hydrated = true even when hydration throws.
  • isFlagEnabled(flagName: string): boolean — synchronous lookup. Returns true if not yet hydrated, returns _cache.get(flagName) ?? true otherwise.

Pattern notes

  • Default-on / fail-open: every branch returns true when the answer is unknown — pre-hydration, hydration error, or missing key in cache. Kill switches are opt-in disablement, not opt-in enablement.
  • Failure path still sets _hydrated = true so the pre-hydration branch (which always returns true) doesn’t become a permanent fallback masking a broken Supabase connection.
  • Module-level singleton cache (no class, no exported reset); state lives for the lifetime of the JS module.
  • Synchronous lookup at call sites; the only async surface is the one-shot hydrateFeatureFlags at boot.