Shipeasy
Flags & ExperimentsFeature flags

Feature flags

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

Production readyOn this page · 9 min readUpdated · May 3, 2026Works 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. For yes/no with measurement attached, reach for an experiment.

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 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. With the default 30s poll a worst-case worker still serves the new path for ~30s after you hit the switch. For sub-second cutoff, use the Pro 10s poll or the Enterprise streaming push.

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 flags list --json \
  | jq '.[] | select(.enabled == 1 and .rolloutPct == 10000) | .name'

These are candidates for removal. Run a shipeasy flags validate ./src after the deletion to catch any 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 flags update my-feature --salt v2

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

PlanFeature flags per projectRules per feature flagOverrides per feature flag
Free505100
Pro5002010,000
EnterpriseUnlimitedUnlimitedUnlimited

If you hit a limit, audit for stale flags first; those numbers are very 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 flags create dark-mode --rule plan=pro --rollout 100
Re-shuffle buckets
$shipeasy flags update dark-mode --salt v2
Was this page helpful?
✎ Edit this page

On this page