Shipeasy
Flags & ConfigsFeature flags

Feature flags

Boolean feature flags with targeting rules, percentage rollouts, kill-switches and per-user overrides — evaluated locally, at zero per-request cost.

Production readyOn this page · 9 min readWorks with · Server SDK · Browser SDK

A feature flag is a single boolean answer for a single user. It is the unit of "should I do the new thing?" in your code. Feature flags are evaluated locally — the SDK keeps the bundle in process and the call is a hash table lookup, not a network round trip.

Use a feature flag when the answer is yes/no. For typed payloads (strings, numbers, JSON), reach for a dynamic value. To know whether the change worked, define a metric and watch it across the ramp.

Anatomy of a feature flag

Evaluation order

For a request gate(name, user):

1. killswitch ON                       → false
2. enabled OFF                         → false
3. user.user_id ∈ overrides[true]      → true
4. user.user_id ∈ overrides[false]     → false
5. any rule fails                      → false
6. murmur3(salt + user_id) % 10000 < rolloutPct → true
7. otherwise                           → false

That order matters: rules are evaluated before the rollout. "100% of plan = pro" means 100% of pro users, not the same 100% bucket filtered down to pros. The rollout filter never fights a targeting rule.

Why deterministic bucketing matters

Same user, same feature flag, same answer — every time, on every server, in every language SDK. No flicker, no "why does this user see different things on different requests", no need to persist assignments anywhere. The hash is the persistence.

API

Server SDK

import { configure, Client } from "@shipeasy/sdk/server";

configure({
  apiKey: process.env.SHIPEASY_SERVER_KEY ?? "",
  attributes: (u) => ({ user_id: u.id, plan: u.plan, country: u.country }),
});

const flags = new Client(currentUser);
const enabled = flags.getFlag("new-checkout-flow");

Browser SDK

import { configure, Client } from "@shipeasy/sdk/client";

configure({
  clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY ?? "",
  attributes: (u) => ({ user_id: u.id, plan: u.plan, country: u.country }),
});

const flags = new Client(currentUser);
await flags.ready();

if (flags.getFlag("new-checkout-flow")) {
  // …
}

getFlag() is synchronous. If you call it before the first poll resolves, you get false (or the bootstrap value when SSR has provided one) — never a thrown error. configure() is idempotent; call it once per cold start.

Prop

Type

The return is a plain boolean. There is no async, no Promise, no fetch — the bundle is in memory.

Killswitch

The killswitch is a separate field from enabled so you can keep targeting and rollout intact while turning the feature off. Three places to flip it:

  • Dashboard: red Killswitch chip on the feature flag row.
  • CLI: shipeasy release flags disable <name> --killswitch (without --killswitch it toggles enabled instead).
  • API: PATCH /api/flags/<name> with { "killswitch": true }.

A killed feature flag returns false everywhere within the SDK's next poll. Re-enabling restores the full prior state — overrides, rules, rollout %, salt, all of it. Treat the killswitch as your incident lever; treat enabled as your "feature is shipped, deactivated cleanly" lever.

Killswitch is a contract, not a panic button

A killed feature flag goes false within the next poll, not instantly — a worst-case worker still serves the old path for up to one poll interval after you hit the switch. That interval is plan-derived; the pricing page says what yours is. If your cutoff has to be tighter than a poll, gate the call site on a value you already hold rather than waiting on a refresh.

Overrides

In the dashboard, expand a feature flag and add user IDs under Overrides → Always on or Always off. Overrides:

  • Bypass enabled, killswitch, all rules, and the rollout.
  • Are scoped to user_id only (no attribute matching — it's a literal set lookup).
  • Are great for: QA accounts, internal dogfooders, customer-success demos, repro of a bug a single customer is hitting.
// dashboard JSON shape:
{
  "name": "new-checkout-flow",
  "overrides": {
    "always_on":  ["u_qa1", "u_qa2", "u_demo"],
    "always_off": ["u_legacy_partner"]
  }
}

If you need a percentage-based "always-on for cohort X", use a targeting rule on a custom attribute (internal: true) instead. Overrides scale linearly per-row in KV; rules are O(1).

Naming

  • kebab-case: new-checkout-flow, not newCheckoutFlow.
  • Describe the change, not the abstract feature: enable-redis-pool ages better than redis-pool (which sounds like a permanent feature, not a temporary feature flag).
  • Prefix by area for grouping: checkout-, nav-, infra-.
  • Avoid double-negatives: prefer show-banner over hide-banner.

Cleaning up

Stale flags are technical debt — once a feature is fully launched, delete the flag and the dead code. The CLI helps:

# Every gate currently at enabled: true, rollout: 100%
shipeasy release flags list \
  | jq '.[] | select(.enabled == 1 and .rolloutPct == 10000) | .name'

These are candidates for removal. Grep your source for each name after archiving it, to catch code references that need to be ripped out.

The dashboard also surfaces a Cleanup view that lists feature flags which have been at 100% (or 0%) for more than 30 days, with a one-click PR-creation button if you've connected a GitHub app.

The salt

Each feature flag has its own salt. If you ever want to re-shuffle the rollout buckets without changing the percentage (say, to avoid the same 5% always being chosen for every feature flag), bump the salt:

shipeasy release flags update my-feature

That's rare. The default salt is fine for almost every case. The salt is also why two feature flags at 50% don't serve the same 50% of users — different salts, different buckets, independently distributed.

Audit log

Every write to a feature flag is recorded in the audit log: who, when, from what (dashboard, CLI, API), and the diff. Find it under Configs → Feature flags → <feature flag> → History. Each row links to a one-click revert.

Limits

Feature flags per project, rules per flag and overrides per flag are all plan-derived. The pricing page has the matrix; Settings → Billing shows your own caps and how close you are to each. A create over a cap is rejected with PLAN_LIMIT rather than silently truncated.

If you hit one, audit for stale flags first — the caps are generous if you delete what you ship.

NEXT

Add targeting rules.

A flag with no rules is a kill-switch. Add an attribute predicate and you've got the rules engine — "100% of plan=pro in country=US" in one line.

Create with a rule
$shipeasy release flags create dark-mode --rules plan=pro --rollout-percent 100
Re-shuffle buckets
$shipeasy release flags update dark-mode
Was this page helpful?
Updated August 9, 2026✎ Edit this page

On this page