Skip to main content

Device-local post-filter gates

Remote feature flags answer: “Should this user see this feature?” — based on rollout percentage, segments, and rules evaluated on the Toggly worker.

Some products also need a second question answered on the device:

“Has this user opted in on this phone?”

That second answer lives in iOS Settings, localStorage, a debug menu, or an in-app beta toggle. It is not in the Toggly dashboard, and it can change instantly while the user is in your app — without waiting for a network round-trip.

Post-filter gates let you combine both answers cleanly:

effective(key) = remote(key) AND localPrerequisite(key)
  • remote(key) — signed boolean from evaluated-signed (rollouts, targeting, %)
  • localPrerequisite(key) — your device-local switch, read synchronously when the flag is checked

The cached remote map is never mutated. Gates apply at read time, so a cached remote true cannot bypass a local switch that is OFF.

Plain-language summary

Think of a local gate as a master switch on the phone that controls a bundle of Toggly flags. Remote rollouts still decide eligibility; the local switch decides whether this device is allowed to show the feature right now.

Security

Post-filter gates are for UX and product gating on the device. Always enforce security-sensitive behavior on your server as well.

The problem this solves

Without post-filter, teams usually pick one of these workarounds:

WorkaroundWhat goes wrong
Duplicate every flag in the dashboard“Beta checkout” + “Checkout v2” for every gated feature; hard to keep in sync
Store opt-in only in app codeRemote flags and local state drift; UI can show features the user turned off
Overwrite the cached remote map when the switch flipsStale remote true values can “stick” until the next fetch; turning OFF is not instant
Skip Toggly for gated featuresLose rollouts, targeting, and signed definitions for that whole bundle

Post-filter keeps one remote flag per feature in Toggly and adds a local AND at read time.

A concrete example

You ship a redesigned checkout behind ApiV2Checkout and ApiV2Profile, with a 10% remote rollout. You also add Settings → Try the new checkout, stored in localStorage.

What you want:

User stateRemote rolloutLocal toggleCheckout UI
Not in rolloutfalseON or OFFHidden
In rollout, toggle OFFtrueOFFHidden immediately
In rollout, toggle ONtrueONShown
Turns toggle OFF while using apptrue (cached)OFFHidden immediately — no fetch

Register one local gate that covers both flag keys:

localGates: [{
id: 'apiRedesign',
flagKeys: ['ApiV2Checkout', 'ApiV2Profile'],
isEnabled: () => readApiRedesignSetting(), // e.g. localStorage
}]

When the user flips the Settings switch OFF, call notifyLocalGatesChanged(). Every isFeatureOn, <Feature>, and $flag read recomputes remote AND local without a network call.

How it fits in the client SDK flow

flowchart LR
subgraph worker [Toggly worker]
R[evaluated-signed]
end
subgraph device [Your app]
C[Cached remote booleans]
L[Local gate isEnabled]
E["effective = remote AND local"]
UI[UI / components]
end
R -->|fetch / refresh| C
C --> E
L --> E
E --> UI
  1. Fetchrefresh() loads remote booleans (signed evaluated-signed response).
  2. Cache — SDK stores remote values; local gates do not change this cache.
  3. Read — Each flag check runs applyLocalGate(remote, key, gates).
  4. Local change — User toggles Settings → update local state → notifyLocalGatesChanged() → UI re-reads effective values.

For response format details, see Evaluated-signed.

Three kinds of flags in your app

KindWhere it livesExample
Remote-onlyToggly worker onlyKill switch, % rollout
Remote + device-gatedToggly + local gateBeta UI behind Settings toggle
Pure deviceApp only, not in TogglyDebug overlay, one-off dev tools

Use post-filter for the middle row: remote rules stay in the dashboard; the device owns the opt-in switch.

Post-filter vs worker traits

Both can express “only show when context X”. Choose based on where the truth lives and how fast it must update.

Post-filter (local AND)Traits → worker
Best forSettings toggles, localStorage, OS permissions on this deviceDashboard rules using user/org attributes
Turn OFF latencyInstant (no network)Next fetch (or WebSocket refresh)
Turn ON latencyrefresh() then readNext fetch
Who configures rulesApp code registers gatesToggly dashboard segments / %

They can coexist: worker traits for “is this user in the beta cohort?” and a local gate for “has this device opted in?”

Toggle lifecycle

EventWhat to do
App launchRegister gates → refresh() → reads apply post-filter
User turns master switch OFFUpdate local setting → notifyLocalGatesChanged() (no network)
User turns master switch ONUpdate local setting → refresh()notifyLocalGatesChanged()
Foreground / periodic refreshExisting refresh; post-filter still applied on every read

JavaScript example

import Toggly from '@ops-ai/feature-flags-toggly';

let apiRedesignEnabled = false;

await Toggly.init({
appKey: 'your-app-key',
environment: 'Production',
localGates: [{
id: 'apiRedesign',
flagKeys: ['ApiV2Checkout', 'ApiV2Profile'],
isEnabled: () => apiRedesignEnabled,
}],
});

// User turns OFF in Settings — instant, no fetch
apiRedesignEnabled = false;
Toggly.notifyLocalGatesChanged();

// User turns ON — refresh remote rollouts, then notify UI
apiRedesignEnabled = true;
await Toggly.refresh();
Toggly.notifyLocalGatesChanged();

if (Toggly.isFeatureOn('ApiV2Checkout')) {
// effective: remote AND apiRedesignEnabled
}

Subscribe UI to local gate changes:

const unsub = Toggly.subscribeLocalGatesChanged(() => {
renderShell();
});

Flutter example

var apiRedesignEnabled = false;

await Toggly.init(
appKey: 'your-app-key',
config: TogglyConfig(
localGates: [
LocalGate(
id: 'apiRedesign',
flagKeys: ['ApiV2Checkout', 'ApiV2Profile'],
isEnabled: () => apiRedesignEnabled,
),
],
),
);

apiRedesignEnabled = false;
Toggly.notifyLocalGatesChanged();

// Feature widget, FeatureGateBuilder, and evaluateFeatureGate re-read effective flags

API summary (cross-SDK)

APIPurpose
localGates / setLocalGatesStatic registry: gate id → flag keys + isEnabled()
notifyLocalGatesChangedRecompute effective flags locally; notify UI (no network)
subscribeLocalGatesChangedSubscribe to local gate changes (JS-family SDKs)
onLocalGatesChangedStream of local gate changes (Flutter); FeatureGateBuilder listens automatically

Framework wrappers (Feature, FeatureGateBuilder, *featureGateBuilder, useFeatureGate, useFeatureFlag, $flag, $gate, etc.) subscribe to local gate notifications where applicable.

Per-SDK references