shovelbasedocs

Feature Flags

Named booleans you toggle on the portal's Feature Flags page and read in your apps through the SDK — ship code dark, turn it on without a deploy, and turn it back off if it misbehaves. Flags live in a flags schema inside the project's own Postgres database. That schema is not exposed through /rest/v1, so API keys can read the snapshot but never change it — safe to ship in browsers and mobile apps.

Why use it

A feature flag separates deploying code from releasing it. Normally those are the same event, which is what makes shipping stressful: the moment the build goes out, every user gets the change, and the only way back is another deploy. Behind a flag, the code ships dark, and turning it on is a switch — one you can flip back in seconds, without a rollback, a rebuild, or an app-store review.

That decoupling is what makes short-lived branches practical. Instead of a feature branch living for weeks and merging in one terrifying lump, half-finished work merges to main continuously behind a flag that's off. Everyone integrates against the same code, and the risky moment is a toggle rather than a merge.

Flags are cheap to add and easy to forget, so treat each one as temporary: once a feature is fully on and stable, delete the flag and the dead branch of code. A codebase with fifty stale flags has 2⁵⁰ nominal configurations and no one who knows which are real.

Kill switches
Wrap anything expensive or externally dependent — an LLM call, a recommendation feed — so you can shed it under load or when a vendor goes down, without shipping anything.
Releasing on your own schedule
Especially for mobile, where a release takes days to review and users update whenever they feel like it. Ship the code in one version, switch it on later for everyone at once — including the people still on that old build.
Trunk-based development
Merge incomplete work to main behind an off flag rather than maintaining a long-lived branch that grows harder to merge every day.
Demo and staging behavior
Turn a work-in-progress screen on in a demo project while it stays off in production — same binary, different flag state per project.
SurfaceAvailability
Portal UICreate/toggle/delete flags and set rollout percentages on the Feature Flags page
CLIshovelbase flags list | create | enable | disable | configure | delete, with --rollout <0-100> to ramp
SDK / HTTPshovelbase.flags.* — the primary way apps read a flag's value

Percentage rollouts

A flag has two controls: enabled and a rollout percentage. Enabled is the master switch — off means off for everyone, whatever the percentage says, so you can kill a half-finished ramp without losing the number it reached. When it's on, the rollout decides how many people see it.

SettingWho sees the flag
Off, any rolloutNobody. The kill switch always wins
On, 100%Everyone — how every flag behaves by default
On, 25%A quarter of users, and the same quarter every time
On, 0%Nobody yet — stage the flag, then ramp it

The same user always gets the same answer. Whether someone is in a rollout is computed from their id, not from chance, so it survives reloads, app restarts, snapshot refreshes, and SDK upgrades. Ramping up is safe by construction: going from 25% to 50% only ever adds people, and never takes the feature away from someone who already had it.

Bucketing uses the same distinct_id that Signals attributes events to — anonymous before sign-in, your user id after identify(). That sharing is deliberate: an experiment and the numbers measuring it describe the same people. It has one consequence worth planning around — because the id changes at sign-in, a user's bucket is recomputed then, and someone can cross into or out of a rollout at that moment. Keep an experiment either side of the sign-in line rather than straddling it. Signed-in users bucket on their account id, so they get the same treatment on every device.

The server sends the rule, not a yes/no answer, and the SDK buckets locally. One cached snapshot therefore answers for any number of users — a server can decide for thousands of people per second without a request each — and flags keep answering consistently while offline. The bucket is FNV-1a over the UTF-8 bytes of "<flag>:<distinct_id>" finished with MurmurHash3's avalanche step, taken modulo 100; every shovelbase SDK computes it identically. The flag name is part of the hash so that landing in the first 10% of one flag doesn't put you in the first 10% of all of them.

JavaScript / TypeScript

Every shovelbase-js client has a flags handle. The snapshot is fetched once and cached for 60 seconds (tune with createClient(url, key, { flags: { cacheTtlMs } })); lookups never throw — offline they serve the last snapshot, and before the first fetch they return your fallback value.

const shovelbase = createClient(SHOVELBASE_URL, SHOVELBASE_ANON_KEY);
if (await shovelbase.flags.isEnabled('new-checkout')) { /* */ }
await shovelbase.flags.isEnabled('kill-switch', true); // fallback when unset
const all = await shovelbase.flags.getAll(); // { 'new-checkout': true, }
shovelbase.flags.peek('new-checkout'); // sync, last-known value
await shovelbase.flags.refresh(); // bypass the cache
// On a server, decide for any user off the one cached snapshot no
// extra requests, and each user's answer matches what their browser sees.
await shovelbase.flags.isEnabledFor('new-checkout', user.id);
await shovelbase.flags.getAllFor(user.id);
// Standalone (no database/auth client needed):
import { ShovelbaseFlags } from 'shovelbase-js';
const flags = new ShovelbaseFlags(SHOVELBASE_URL, SHOVELBASE_ANON_KEY);

iOS / Swift

The ShovelbaseFlags Swift package (iOS, macOS, tvOS, watchOS) has the same API and caching behavior. It ships in the same shovelbase-swift package (add ShovelbaseFlags instead of, or alongside, Shovelbase).

Swift
ShovelbaseFlags.configure(url: "https://<ref>.shovelbase.com", apiKey: ANON_KEY)
if await ShovelbaseFlags.shared.isEnabled("new-checkout") { /* */ }
let all = await ShovelbaseFlags.shared.getAll() // ["new-checkout": true, …]
ShovelbaseFlags.shared.peek("new-checkout") // sync, last-known value
await ShovelbaseFlags.shared.refresh() // bypass the cache
// Decide for a specific user rather than the stored identity:
await ShovelbaseFlags.shared.isEnabled("new-checkout", for: userId)

HTTP API (/flags/v1/flags)

The SDKs are thin wrappers over one endpoint, so any HTTP client can read flags:

feature flags — /flags/v1/flags
curl "https://<ref>.shovelbase.com/flags/v1/flags" -H "apikey: $ANON_KEY"
# → {"rules": {"new-checkout": {"enabled": true, "rollout": 25},
# "kill-switch": {"enabled": false, "rollout": 100}},
# "flags": {"new-checkout": false, "kill-switch": false}}

Read rules and bucket yourself if you're calling the endpoint directly. The flags object is the pre-rollout response shape, kept so that SDKs released before percentage rollouts keep working; a flag mid-ramp reads false there, because degrading to off is the safe direction for a client that has no way to bucket.

The key can come as an apikey header, Authorization: Bearer, or an ?apikey= query parameter. Flag names must start with a lowercase letter or digit and may then contain lowercase letters, digits, and . _ -, up to 120 characters. Deleting a flag makes clients fall back to the default your code passes.

A toggle is not instant. Clients serve a cached snapshot, so flipping a flag in the portal reaches them within the cache TTL — 60 seconds by default, and only on the next lookup. That's fine for a release switch; if you need a kill switch to bite faster, construct the client with a shorter flags: { cacheTtlMs }, and remember every lookup still returns your fallback value before the first fetch completes.