Feature Flags Meet Static: Enabling Release on Demand with SSG

Feature Flags Meet Static: Enabling Release on Demand with SSG

When I published Release on Demand, I outlined a framework for shipping software with precision—delivering code continuously while choosing exactly when to activate functionality. But when you're building statically-generated websites, how do you retain that dynamism?

This post dives into the core problem of reconciling feature flags with SSG—a challenge we faced while building corewellhealth.org, a JAMstack-powered static site designed for performance, safety, and continuous delivery.


The Problem: SSG and Runtime Feature Toggles Don't Mix Easily

With static site generation, content is baked into HTML during build time. But feature flags are inherently dynamic—they change at runtime. So when we ship a static site, what state should the feature flags be in?

Options we initially explored:

  1. Client-side fetching: Fetch flags after the site loads. But this delays rendering. Visitors would first see the wrong state, then a flash of change as flags load.
  2. In-code defaults: Ship the site assuming some hardcoded defaults. But this meant PRs to change the defaults, defeating the purpose of runtime flexibility.

Neither option was good enough. Our release on demand model demanded more agility.


Early Attempts: Client-Side Fetching + In-Code Defaults

We started by embedding our feature flag system into our CMS and fetching flags on the client. The initial paint used default flag states hardcoded in our React context.

This improved things—users got an immediate render, and updated flag states would take over asynchronously. But a mismatch between the default state and the actual flag state led to UX bugs. For example:

Search is on by default in the static build. Suddenly, our backend search service goes down. We toggle the flag off, but users still see and interact with the feature before their client gets the updated flags.

We needed the static default to sync with the current flag state. But that required a rebuild. And rebuilding meant a PR. Unless...


The Breakthrough: Build-Time Flags as Dynamic Defaults

Our solution: Integrate the feature flags into the SSG build process itself.

What we were missing was the ability to automatically resync the static build with the current flag state. This meant we could:

  • Embed the current flag state into the static build.
  • Serve the latest flag state to users on first load.
  • Flip the flag state in our Feature Flag System.
  • Hydrate the client-side app with the latest flags, overriding the static defaults.
  • Trigger a rebuild whenever the flag state changes, resyncing the static defaults.
  • Serve the latest flag state to users on first load again after a brief build time.
  • Avoid PRs just to update the default state.

This approach also addressed another problem for us inherently: because of our GraphQL integration with the static builds, if our code didn't match our flags, it would break the build and fail. While this was a happenstance, it effectively acted as a form of type-safety for feature flags.

Check out github.com/open-feature/cli for a new project we're working on to actually generate type-safe feature flag clients!

Here’s how we made it work:

  1. Expose feature flags via GraphQL from our Feature Flag System.
  2. During static builds, query the current flags and use them as the default state for the app.
  3. Continue using client-side fetching to hydrate flags at runtime, overriding the defaults if needed.
  4. When a flag is updated in our Feature Flag System, it automatically triggers a rebuild for the affected environment.

This meant:

  • At initial page load, the site reflects the latest known flag state.
  • Once the client-side flags arrive, they override the static defaults in the React context.
  • The mismatch window is minimal—and eliminated after the incremental rebuild finishes.

Real-World Impact: From Theory to Confidence

This hybrid solution gave us the best of both worlds:

  • Static performance and SEO.
  • Dynamic flexibility for release on demand.
  • No more PR bottlenecks just to update default states.
  • Incident response became a matter of toggling a flag and watching the site adjust, automatically and safely.

Even our Change Advisory Board gained confidence. Feature toggles became a formal mechanism for release control—with traceability, safety, and minimal blast radius.


Want to Try This?

I've created a Next.js example repo that demonstrates this pattern. You can find it here:

Release on Demand with SSG Example

This repo shows how to:

  • Define feature flags for your SSG app.
  • Fetch them in getStaticProps or getStaticPaths for static generation.
  • Use them in your React app as defaults.
  • Refetch flags on the client side (e.g., in useEffect).
  • Hydrate them client-side.
  • Retrigger builds based on flag changes.

Final Thoughts

Statically-generated sites and feature flags don’t have to be at odds. By embedding flag state at build time, rehydrating with runtime updates, and triggering fresh static builds on flag changes, we bridged the gap between static delivery and dynamic behavior.

This pattern isn’t just a workaround—it’s a powerful enabler for truly modern web delivery.

Have questions or want to dive deeper? I’d love to chat CNCF Slack. Let’s keep the conversation going!

2025-04-06