Feature flags without a vendor: one table, one function, one rule

For most teams, feature flags are a Postgres table, a hashing function and a policy about expiry dates. Here is ours, and when we would pay for a product instead.

$_$ test
Illustration for “Feature flags without a vendor: one table, one function, one rule”.
On this page 4 sections

This is an opinion post, so here is the opinion up front: a team of under fifty engineers, shipping one or two services, does not need a feature flag vendor. It needs a table, about thirty lines of code, and a rule that every flag has an owner and an expiry date.

We ran a hosted flag service for two years. It was fine. It also became the place where 140 flags went to be forgotten, it added a network dependency to our boot sequence, and the bill grew with our monthly active users rather than with anything we used. When we moved off it, the replacement took two days.

The table

Flags live in the same Postgres database as everything else. That means they are backed up, they show up in the audit log, and changing one is an UPDATE with a name attached.

CREATE TABLE feature_flags (
  key          text PRIMARY KEY,
  enabled      boolean  NOT NULL DEFAULT false,
  rollout_pct  smallint NOT NULL DEFAULT 0 CHECK (rollout_pct BETWEEN 0 AND 100),
  allow_ids    text[]   NOT NULL DEFAULT '{}',
  owner        text     NOT NULL,
  expires_on   date     NOT NULL,
  updated_at   timestamptz NOT NULL DEFAULT now()
);

INSERT INTO feature_flags (key, enabled, rollout_pct, owner, expires_on)
VALUES ('new-invoice-editor', true, 10, 'hana', '2026-09-30');

owner and expires_on are NOT NULL on purpose. You cannot create a flag without saying who will delete it and when.

Evaluation is a pure function

Percentage rollouts need to be sticky: a user who sees the new editor at 10% must still see it at 20%. Hashing the flag key together with the user ID gives each user a stable bucket from 0 to 99, and including the flag key means the same 10% of users do not get every experiment.

// flags.ts (createHash comes from node:crypto)

export type Flag = {
  key: string;
  enabled: boolean;
  rolloutPct: number;
  allowIds: string[];
};

export function bucket(flagKey: string, subjectId: string): number {
  const digest = createHash('sha1').update(`${flagKey}:${subjectId}`).digest();
  return digest.readUInt32BE(0) % 100;
}

export function isOn(flag: Flag | undefined, subjectId: string): boolean {
  if (!flag || !flag.enabled) return false;
  if (flag.allowIds.includes(subjectId)) return true;
  return bucket(flag.key, subjectId) < flag.rolloutPct;
}

An unknown flag is off. A disabled flag is off, even for allow-listed users, which gives you a kill switch. The modulo bias from 2^32 values into 100 buckets is far too small to matter for rollouts.

Each service loads the whole table into a Map at startup and refreshes it every 15 seconds. If the refresh fails, it keeps the last good copy and logs a warning. Flag checks never touch the network, and a database blip never flips a feature off.

The rule that matters more than the code

Every flag tool eventually fills up with flags nobody remembers. The code above does nothing to prevent that. A CI check does:

  • A nightly job lists flags whose expires_on has passed and opens an issue assigned to the owner.
  • A lint rule fails the build if the code references a flag key that is not in the table, or the table holds a flag no code references for 30 days.
  • Extending an expiry date is allowed, but it needs a commit with a reason.

When we would buy one

There are good reasons to pay. If product managers or support staff need to toggle flags without asking an engineer, you need a real interface with permissions, and building it well is more than two days. If you run proper experiments, you need exposure logging and statistics you can trust. If you have services in six languages, you want SDKs someone else maintains.

We have none of those needs today. Our flags are a deploy tool for engineers: release dark, ramp to 10%, watch the error rate, ramp to 100%, delete. For that job, a table and a function are easier to reason about than any dashboard, and our count of live flags has gone from 140 to 11.

Filed under
Portrait of Hana Sato (illustration)

Written by

Hana Sato

Developer tooling lead · Osaka, Japan

Hana owns CI, the monorepo and the test suite at a mid-sized SaaS team. She writes about fast builds, flaky tests, feature flags and the small internal tools that are cheaper than a vendor.

Live demo · Green Build Gazette— View template →