Get a quote
Search

From YAML to Flipper: the evolution of feature flag tooling at Kin

In the fall of last year, as I was starting a feature set to cross sell auto insurance at Kin, I experienced one of those down-the-rabbit-hole moments that every engineer is familiar with: a seemingly simple task sends you poring over years of history in GitHub, Googling exhaustively, and scouring third-party library docs—all this research aided in great part by Claude—until you emerge slightly addled but equipped with a better understanding of your company's codebase and a programming concept in general.

What sent me? A feature flag. A simple thing, right?

I thought so, too, until a colleague asked me why I implemented the flag in a particular way. Her innocent question made me realize: I didn't have a good answer. There wasn't one way of managing flags in the app. There were several, sitting side by side, and I had been cargo-culting one of them without really knowing why.

So I put on my amateur historian hat and tried to piece together the story of feature flag tooling evolution at Kin.

What I found was that each tool was adopted for sensible reasons, and each one was eventually outgrown. Now, Kin was in a position to standardize the tooling. This is one of the fun things about working for a tech startup. The company moves quickly, the code changes quickly, and we move from scrappy solutions to standardized solutions.

This article is my retelling of the investigation I did and the lessons I walked away with.

First, a quick refresher on why we care. Feature flags are one of the most useful tools in an engineering team's toolbox. At their best, they let you separate deploying code from releasing it: you can merge and ship work continuously, keep it dark behind a flag, and then turn it on with a runtime change—no new deploy, no PR, no release to cut. And if something goes sideways, you flip the flag back off and the feature quietly disappears from production.

Let's examine Kin's journey with feature flags, moving from static config files to environment variables to purpose-built feature flag libraries.


The Beginning: The feature Gem

The first feature-flagging solution we reached for was the feature gem. It's a straightforward Ruby library that lets you wrap code in conditional checks:

if Feature.active?(:new_checkout_flow)
 render_new_checkout
else
 render_legacy_checkout
end

Here's the first thing that surprised me while digging through the early history: updating a flag value originally required a code change and a code release. In other words, adding or flipping a flag required editing the YAML file, making a PR, merging, and deploying.

That cuts against one of the core promises of feature flags—you really don't want to ship a release just to change a value. So why did it work that way?

It comes down to which storage backend the gem is pointed at. The feature gem actually supports a few different repository patterns:

Backend Runtime toggleable? Storage
YamlRepository No Static YAML file
ActiveRecordRepository Yes Database
RedisRepository Yes Redis

We went with YamlRepository. That meant flags lived in a static YAML file read once at boot, so changing a flag meant a code change and a deploy.

Interestingly, the gem does offer a runtime-toggleable path: the ActiveRecordRepository, which stores flags in the database and can be updated from the Rails console, a rake task, or even raw SQL:

# Define features in the FeatureToggle table, e.g. in db/schema.rb
FeatureToggle.create!(name: "ActiveFeature", active: true)
FeatureToggle.create!(name: "InActiveFeature", active: false)

But the ActiveRecordRepository path wasn't taken; instead, environment variables ended up used alongside the YamlRespository implementation.

The First Evolution: Environment Variables Enter the Picture

The next move was to stop hardcoding flag values in the YAML file and source them from environment variables instead. So the file went from this:

my_feature: true

To this:

my_feature: <%= ENV['FRESHSALES_ENABLED'] == "true" %>

Now you could change a flag by updating an environment variable in AWS Secrets Manager instead of shipping code. Eventually every flag value in the file got swapped out for an environment variable lookup.

Is this a standard pattern? No. It's linking together three pieces of functionality:

  1. Rails's ability to use ERB inside YAML config files
  2. The feature gem's YamlRepository
  3. AWS Secrets Manager for ENV injection

It gave us externalized configuration while keeping the feature gem's familiar API. Clever—but, in hindsight, an early hint that we were starting to outgrow the tool.

The Second Evolution: Bypassing the Gem Entirely

Once flag values were really just environment variables, an obvious question popped up: why bother routing through the gem at all? If a flag is just an env var, you can check it directly:

if ENV["PRODUCT_API_SYNC_ENABLED"] == "true"
 sync_via_api
else
 sync_via_activerecord
end

And that's exactly what happened: this direct-checking pattern became the most common approach in the codebase. We were left with two parallel systems: the gem and direct ENV checks, both ultimately reading from environment variables, with the gem just adding a layer of indirection. (This, by the way, is the exact thing that sent me down the rabbit hole: seeing env vars used for feature flags in two different ways and wanting to know why that was.)

Standardizing access with EnvWrapper

Reading environment variables directly all over a big codebase brings its own headaches: no consistency and no visibility into which variables are even in use. To tame that, we introduced an EnvWrapper that standardizes ENV access and sprinkles in some observability:

class EnvWrapper
 def self.[](key)
 Kin::Metrics.track(category: "environment_variable", metric: key)
 ENV[key]
 end

 def self.fetch(key, default = nil)
 Kin::Metrics.track(category: "environment_variable", metric: key)
 ENV.fetch(key, default)
 end
end

Every access gets tracked in DataDog, so we can actually see which environment variables are being read in the wild. There's even a custom RuboCop cop that enforces the convention, detecting direct ENV usage in application code and nudging you toward EnvWrapper instead.

Hold on… are environment variables even a good fit for feature flags?

This is the point in my investigation where I had to take a big step back, because it's a genuinely unusual choice. Environment variables are the go-to for configuration—database URLs, API keys, that sort of thing. Feature flags want different things, and using one tool for the other comes with some real tradeoffs:

Limitation Impact
No targeting Can't enable for specific users or percentages
No gradual rollouts All-or-nothing; 100% on or 100% off
No audit trail No record of who changed what, and when
No built-in UI No out-of-the-box flag management
No type safety Everything is a string; hello, "true" vs true bugs
No validation A typo in a flag name just silently reads as nil

As these limitations piled up, the case for a real, purpose-built tool got harder to ignore. Which brings us to the cleanup going on today to consolidate feature flag tooling.

The Third Evolution: Flipper

Last year, we kicked off the move to Flipper, a modern feature flag library that's become something of a de facto standard in the Rails world. Flipper keeps flags in the database, so changes take effect immediately—no deploy required. The migration behind it is refreshingly simple; it just creates a flipper_features table:

class CreateFlipperTables < ActiveRecord::Migration[7.1]
 def up
 create_table :flipper_features do |t|
 t.string :key, null: false
 t.timestamps null: false
 end
 add_index :flipper_features, :key, unique: true
 end

 def down
 drop_table :flipper_features
 end
end

What Flipper brings to the table

Aspect feature gem + ENV Flipper
Storage YAML file → ENV Database
Admin UI None (edit AWS console) Built-in at /flipper
Actor targeting N/A Supports user-specific flags
Percentage rollouts N/A Built-in

Flipper's initializer even sets up an admin UI, so flags can be viewed and toggled directly. Feature definitions live in a YAML file alongside ownership metadata, so every flag comes with a description and an owning team:

features:
 - name: enable_report_manual_uploads
 description: Allow manual upload of reports
 owner: Accounting Team

Access to the Flipper UI is gated by roles, so that only the employees with appropriate permissions can update flags in each environment.

The move to Flipper and the cleanup of the previous two paradigms solves the Rails implementation of feature flagging. But it's never quite that simple, is it? Kin's application ecosystem is more than just Ruby on Rails, and Flipper isn't always the right choice once we consider apps beyond pure Ruby on Rails.

A Different Side of the Story: Client-Side Components

Here's where things got really interesting for me. I'm an engineer with experience primarily building user interfaces for SPAs with Vue, React, and Angular. So it was interesting for me to consider how feature flag tooling would work beyond Ruby on Rails.

Flipper is Ruby-only, and it evaluates flags on the server at request time. That's a clean fit for server-rendered views. But our apps supplement those views with client-side behavior (most often through Lit components) and I had to wonder if or how Flipper could be used to provide flag evaluations client-side, like in a Lit component.

Some research revealed several patterns in the wild for hybrid Rails apps:

Pattern 1: the server hands flags to the client via window:

<script>
 window.FEATURE_FLAGS = {
 newFeature: <%= Flipper.enabled?(:new_feature).to_json %>
 };
</script>

Pattern 2: data attributes:

<my-component data-enable-feature="<%= Flipper.enabled?(:new_feature) %>">

Both of these are functional. But both of them also have some real drawbacks.

Why these patterns are risky

Information leakage. Anyone can pop open DevTools and read your flags:

// The whole feature flag schema, right there for the taking:
window.FEATURE_FLAGS
// { secretBetaFeature: false, employeeOnlyTools: false, internalDashboard: false }

Even if a flag is false, you've just announced that the feature exists, which tells a curious (or malicious) user exactly what to go poke at.

Tampering. Client-side values can be edited on the spot:

window.FEATURE_FLAGS.premiumPaidFeature = true;
// A client-side component shouldn't trust this value

The client should never be trusted. Any client-side check can be bypassed, and the same goes for data attributes.

Type coercion. Data attributes are always strings, which sets you up for a classic bug:

element.dataset.enableFeature // "true" (a string, not a boolean)
element.dataset.enableFeature === true // false
element.dataset.enableFeature === "true" // true

The deeper issue is that all of these patterns try to use server-side flag evaluation to drive client-side behavior, and those two worlds want fundamentally different things:

Aspect Server-side flags Client-side needs
Evaluation time Request time Anytime
Update mechanism Page refresh Real-time
Trust model Server is trusted Client is untrusted
Security Flag values hidden Flag values exposed

The right tool: client-side SDKs

For genuinely client-side feature flags, the right tool is a client-side SDK. Tools like SplitIO and LaunchDarkly offer JavaScript SDKs that:

  • Fetch only the flags relevant to the current user
  • Cache locally and stream updates in real time
  • Handle the trust boundary properly
  • Can't be tampered with, because evaluation happens on the provider's server

This is the same model you see all over single-page-application frameworks like Vue, React, and Angular: you configure flags in a third-party tool, and client code asks for the values it's allowed to see. For a hybrid Rails app with client-side components that need secure, real-time flags, a client-side SDK is the right fit. This is a complement to Flipper on the server, not a replacement for it.

As it turns out, I had already used SplitIO at Kin in a smaller Angular client app. I realized that it could be used client-side from our Rails app in situations where it was appropriate.

The tech stack was officially covered for a comprehensive feature flagging solution!


What I took away

As with just about everything in programming, there are a lot of ways to implement feature flags, and the "right" one really depends on what you need.

My investigation led me through several different approaches and tooling: the feature gem, using environment variables, Flipper (a server-side Ruby-specific tool), and SplitIO (a client-side tool whose flag definitions aren't even stored in our own codebase).

Of course, there are plenty of other tools and strategies out there.

One reflection that really stuck with me throughout this whole process of discovery was that behind every step in the evolution of feature flag tooling at Kin was a decision about tradeoffs. Every once in awhile I'm reminded that there is no perfect solution, there is no tool that's right for every situation, and applications are pieces of software that are constantly in flux.

I went looking for a quick answer and walked away with something far more useful: a better understanding of how the codebase I'm working in evolved.

Taking the time to understand why a system looks the way it does—not just how to copy the nearest example—is one the most rewarding parts of programming, and surely one of the best ways to grow as an engineer. At Kin, I'm grateful to have the freedom to go on these journeys and end up better for it.