Skip to main content

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_STORIES in libs/stretched-components/src/components/story-teller/cost-stories.ts. Each Story.featureFlag becomes storyFlagKey(featureFlag)story_<normalized_feature_flag>_enabled.
  • Web footer pages: FOOTER_GROUPS in apps/stretched/src/app/shared/components/site-footer/site-footer-links.ts. Every internal footer link becomes webPageFlagKey(slug)web_page_<normalized_slug>_enabled.
  • Web page sections: WEB_SECTION_FLAGS in apps/stretched/src/app/core/feature-flags/section-flags.ts. Each slug becomes webSectionFlagKey(slug)web_section_<normalized_slug>_enabled.
  • Web app launch controls: WEB_APP_FEATURE_FLAGS in apps/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.

SurfaceSource definitionFirebase parameterCode defaultDev Remote ConfigProd Remote ConfigOwner
StoryairlineClass (airline-class)story_airline_class_enabledfalsefalsefalseProduct / Story Engine
StorybedMaking (bed-making)story_bed_making_enabledfalsefalsefalseProduct / Story Engine
StorycarWash (car-wash)story_car_wash_enabledfalsefalsefalseProduct / Story Engine
StorycreditCardPromo (card-promo)story_card_promo_enabledfalsefalsefalseProduct / Story Engine
StorycleaningService (cleaning-service)story_cleaning_service_enabledfalsefalsefalseProduct / Story Engine
StoryclothingCare (clothing-care)story_clothing_care_enabledfalsefalsefalseProduct / Story Engine
Storycoffee (coffee)story_coffee_enabledfalsefalsefalseProduct / Story Engine
Storydishes (dishes)story_dishes_enabledfalsefalsefalseProduct / Story Engine
StoryeatingOut (eating-out)story_eating_out_enabledfalsefalsefalseProduct / Story Engine
StoryescrowTiming (escrow-timing)story_escrow_timing_enabledfalsefalsefalseProduct / Story Engine
StoryfireSavings (fire-savings)story_fire_savings_enabledfalsefalsefalseProduct / Story Engine
Storyfireplace (fireplace)story_fireplace_enabledfalsefalsefalseProduct / Story Engine
Storygasoline (gasoline)story_gasoline_enabledfalsefalsefalseProduct / Story Engine
Storygroceries (groceries)story_groceries_enabledfalsefalsefalseProduct / Story Engine
StorygymVsHomeWeights (gym-vs-home)story_gym_vs_home_enabledfalsefalsefalseProduct / Story Engine
Storyhaircut (haircut)story_haircut_enabledfalsefalsefalseProduct / Story Engine
StoryhomeLocation (home-location)story_home_location_enabledfalsefalsefalseProduct / Story Engine
StoryhomeSize (home-size)story_home_size_enabledfalsefalsefalseProduct / Story Engine
StorycarLeaseVsKeep (lease-vs-keep)story_lease_vs_keep_enabledfalsefalsefalseProduct / Story Engine
Storylighting (lighting)story_lighting_enabledfalsefalsefalseProduct / Story Engine
Storynails (nails)story_nails_enabledfalsefalsefalseProduct / Story Engine
StoryoralCare (oral-care)story_oral_care_enabledfalsefalsefalseProduct / Story Engine
StorypestControl (pest-control)story_pest_control_enabledfalsefalsefalseProduct / Story Engine
StoryphoneUpgrade (phone-upgrade)story_phone_upgrade_enabledfalsefalsefalseProduct / Story Engine
StoryrentVsBuy (rent-vs-buy)story_rent_vs_buy_enabledfalsefalsefalseProduct / Story Engine
StorysickDays (sick-days)story_sick_days_enabledfalsefalsefalseProduct / Story Engine
Storytime (time)story_time_enabledfalsefalsefalseProduct / Story Engine
StorytoiletPaper (toilet-paper)story_toilet_paper_enabledfalsefalsefalseProduct / Story Engine
Storyvacuum (vacuum)story_vacuum_enabledfalsefalsefalseProduct / Story Engine
Storywage (wage)story_wage_enabledfalsefalsefalseProduct / Story Engine
StorywaterFilter (water-filter)story_water_filter_enabledfalsefalsefalseProduct / Story Engine
StorywindowsVsAc (windows-vs-ac)story_windows_vs_ac_enabledfalsefalsefalseProduct / Story Engine
StoryyardCare (yard-care)story_yard_care_enabledfalsefalsefalseProduct / Story Engine
Web appComing-soon landing pageweb_app_coming_soon_enabledtruefalsetrueWeb / Launch
Web footer pageAbout Us (/about)web_page_about_enabledfalsefalsefalseWeb / Footer
Web footer pagePricing (/account)web_page_account_enabledfalsefalsefalseWeb / Footer
Web footer pageBlog (/blog)web_page_blog_enabledfalsefalsefalseWeb / Footer
Web footer pageCareers (/careers)web_page_careers_enabledfalsefalsefalseWeb / Footer
Web footer pageChrome Extension (/chrome-extension)web_page_chrome_extension_enabledfalsefalsefalseWeb / Footer
Web footer pageContact Us (/contact)web_page_contact_enabledfalsefalsefalseWeb / Footer
Web footer pageAPI (/developers)web_page_developers_enabledfalsefalsefalseWeb / Footer
Web footer pageHelp Center (/help)web_page_help_enabledfalsefalsefalseWeb / Footer
Web footer pageLearn (/learn)web_page_learn_enabledfalsefalsefalseWeb / Footer
Web footer pageMerch (/merch)web_page_merch_enabledfalsefalsefalseWeb / Footer
Web footer pagePartner with Us & Affiliates (/partners)web_page_partners_enabledfalsefalsefalseWeb / Footer
Web footer pagePress (/press)web_page_press_enabledfalsefalsefalseWeb / Footer
Web footer pageSecurity (/security)web_page_security_enabledfalsefalsefalseWeb / Footer
Web footer pageStories (/stories)web_page_stories_enabledfalsefalsefalseWeb / Footer
Web footer pageWhy Stretched? (/why-stretched)web_page_why_stretched_enabledfalsefalsefalseWeb / Footer
Web sectionPricing upper tiers (upper-tiers)web_section_upper_tiers_enabledfalsefalsefalseWeb / 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.json
  • firebase/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:

EnvironmentFirebase projectConsole resource
Developmentstretched-devEngage → Remote Config for project stretched-dev
Productionstretched-2c16aEngage → 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

  1. Regenerate and review the target template: bun run firebase:remote-config:generate && bun run firebase:remote-config:check.
  2. 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-dev for development, stretched-2c16a for production). Validate that all parameters are Boolean and that the console preview has the expected defaults/conditions.
  3. 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.
  4. 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 environmentMinimum fetch interval
Development15 minutes
Production1 hour
Emulated/localNo 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.