Use Feature Flags to Gate Your API
Zuplo doesn't have a dedicated feature-flag policy — instead, every provider works, because you evaluate flags in custom code. This guide shows the patterns that keep flag checks fast on a gateway, with worked examples for Statsig, PostHog, Flagsmith, and GrowthBook:
- Live evaluation with caching — call the provider's API on the request and cache the result per user for a short TTL. Use for flags targeted at individual users, plans, or tenants.
- Background loading — load flags that apply globally (kill switches, maintenance mode, flag definitions) with the BackgroundLoader, which refreshes out of band instead of blocking requests.
- Provider SDK with local evaluation — evaluate flags in-process with no per-request provider call, at the cost of a heavier cold start. See Statsig, fully local.
Choose a pattern
| Pattern | Best for | Latency cost per request |
|---|---|---|
| Live evaluation + cache | Per-user or per-plan targeting | One provider call per user per TTL window |
| Background loading | Global flags, kill switches, flag definition files | None in the steady state; refreshes run out of band |
| Provider SDK with local evaluation | Complex targeting rules, many gates per request | None after initialization, but heavier cold start |
Most APIs mix these: a background-loaded global flag set plus a small number of cached per-user checks on the routes that need them.
Prerequisites
-
A Zuplo project with an authentication policy on the routes you want to gate (for example API key authentication). The examples read
request.user, which an inbound authentication policy populates —request.user.subis the user identifier andrequest.user.datacarries JWT claims or API key metadata such as the customer'splan. See Request User. -
An API key or token from your flag provider, stored as a secret environment variable. Where to find each one:
Variable Provider Where to find it Secret? STATSIG_SERVER_KEYStatsig Console → Settings → API Keys → Server Secret Key Yes STATSIG_CLIENT_KEYStatsig Console → Settings → API Keys → Client SDK Key No POSTHOG_PROJECT_TOKENPostHog Project Settings → Project ID → project token No FLAGSMITH_ENVIRONMENT_KEYFlagsmith Environment settings → client-side environment key No GROWTHBOOK_CLIENT_KEYGrowthBook SDK Connections → client key No New environment variables require a redeployment before your code sees them.
You only need the variables for the providers you use — skip the rest.
Pattern 1: Live evaluation with caching
Every major provider exposes an HTTP endpoint that evaluates a flag for a user and returns the result. Call it on the first request, then cache the result in a MemoryZoneReadThroughCache so subsequent requests for the same user skip the network call.
This example checks a Statsig gate and caches the result for 60 seconds:
Code
Only the first request for each user pays for the Statsig call; every request
after that reads from memory until the TTL expires. Keep the TTL short (30–120
seconds) so flag changes in the provider propagate quickly. To check several
gates, fire the check_gate calls in parallel with Promise.all and cache each
result under its own key.
Wire up the policy
Register the module as a custom code policy in config/policies.json:
Code
Then add it to the inbound policy chain of each route you want to gate, after authentication:
Code
Verify the gate
Call the route twice — once with the flag off and once on:
- Flag off — the request returns
403with anapplication/problem+jsonbody:"detail": "This feature is not enabled for your account". - Flag on — the request passes through to the handler normally.
Because the example fails closed, a bad key or unreachable Statsig returns the
same 403 as a disabled flag. Watch the logs for the Statsig check failed:
error line to tell a provider outage apart from a legitimately denied request.
Pattern 2: Background loading for global flags
Some flags apply to everyone: kill switches, maintenance mode, or a global rollout. Others are cheapest to fetch as one shared definition file rather than per-user evaluations. Either way, there's no reason to make a request wait on that fetch — load it in the background instead.
The BackgroundLoader returns cached data immediately when available, refreshes it asynchronously when the TTL expires, and only blocks while the cache is empty on a cold start. Create it at module level so the loader is shared across requests handled by the same worker process (isolate).
GrowthBook is a natural fit here: its CDN returns the full feature definition payload as a single cacheable JSON document.
Code
Register this module as a custom-code-inbound policy in config/policies.json
and add it to the routes you want to gate, exactly as in pattern 1.
The same pattern works for any provider payload that isn't user-specific — for example a Statsig gate evaluated against a fixed global user, or the Flagsmith environment document. One BackgroundLoader per provider keeps each payload on its own refresh cadence.
The defaultValue shortcut covers simple on/off flags. Flags with targeting
rules or percentage rollouts need an evaluator — either evaluate remotely
(pattern 1) or pass the definitions to the provider's JavaScript SDK.
More providers
The live-evaluation pattern is the same everywhere; only the endpoint, headers, and response shape change. Swap the fetch call inside the cached helper — and, for providers like PostHog that evaluate all flags in one call, cache the whole map keyed by user instead of a boolean keyed by gate and user.
PostHog
PostHog evaluates all flags for a user in one call, so cache the whole map
per distinct_id. Use your project token — it's safe for this public endpoint,
though you should still store it in an environment variable.
Code
Use https://eu.i.posthog.com if your PostHog project is hosted in the EU.
Flagsmith
Flagsmith's edge API evaluates flags for an identity with a simple GET. Use the client-side environment key (not a server-side SDK key) — it's designed to be public.
Code
Statsig, fully local
If you need many gates evaluated per request with zero provider calls, use
Statsig's @statsig/serverless-client SDK, which is built for short-lived
worker processes. Unlike the HTTP API examples above (which use the server
secret key), the serverless client evaluates on-device and authenticates with
the publishable client SDK key.
Zuplo runs a custom JavaScript engine rather than Node.js, so third-party packages must be bundled into your project before you can import them — see Node Modules. Bundle the SDK once, then import it from the bundled path:
Code
Initialize the client once at module level so the configuration download happens
on cold start, not per request. Evaluate gates locally, and flush exposure
events with
context.waitUntil so
analytics never add latency to a response:
Code
What about LaunchDarkly?
LaunchDarkly is the least HTTP-friendly option. Its server-side SDK holds a persistent streaming connection open and relies on Node.js APIs that don't run in Zuplo's engine, and LaunchDarkly doesn't document a plain-HTTP endpoint for server-side evaluation. Its edge SDKs evaluate locally but assume platform-specific storage bindings (Cloudflare KV, Vercel Edge Config, and similar), so they don't drop directly into a Zuplo project either. If LaunchDarkly is your provider, the practical paths are evaluating flags in your backend behind the gateway, or syncing flag state into a store your Zuplo code reads with the patterns above.
Gate an MCP server with flags
If you're fronting an MCP server with the MCP Gateway, the same policies apply — MCP routes run the standard inbound policy chain before the handler. Add your feature-gate policy to the MCP route to control access per user or plan, for example to make MCP access a paid-tier feature or to turn off a beta server.
For curating which tools a server exposes, start with the static capability filtering policy, which needs no code. Feature flags complement it when the availability of the server itself must change dynamically per user without a redeploy.
Best practices
- Decide how to fail. If the provider is unreachable, fail closed (deny the feature) for rollouts of risky functionality, or fail open (allow) for cosmetic features. The examples above fail closed and log the error.
- Keep per-user TTLs short. 30–120 seconds balances provider traffic against flag-change propagation. Global definitions can tolerate longer.
- Cache the smallest useful value. A boolean per gate per user stays well within memory limits; don't cache whole provider responses you don't need. Remember a Zuplo process has roughly 120 MB of memory — see Lazy Load Configuration for the caching trade-offs.
- Store keys in environment variables. Use secret environment variables for server keys like Statsig's server secret; never hardcode them.
- Log exposures in the background. Providers use exposure events for
analytics and experiments. Send them with
context.waitUntil(as in the Statsig SDK example) so analytics never add latency to a request.
Next steps
- BackgroundLoader — the primitive behind pattern 2
- MemoryZoneReadThroughCache and ZoneCache — the caching layers
- Custom code policies — how inbound policies are wired and run
- Lazy Load Configuration — the general config-caching pattern this guide builds on
- Curate the tools an upstream exposes — static MCP tool curation to combine with flags