Feature Flags and Firebase Remote Config
See also: Local Development · Mobile App · Testing · Tooling and Scripts
Stretched uses typed, boolean feature flags to control availability of web pages, page sections, and runtime stories. The web and mobile applications use the Firebase JavaScript/Web Remote Config SDK; neither app uses a native Remote Config plugin.
Source of truth and defaults
The source definitions are deliberately close to the features they control:
- Stories:
ALL_STORIESinlibs/stretched-components/src/components/story-teller/cost-stories.ts. EachStory.featureFlagbecomesstoryFlagKey(featureFlag)—story_<normalized_feature_flag>_enabled. - Web footer pages:
FOOTER_GROUPSinapps/stretched/src/app/shared/components/site-footer/site-footer-links.ts. Every internal footer link becomeswebPageFlagKey(slug)—web_page_<normalized_slug>_enabled. - Web page sections:
WEB_SECTION_FLAGSinapps/stretched/src/app/core/feature-flags/section-flags.ts. Each slug becomeswebSectionFlagKey(slug)—web_section_<normalized_slug>_enabled. - Web app launch controls:
WEB_APP_FEATURE_FLAGSinapps/stretched/src/app/core/feature-flags/web-app-feature-flags.ts. These carry explicit development and production Remote Config defaults because they are not generated from a footer destination. - Typed helper and runtime contract:
libs/feature-flags. Keys are lower-case Firebase-safe segments; hyphens normalize to underscores.
Most flags default to false in code and in both checked-in Remote Config
templates. The temporary launch-only web_app_coming_soon_enabled flag is the
intentional exception: its safe in-code default is true so a missing or slow
Remote Config response cannot expose the pre-launch app. Its Remote Config
default is false in development and true in production. Development and the
web emulated environment explicitly override it to false, keeping normal local,
dev, e2e, and visual work fully navigable. The production deploy configuration
has no local override. A Remote Config flag is an availability/rollout switch,
not authorization; the application must still enforce authentication, Firestore
rules, API authorization, and any paid-entitlement checks independently. A
disabled footer route is a UX gate: Angular falls through to the application's
not-found route; it is not an access-control boundary.
| Surface | Source definition | Firebase parameter | Code default | Dev Remote Config | Prod Remote Config | Owner |
|---|---|---|---|---|---|---|
| Story | airlineClass (airline-class) | story_airline_class_enabled | false | false | false | Product / Story Engine |
| Story | bedMaking (bed-making) | story_bed_making_enabled | false | false | false | Product / Story Engine |
| Story | carWash (car-wash) | story_car_wash_enabled | false | false | false | Product / Story Engine |
| Story | creditCardPromo (card-promo) | story_card_promo_enabled | false | false | false | Product / Story Engine |
| Story | cleaningService (cleaning-service) | story_cleaning_service_enabled | false | false | false | Product / Story Engine |
| Story | clothingCare (clothing-care) | story_clothing_care_enabled | false | false | false | Product / Story Engine |
| Story | coffee (coffee) | story_coffee_enabled | false | false | false | Product / Story Engine |
| Story | dishes (dishes) | story_dishes_enabled | false | false | false | Product / Story Engine |
| Story | eatingOut (eating-out) | story_eating_out_enabled | false | false | false | Product / Story Engine |
| Story | escrowTiming (escrow-timing) | story_escrow_timing_enabled | false | false | false | Product / Story Engine |
| Story | fireSavings (fire-savings) | story_fire_savings_enabled | false | false | false | Product / Story Engine |
| Story | fireplace (fireplace) | story_fireplace_enabled | false | false | false | Product / Story Engine |
| Story | gasoline (gasoline) | story_gasoline_enabled | false | false | false | Product / Story Engine |
| Story | groceries (groceries) | story_groceries_enabled | false | false | false | Product / Story Engine |
| Story | gymVsHomeWeights (gym-vs-home) | story_gym_vs_home_enabled | false | false | false | Product / Story Engine |
| Story | haircut (haircut) | story_haircut_enabled | false | false | false | Product / Story Engine |
| Story | homeLocation (home-location) | story_home_location_enabled | false | false | false | Product / Story Engine |
| Story | homeSize (home-size) | story_home_size_enabled | false | false | false | Product / Story Engine |
| Story | carLeaseVsKeep (lease-vs-keep) | story_lease_vs_keep_enabled | false | false | false | Product / Story Engine |
| Story | lighting (lighting) | story_lighting_enabled | false | false | false | Product / Story Engine |
| Story | nails (nails) | story_nails_enabled | false | false | false | Product / Story Engine |
| Story | oralCare (oral-care) | story_oral_care_enabled | false | false | false | Product / Story Engine |
| Story | pestControl (pest-control) | story_pest_control_enabled | false | false | false | Product / Story Engine |
| Story | phoneUpgrade (phone-upgrade) | story_phone_upgrade_enabled | false | false | false | Product / Story Engine |
| Story | rentVsBuy (rent-vs-buy) | story_rent_vs_buy_enabled | false | false | false | Product / Story Engine |
| Story | sickDays (sick-days) | story_sick_days_enabled | false | false | false | Product / Story Engine |
| Story | time (time) | story_time_enabled | false | false | false | Product / Story Engine |
| Story | toiletPaper (toilet-paper) | story_toilet_paper_enabled | false | false | false | Product / Story Engine |
| Story | vacuum (vacuum) | story_vacuum_enabled | false | false | false | Product / Story Engine |
| Story | wage (wage) | story_wage_enabled | false | false | false | Product / Story Engine |
| Story | waterFilter (water-filter) | story_water_filter_enabled | false | false | false | Product / Story Engine |
| Story | windowsVsAc (windows-vs-ac) | story_windows_vs_ac_enabled | false | false | false | Product / Story Engine |
| Story | yardCare (yard-care) | story_yard_care_enabled | false | false | false | Product / Story Engine |
| Web app | Coming-soon landing page | web_app_coming_soon_enabled | true | false | true | Web / Launch |
| Web footer page | About Us (/about) | web_page_about_enabled | false | false | false | Web / Footer |
| Web footer page | Pricing (/account) | web_page_account_enabled | false | false | false | Web / Footer |
| Web footer page | Blog (/blog) | web_page_blog_enabled | false | false | false | Web / Footer |
| Web footer page | Careers (/careers) | web_page_careers_enabled | false | false | false | Web / Footer |
| Web footer page | Chrome Extension (/chrome-extension) | web_page_chrome_extension_enabled | false | false | false | Web / Footer |
| Web footer page | Contact Us (/contact) | web_page_contact_enabled | false | false | false | Web / Footer |
| Web footer page | API (/developers) | web_page_developers_enabled | false | false | false | Web / Footer |
| Web footer page | Help Center (/help) | web_page_help_enabled | false | false | false | Web / Footer |
| Web footer page | Learn (/learn) | web_page_learn_enabled | false | false | false | Web / Footer |
| Web footer page | Merch (/merch) | web_page_merch_enabled | false | false | false | Web / Footer |
| Web footer page | Partner with Us & Affiliates (/partners) | web_page_partners_enabled | false | false | false | Web / Footer |
| Web footer page | Press (/press) | web_page_press_enabled | false | false | false | Web / Footer |
| Web footer page | Security (/security) | web_page_security_enabled | false | false | false | Web / Footer |
| Web footer page | Stories (/stories) | web_page_stories_enabled | false | false | false | Web / Footer |
| Web footer page | Why Stretched? (/why-stretched) | web_page_why_stretched_enabled | false | false | false | Web / Footer |
| Web section | Pricing upper tiers (upper-tiers) | web_section_upper_tiers_enabled | false | false | false | Web / Pages |
The table is generated from the current source registries. Do not hand-edit it;
run bun run firebase:remote-config:generate after adding, renaming, or
removing a story, internal footer link, web section, or web-app flag.
Checked-in templates and validation
tools/generate-remote-config.ts is the canonical generator and validator. It
loads the runtime ALL_STORIES, parses the footer and web-section registries,
includes the web-app launch registry, uses the typed key helpers, and fails on
duplicate or normalized key collisions. It emits Firebase-native BOOLEAN
parameters. Most use
"defaultValue": { "value": "false" }; flags with an intentional
environment-specific launch default may emit true in only one template.
The generated, checked-in inputs are intentionally separate because their reviewed defaults may differ:
firebase/remote-config/stretched-dev.template.jsonfirebase/remote-config/stretched-2c16a.template.json
Run these commands locally and in a documentation/configuration CI gate:
bun run firebase:remote-config:generate # rewrite templates and flag table
bun run firebase:remote-config:check # fail if any generated output is stale
bun run firebase:remote-config:test # focused generator tests
The generator is offline: it never deploys, publishes, or modifies a Firebase project.
Required Firebase resources
Create and manage the following two independent Firebase Remote Config resources in the Firebase console:
| Environment | Firebase project | Console resource |
|---|---|---|
| Development | stretched-dev | Engage → Remote Config for project stretched-dev |
| Production | stretched-2c16a | Engage → Remote Config for project stretched-2c16a |
For each project, enable/use its Remote Config service and create every
parameter from that project's checked-in template. Each parameter must be type
Boolean with the default shown in that template; do not create a String
parameter containing "true" or "false". No condition is required for the
baseline template. Use console conditions only
for a deliberate, reviewed rollout and preserve the unconditional default.
There is no Firebase Remote Config emulator. Emulator/local application mode never calls Remote Config and should not be represented by a third Remote Config project or template.
Upload, validate, promote, and rollback
- Regenerate and review the target template:
bun run firebase:remote-config:generate && bun run firebase:remote-config:check. - In the target project's Engage → Remote Config console, first inspect
the currently published template and record its version number. Import or
enter the parameters from the matching checked-in template (
stretched-devfor development,stretched-2c16afor production). Validate that all parameters are Boolean and that the console preview has the expected defaults/conditions. - Publish development first. Smoke-test a fresh web and mobile session, then promote the same reviewed parameter/condition change to production. Never copy a development template over production without comparing its current template/version and intended conditions.
- For an urgent rollback, in the same project's Remote Config console use
Version history to select the known-good version and restore it, or
publish a reviewed change that returns the affected flag to
true. Verify the resulting version before announcing the rollback.
For API/CLI automation performed outside this repository, use Remote Config's
optimistic concurrency correctly: read the current template and its ETag,
then update with If-Match: <that exact ETag>. Treat HTTP 412 Precondition Failed as a concurrent edit: re-read, merge/review the live
change, and retry with the new ETag. Do not use If-Match: * for routine
changes, because it can overwrite a console edit. Keep the generated template
free of live version/ETag metadata; those belong to the target project's
published template, not source control.
Runtime lifecycle and local overrides
Remote mode supplies code defaults to the Firebase Web SDK, activates a cached configuration during initialization, and fetches in the background. A fetched value is normally activated on the next navigation, so a user does not see a page or story disappear in the middle of their current view. The temporary coming-soon gate is the one deliberate exception: while that gate is visible, the web shell activates its completed fetch immediately because the page has no navigation controls and must be able to release already-open visitors at launch.
Minimum fetch intervals are intentionally conservative:
| Build environment | Minimum fetch interval |
|---|---|
| Development | 15 minutes |
| Production | 1 hour |
| Emulated/local | No Remote Config fetch (local mode) |
Development uses the stretched-dev Remote Config resource with the
coming-soon launch gate explicitly off. Local/emulated builds set
forceAllEnabled: true, making registered features available while remaining
strictly local and never initializing Remote Config. An explicit
localOverrides entry takes precedence over forceAllEnabled and fetched
values; the development and emulated web configurations use that precedence to
keep the coming-soon launch gate off.
bun run start:prod uses the standard production configuration and reads the
actual stretched-2c16a Remote Config values. It has no local overrides, so it
matches bun run build:prod feature-flag behavior. Unknown override keys are
ignored and code defaults remain in effect for omitted keys. Do not add an
emulator URL or attempt to mock a Remote Config emulator.
Story dependencies and mobile caveat
A disabled runtime story is removed from the available story catalog, and a
direct story URL redirects to /stories. Its transitive dependents are also
hidden: a story cannot be exposed if any story it depends on is disabled. Plan
story rollout/rollback in dependency order, especially when disabling a
foundation story.
The separate compact stories/*.json catalog (Story Engine) is
authoring-only — its /stories/map viewer was removed 2026-07-29. Those
records are not runtime Story objects and do not carry Story.featureFlag;
per-story Remote Config switches apply only to the runtime /stories catalog.
Mobile uses the same Firebase Web SDK and the same flag keys/defaults as web. It therefore follows the same cache/fetch/next-navigation lifecycle, but a mobile client may keep an older cached template while it is backgrounded or has no network. Test a cold/fresh mobile launch after publish and do not rely on a Remote Config change as a security or emergency-access-control mechanism.