Feature Flags Without Another Vendor: Rollouts in Your Backend

Feature flags let you ship code switched off and turn it on later for chosen users, without a new release. You do not need a separate flag vendor for that: the flags can live in the backend you already use, evaluated against the same signed-in user your database and Row Level Security see. StitchDB has feature flags and remote config built in, with targeting rules, segments, percentage rollouts, history and undo. This article shows how to use them, how to budget requests, and when a simple flags table in Postgres is enough.

#What a flag system has to get right

Turning a boolean on and off is the easy part. A flag system worth using handles:

  • Trusted targeting. "Only for Pro customers" must not be fakeable by a user editing local storage.
  • Stable rollouts. A user in the first 10% must stay in it when you raise the rollout to 30%.
  • Fast reads. Checking a flag must not add a network round trip to every render.
  • Safe fallbacks. If the flag service is unreachable, the app still works.
  • An audit trail. When something breaks at 15:12, you need to know who changed which flag at 15:10, and undo it.

A dedicated vendor covers these, at the price of another account, another SDK and another place where user data goes. Inside your backend, the first point gets easier: the backend already knows who the user is.

#Turn flags on and create one

In Studio open Integrations › Feature flags and click Turn on feature flags (owners and admins). Then New flag, with a key such as new-checkout and a kind:

  • On / off: true and false.
  • Text, Number or JSON: one or more values to choose between, for remote config such as an upload limit, a banner text or a group of settings.

A new flag starts off, and everyone gets its off value. When it is on, its targeting rules apply.

#Read flags in the app

JavaScript
import { createClient } from 'stitchdb-js'
const stitchdb = createClient(URL, ANON_KEY)

// once at start
await stitchdb.flags.load({ attributes: { app_version: '2.4.1', country: 'DE', platform: 'web' } })

if (stitchdb.flags.get('new-checkout', false)) showNewCheckout()
const limitMb = stitchdb.flags.get('upload-limit-mb', 10)
const theme = stitchdb.flags.get('theme', { accent: 'blue' })

// values changed (a refresh, a sign-in)
stitchdb.flags.on('change', (keys) => render())

get(key, fallback) answers immediately: with the value StitchDB gave for this user, or with fallback while it is unknown, such as before the first load, offline on the first start, or for a deleted or archived flag. The SDK keeps the last values in localStorage, so the next start has them before the network answers. It fetches again on sign-in, on sign-out and every minute.

Always write a fallback that is safe. For a new feature, that is usually "off". For remote config, the value the app shipped with.

While testing, flags.detail(key) returns the value together with the variation and the reason (off, rule:0, default, default:split), which tells you why a user got what they got.

#Target the right users

When a flag is on, rules are tried from the top and the first match decides. Users that match no rule get the value under Everyone else. A rule can require a segment and conditions, and serve one value or a percentage split.

AttributeComes fromTrust it for
user.id, user.email, user.role, user.is_anonymousThe sessionTargeting by identity
user.app_metadata.<field>The session; only your server can set itPlan, team, internal staff
user.user_metadata.<field>The session; users can edit their ownPreferences only
attr.<name>What the app sendsApp version, country, platform

Flags are evaluated by StitchDB for the signed-in user, from the session, so users cannot change their plan or e-mail to get a feature. Put the plan in app_metadata from your server and target user.app_metadata.plan is pro.

Operators include is, is one of, contains, starts with, numeric comparisons, version ≥ and version < for app versions like 2.10.1, dates, and is set. Version comparison is the one people hand-roll badly: 2.10 is newer than 2.9, which string comparison gets wrong.

#Segments

A segment is a reusable group, such as beta testers, staff or customers in one country, with conditions and lists of user ids to always include or leave out. Several flags can target the same segment, so adding a tester to "beta" turns on every beta feature for them.

#Percentage rollouts

A split puts each user in a bucket from a hash of the flag and the user id. The same user always gets the same value, and raising a rollout from 10% to 30% keeps the first 10% on and adds 20% more. Visitors who are not signed in get a random id kept in localStorage, so they stay on the same side too; after sign-in, their user id is used.

A typical release:

  1. Turn the flag on for the staff segment.
  2. Add a rule for user.app_metadata.plan is one of pro, scale at 10%.
  3. Watch errors and support tickets. Raise to 50%, then 100%.
  4. Serve true to everyone else.
  5. Remove the flag from the code and archive it.

Before turning a flag on, use Test a user: enter an e-mail or id and attributes, and Studio shows what every flag gives that user and why. Test requests are not counted.

#Server-side flags

On your server, the service key evaluates flags for any user and also returns flags marked server only, which the browser never sees, such as a price or a provider switch:

JavaScript
const admin = createClient(URL, SERVICE_KEY)
const { data, error } = await admin.flags.evaluate(
  { id: user.id, email: user.email, app_metadata: user.app_metadata },
  { country: 'DE' },
)
if (data['new-pricing']?.value) { /* … */ }

The same is available over HTTP at POST /flags/v1/evaluate, so Edge Functions, scripts and other languages can use it. See feature flags.

#Flags are not access control

A flag decides what the app shows. It does not protect data. A user who calls your API directly is not stopped by a hidden button. If a feature exposes data that only some users may see, protect the table with a Row Level Security policy too, for example by checking the same app_metadata plan claim or an entitlement. The Row Level Security guide has patterns for that.

#History, undo and cleanup

Every change to a flag or segment is logged with who made it and the flag before and after. What changed shows the difference, and Undo restores the flag to how it was before that change; the undo is logged as a new change. Apps pick up changes on their next refresh, within about a minute.

Flags that live forever become debt. When a feature is out for everyone, remove the flag from your code, then Archive it. Apps stop getting it and fall back to the value in code, which also reveals anything that still reads it. Archived flags can be restored with their buckets or deleted.

#Budget your requests

Each call to /flags/v1/evaluate, meaning one load or one refresh, is one flag request, however many flags it returns.

PlanFlagsFlag requests / month
Free10100,000
Pro1005,000,000
Scale50050,000,000
Enterprise5,000100,000,000

Values from the plans API as of November 2026 (pricing). The default refresh is every minute, so one user with the app open for an hour makes about 61 requests: one load and 60 refreshes. If your users keep the app open all day, raise refreshInterval (in seconds) or set it to 0 and call flags.refresh() yourself, for example when the app returns to the foreground. When a project uses up the month's requests, the endpoint answers 429 and the SDK keeps serving the last values until the next month, so nothing breaks; flag changes just stop arriving.

#The DIY option: a flags table

If you need three global switches and nothing else, a table is fine:

SQL
create table public.app_settings (
  key text primary key,
  value jsonb not null
);

alter table public.app_settings enable row level security;

create policy "Anyone can read settings" on public.app_settings
  for select to anon, authenticated
  using (true);
JavaScript
const { data } = await stitchdb.from('app_settings').select('key, value')

Writes go through Studio or the service role, and you can subscribe with Realtime for instant changes. What you give up is everything in the list at the top: per-user targeting, stable percentage splits, version rules, server-only values, history and undo. Once you need "10% of Pro users on 2.4 or later", the built-in flags are less code than extending the table.

#FAQ

#Can I use flags for A/B tests?

You can split users between variations with stable buckets, and Studio shows how often each value was given out over 30 days. Measure the outcome with your analytics; see crash reporting and analytics in your backend.

#What happens if the flags endpoint is down?

The SDK serves the last values it stored, or your fallbacks on a first start. Your app keeps working.

#Do flags work in mobile apps?

Yes. The Kotlin, Swift, Flutter, Python and C# SDKs have the same flags.load and get calls, for example in Kotlin:

kotlin
stitchdb.flags.load(attributes = mapOf("app_version" to "2.4.1"))
if (stitchdb.flags.isOn("new-checkout")) showNewCheckout()

Send app_version and platform as attributes to target releases (SDK overview).

#Are flag rules visible to users?

No. Users receive evaluated values, not your rules. Flags marked server only are never sent to apps at all.

Ship features behind flags without adding a vendor: create a free project, or compare limits on the pricing page.