Skip to content

How flags reach your app

Your app doesn’t call FortressFlag every time it checks a flag. Flag reads are local — instant, offline-safe, no network in the path. What travels over the network is a periodic poll, and what that poll fetches depends on which of the two planes you’re on.

The client plane: your apps get answers, not rules

Section titled “The client plane: your apps get answers, not rules”

The iOS, Android, and web SDKs use the client plane. The device sends its identity and its tags; FortressFlag evaluates your targeting rules server-side and returns just that device’s values — a small map like {"new-checkout": true, "dark-mode": false}.

The device never receives your rules. That’s deliberate, twice over: your targeting logic (“beta cohort”, “internal devices”) is your business and shouldn’t ship inside something anyone can unpack from an app binary — and the payload stays tiny.

The client plane authenticates with a client key (ffc_…), which ships inside your app and is public by design — more on that in Keys.

The server plane: your backend gets the ruleset

Section titled “The server plane: your backend gets the ruleset”

The Go SDK uses the server plane. Your server downloads the full ruleset for one project + environment — kinds, defaults, rules, with segments already flattened in — and evaluates flags in its own process. A flag check is a function call on data already in memory: no network hop per check, no per-request latency, and one poll serves your whole process no matter how many requests it handles.

Because the ruleset includes your targeting rules, this plane sits behind a server key (ffs_…) — a genuine secret, handled like a database password. And since your rules reach every server holding that key, the corollary is yours to apply: don’t put secrets or your users’ personal data in rule conditions.

On this plane there’s no device identity. Your code passes a context key per evaluation — a user ID, a session ID, whatever you bucket on — and rollout arithmetic uses it exactly as the client plane uses the device ID, so the same input lands in the same cohort whichever plane evaluates it.

Both planes poll on an interval, and both use conditional requests: each response carries a validator (an ETag), and when nothing has changed since the last poll, the answer is a tiny “nothing changed” — no body, no re-evaluation. The steady state of a fleet that isn’t changing flags is a stream of near-free checks.

When you do change something — a toggle, a rule, a schedule firing — the next poll picks it up in full. In practice a change reaches online client devices within about a minute.

(One billing note: a “nothing changed” response still counts as activity for the monthly-active device count — but remember, a device counts once per month no matter how often it polls.)

A FortressFlag outage must never take down your app. Every SDK keeps the last values it received in a durable local cache and resolves flags through the same cascade:

  1. The last values received — even from a previous run. A device that’s been offline for a month serves what it last saw; staleness is preferred to surprise regressions.
  2. Your code’s default — the value at the call site — when nothing has ever been received. (Client SDKs bottom out at false for boolean reads without a default.)

Two consequences worth knowing:

  • A revoked or failing credential doesn’t break your app. The SDK keeps serving its cache and keeps trying. Errors never reach your users through a flag read.
  • Archiving a flag really turns it off. When a flag leaves the payload, the SDK drops it from live values and cache on the same poll — within about a minute, online devices everywhere resolve your code’s default. The cache never preserves removed flags — if it did, archiving couldn’t end a rollout for good.