zuplo 7.3.2 → 7.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/ai-gateway/apps.mdx +2 -2
- package/docs/ai-gateway/custom-policies.mdx +3 -3
- package/docs/ai-gateway/fallback.mdx +13 -12
- package/docs/ai-gateway/getting-started.mdx +10 -9
- package/docs/ai-gateway/introduction.mdx +19 -18
- package/docs/ai-gateway/managing-apps.mdx +9 -7
- package/docs/ai-gateway/policy-chains.mdx +13 -7
- package/docs/ai-gateway/policy-templates.mdx +4 -3
- package/docs/ai-gateway/usage-limits.mdx +8 -10
- package/docs/api-management/introduction.md +1 -1
- package/docs/articles/api-key-api.mdx +3 -3
- package/docs/articles/custom-ci-cd-azure.mdx +1 -1
- package/docs/articles/custom-ci-cd-bitbucket.mdx +2 -2
- package/docs/articles/custom-code-patterns.md +1 -1
- package/docs/articles/custom-domains.mdx +2 -2
- package/docs/articles/development-options.mdx +4 -4
- package/docs/articles/fastly-zuplo-host-setup.mdx +5 -5
- package/docs/articles/feature-flags.mdx +442 -0
- package/docs/articles/gke-with-upstream-auth-policy.mdx +7 -7
- package/docs/articles/health-checks.mdx +4 -4
- package/docs/articles/hosting-options.mdx +2 -2
- package/docs/articles/local-development-env-variables.mdx +1 -1
- package/docs/articles/metrics-plugins.mdx +1 -1
- package/docs/articles/migration-overview.md +1 -1
- package/docs/articles/monorepo-deployment.mdx +10 -10
- package/docs/articles/performance-testing.mdx +10 -9
- package/docs/articles/plugin-azure-blob.mdx +1 -1
- package/docs/articles/plugin-azure-event-hubs.mdx +2 -2
- package/docs/articles/policies.mdx +2 -2
- package/docs/articles/securing-backend-mtls.mdx +3 -3
- package/docs/articles/securing-the-gateway-with-client-mtls.mdx +3 -3
- package/docs/articles/securing-your-backend.mdx +1 -1
- package/docs/articles/security.mdx +1 -1
- package/docs/articles/terraform.mdx +2 -2
- package/docs/articles/testing.mdx +6 -6
- package/docs/articles/troubleshooting-slow-responses.mdx +5 -6
- package/docs/articles/troubleshooting.md +1 -1
- package/docs/articles/use-openapi-extension-data.mdx +4 -4
- package/docs/articles/waf-ddos-akamai.md +13 -13
- package/docs/articles/waf-ddos-aws-waf-shield.mdx +14 -14
- package/docs/articles/waf-ddos-fastly.mdx +14 -14
- package/docs/articles/waf-ddos.mdx +7 -7
- package/docs/cli/authentication.mdx +1 -1
- package/docs/cli/lint.mdx +103 -0
- package/docs/concepts/how-zuplo-works.mdx +5 -4
- package/docs/concepts/upstream-credentials.mdx +1 -1
- package/docs/dedicated/akamai/architecture.mdx +23 -24
- package/docs/dedicated/akamai/cdn.mdx +20 -20
- package/docs/dedicated/architecture.mdx +28 -27
- package/docs/dedicated/custom-domains.mdx +3 -4
- package/docs/dedicated/federated-gateways.mdx +1 -1
- package/docs/dedicated/networking.mdx +10 -10
- package/docs/dedicated/overview.mdx +3 -3
- package/docs/dedicated/source-control.mdx +6 -5
- package/docs/handlers/custom-handler.mdx +4 -4
- package/docs/handlers/mcp-server.mdx +1 -1
- package/docs/handlers/url-forward.mdx +1 -2
- package/docs/mcp-server/introduction.mdx +4 -4
- package/docs/policies/_index.md +2 -2
- package/docs/policies/ai-gateway-configuration-executor-v2-inbound/schema.json +1 -1
- package/docs/policies/ai-gateway-dlp-inbound/doc.md +79 -45
- package/docs/policies/ai-gateway-dlp-inbound/schema.json +5679 -3359
- package/docs/policies/ai-gateway-metering-v2-inbound/doc.md +13 -13
- package/docs/policies/ai-gateway-metering-v2-inbound/schema.json +1 -1
- package/docs/programmable-api/environment.mdx +1 -1
- package/docs/programmable-api/jwt-service-plugin.mdx +1 -1
- package/docs/programmable-api/logger.mdx +1 -1
- package/docs/programmable-api/runtime-errors.mdx +1 -1
- package/docs/programmable-api/zuplo-context.mdx +3 -3
- package/docs/self-hosted/overview.md +3 -3
- package/package.json +5 -5
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Feature Flags to Gate Your API
|
|
3
|
+
sidebar_label: Feature Flags & Gating
|
|
4
|
+
description:
|
|
5
|
+
Evaluate feature flags from providers like Statsig, PostHog, Flagsmith, and
|
|
6
|
+
GrowthBook inside Zuplo policies. Cache per-user evaluations for low latency
|
|
7
|
+
and load global flags in the background so they never block a request.
|
|
8
|
+
tags:
|
|
9
|
+
- integrations
|
|
10
|
+
- custom-code
|
|
11
|
+
- caching
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Zuplo doesn't have a dedicated feature-flag policy — instead, every provider
|
|
15
|
+
works, because you evaluate flags in custom code. This guide shows the patterns
|
|
16
|
+
that keep flag checks fast on a gateway, with worked examples for Statsig,
|
|
17
|
+
PostHog, Flagsmith, and GrowthBook:
|
|
18
|
+
|
|
19
|
+
- **Live evaluation with caching** — call the provider's API on the request and
|
|
20
|
+
cache the result per user for a short TTL. Use for flags targeted at
|
|
21
|
+
individual users, plans, or tenants.
|
|
22
|
+
- **Background loading** — load flags that apply globally (kill switches,
|
|
23
|
+
maintenance mode, flag definitions) with the
|
|
24
|
+
[BackgroundLoader](../programmable-api/background-loader.mdx), which refreshes
|
|
25
|
+
out of band instead of blocking requests.
|
|
26
|
+
- **Provider SDK with local evaluation** — evaluate flags in-process with no
|
|
27
|
+
per-request provider call, at the cost of a heavier cold start. See
|
|
28
|
+
[Statsig, fully local](#statsig-fully-local).
|
|
29
|
+
|
|
30
|
+
## Choose a pattern
|
|
31
|
+
|
|
32
|
+
| Pattern | Best for | Latency cost per request |
|
|
33
|
+
| ---------------------------------- | -------------------------------------------------- | --------------------------------------------------- |
|
|
34
|
+
| Live evaluation + cache | Per-user or per-plan targeting | One provider call per user per TTL window |
|
|
35
|
+
| Background loading | Global flags, kill switches, flag definition files | None in the steady state; refreshes run out of band |
|
|
36
|
+
| Provider SDK with local evaluation | Complex targeting rules, many gates per request | None after initialization, but heavier cold start |
|
|
37
|
+
|
|
38
|
+
Most APIs mix these: a background-loaded global flag set plus a small number of
|
|
39
|
+
cached per-user checks on the routes that need them.
|
|
40
|
+
|
|
41
|
+
## Prerequisites
|
|
42
|
+
|
|
43
|
+
- A Zuplo project with an authentication policy on the routes you want to gate
|
|
44
|
+
(for example [API key authentication](../policies/api-key-inbound.mdx)). The
|
|
45
|
+
examples read `request.user`, which an inbound authentication policy populates
|
|
46
|
+
— `request.user.sub` is the user identifier and `request.user.data` carries
|
|
47
|
+
JWT claims or API key metadata such as the customer's `plan`. See
|
|
48
|
+
[Request User](../programmable-api/request-user.mdx).
|
|
49
|
+
- An API key or token from your flag provider, stored as a secret
|
|
50
|
+
[environment variable](./environment-variables.mdx). Where to find each one:
|
|
51
|
+
|
|
52
|
+
| Variable | Provider | Where to find it | Secret? |
|
|
53
|
+
| --------------------------- | ---------- | -------------------------------------------------- | ------- |
|
|
54
|
+
| `STATSIG_SERVER_KEY` | Statsig | Console → Settings → API Keys → Server Secret Key | Yes |
|
|
55
|
+
| `STATSIG_CLIENT_KEY` | Statsig | Console → Settings → API Keys → Client SDK Key | No |
|
|
56
|
+
| `POSTHOG_PROJECT_TOKEN` | PostHog | Project Settings → Project ID → project token | No |
|
|
57
|
+
| `FLAGSMITH_ENVIRONMENT_KEY` | Flagsmith | Environment settings → client-side environment key | No |
|
|
58
|
+
| `GROWTHBOOK_CLIENT_KEY` | GrowthBook | SDK Connections → client key | No |
|
|
59
|
+
|
|
60
|
+
New environment variables require a redeployment before your code sees them.
|
|
61
|
+
|
|
62
|
+
You only need the variables for the providers you use — skip the rest.
|
|
63
|
+
|
|
64
|
+
## Pattern 1: Live evaluation with caching
|
|
65
|
+
|
|
66
|
+
Every major provider exposes an HTTP endpoint that evaluates a flag for a user
|
|
67
|
+
and returns the result. Call it on the first request, then cache the result in a
|
|
68
|
+
[MemoryZoneReadThroughCache](../programmable-api/memory-zone-read-through-cache.mdx)
|
|
69
|
+
so subsequent requests for the same user skip the network call.
|
|
70
|
+
|
|
71
|
+
This example checks a Statsig gate and caches the result for 60 seconds:
|
|
72
|
+
|
|
73
|
+
```ts title="modules/feature-gate-policy.ts"
|
|
74
|
+
import {
|
|
75
|
+
HttpProblems,
|
|
76
|
+
MemoryZoneReadThroughCache,
|
|
77
|
+
ZuploContext,
|
|
78
|
+
ZuploRequest,
|
|
79
|
+
environment,
|
|
80
|
+
} from "@zuplo/runtime";
|
|
81
|
+
|
|
82
|
+
const CACHE_NAME = "feature-flags";
|
|
83
|
+
const CACHE_TTL_SECONDS = 60;
|
|
84
|
+
|
|
85
|
+
async function isGateEnabled(
|
|
86
|
+
gateName: string,
|
|
87
|
+
userId: string,
|
|
88
|
+
context: ZuploContext,
|
|
89
|
+
): Promise<boolean> {
|
|
90
|
+
const cache = new MemoryZoneReadThroughCache<boolean>(CACHE_NAME, context);
|
|
91
|
+
const cacheKey = `statsig:${gateName}:${userId}`;
|
|
92
|
+
|
|
93
|
+
const cached = await cache.get(cacheKey);
|
|
94
|
+
if (cached !== undefined) {
|
|
95
|
+
return cached;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const response = await fetch("https://api.statsig.com/v1/check_gate", {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: {
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
"statsig-api-key": environment.STATSIG_SERVER_KEY,
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
gateName,
|
|
106
|
+
user: { userID: userId },
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
// Fail closed: if Statsig is unreachable, deny the feature rather
|
|
112
|
+
// than exposing something that isn't rolled out. Choose deliberately —
|
|
113
|
+
// see "Decide how to fail" below.
|
|
114
|
+
context.log.error(`Statsig check failed: ${response.status}`);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const result = await response.json();
|
|
119
|
+
const enabled = result.value === true;
|
|
120
|
+
cache.put(cacheKey, enabled, CACHE_TTL_SECONDS);
|
|
121
|
+
return enabled;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default async function policy(
|
|
125
|
+
request: ZuploRequest,
|
|
126
|
+
context: ZuploContext,
|
|
127
|
+
) {
|
|
128
|
+
const userId = request.user?.sub;
|
|
129
|
+
if (!userId) {
|
|
130
|
+
return HttpProblems.unauthorized(request, context);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const enabled = await isGateEnabled("new-beta-endpoint", userId, context);
|
|
134
|
+
if (!enabled) {
|
|
135
|
+
return HttpProblems.forbidden(request, context, {
|
|
136
|
+
detail: "This feature is not enabled for your account",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return request;
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Only the first request for each user pays for the Statsig call; every request
|
|
145
|
+
after that reads from memory until the TTL expires. Keep the TTL short (30–120
|
|
146
|
+
seconds) so flag changes in the provider propagate quickly. To check several
|
|
147
|
+
gates, fire the `check_gate` calls in parallel with `Promise.all` and cache each
|
|
148
|
+
result under its own key.
|
|
149
|
+
|
|
150
|
+
### Wire up the policy
|
|
151
|
+
|
|
152
|
+
Register the module as a custom code policy in `config/policies.json`:
|
|
153
|
+
|
|
154
|
+
```json title="config/policies.json"
|
|
155
|
+
{
|
|
156
|
+
"name": "feature-gate",
|
|
157
|
+
"policyType": "custom-code-inbound",
|
|
158
|
+
"handler": {
|
|
159
|
+
"export": "default",
|
|
160
|
+
"module": "$import(./modules/feature-gate-policy)"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Then add it to the inbound policy chain of each route you want to gate, after
|
|
166
|
+
authentication:
|
|
167
|
+
|
|
168
|
+
```json title="config/routes.oas.json"
|
|
169
|
+
{
|
|
170
|
+
"paths": {
|
|
171
|
+
"/beta/insights": {
|
|
172
|
+
"get": {
|
|
173
|
+
"x-zuplo-route": {
|
|
174
|
+
"policies": {
|
|
175
|
+
"inbound": ["api-key-auth", "feature-gate"]
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Verify the gate
|
|
185
|
+
|
|
186
|
+
Call the route twice — once with the flag off and once on:
|
|
187
|
+
|
|
188
|
+
- **Flag off** — the request returns `403` with an `application/problem+json`
|
|
189
|
+
body: `"detail": "This feature is not enabled for your account"`.
|
|
190
|
+
- **Flag on** — the request passes through to the handler normally.
|
|
191
|
+
|
|
192
|
+
Because the example fails closed, a bad key or unreachable Statsig returns the
|
|
193
|
+
same 403 as a disabled flag. Watch the logs for the `Statsig check failed:`
|
|
194
|
+
error line to tell a provider outage apart from a legitimately denied request.
|
|
195
|
+
|
|
196
|
+
## Pattern 2: Background loading for global flags
|
|
197
|
+
|
|
198
|
+
Some flags apply to everyone: kill switches, maintenance mode, or a global
|
|
199
|
+
rollout. Others are cheapest to fetch as one shared definition file rather than
|
|
200
|
+
per-user evaluations. Either way, there's no reason to make a request wait on
|
|
201
|
+
that fetch — load it in the background instead.
|
|
202
|
+
|
|
203
|
+
The [BackgroundLoader](../programmable-api/background-loader.mdx) returns cached
|
|
204
|
+
data immediately when available, refreshes it asynchronously when the TTL
|
|
205
|
+
expires, and only blocks while the cache is empty on a cold start. Create it at
|
|
206
|
+
module level so the loader is shared across requests handled by the same worker
|
|
207
|
+
process ([isolate](../concepts/how-zuplo-works.mdx)).
|
|
208
|
+
|
|
209
|
+
GrowthBook is a natural fit here: its CDN returns the full feature definition
|
|
210
|
+
payload as a single cacheable JSON document.
|
|
211
|
+
|
|
212
|
+
```ts title="modules/global-flags-policy.ts"
|
|
213
|
+
import {
|
|
214
|
+
BackgroundLoader,
|
|
215
|
+
HttpProblems,
|
|
216
|
+
ZuploContext,
|
|
217
|
+
ZuploRequest,
|
|
218
|
+
environment,
|
|
219
|
+
} from "@zuplo/runtime";
|
|
220
|
+
|
|
221
|
+
interface GrowthBookFeatures {
|
|
222
|
+
[flagName: string]: { defaultValue: unknown };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const featureLoader = new BackgroundLoader<GrowthBookFeatures>(
|
|
226
|
+
async () => {
|
|
227
|
+
const response = await fetch(
|
|
228
|
+
`https://cdn.growthbook.io/api/features/${environment.GROWTHBOOK_CLIENT_KEY}`,
|
|
229
|
+
);
|
|
230
|
+
if (!response.ok) {
|
|
231
|
+
throw new Error(`Failed to load features: ${response.status}`);
|
|
232
|
+
}
|
|
233
|
+
const data = await response.json();
|
|
234
|
+
return data.features;
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
ttlSeconds: 60,
|
|
238
|
+
loaderTimeoutSeconds: 5,
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
export default async function policy(
|
|
243
|
+
request: ZuploRequest,
|
|
244
|
+
context: ZuploContext,
|
|
245
|
+
) {
|
|
246
|
+
// Returns cached definitions immediately in the steady state;
|
|
247
|
+
// the only blocking case is an empty cache on a cold start.
|
|
248
|
+
const features = await featureLoader.get("features");
|
|
249
|
+
|
|
250
|
+
if (features["api-maintenance-mode"]?.defaultValue === true) {
|
|
251
|
+
return HttpProblems.serviceUnavailable(request, context, {
|
|
252
|
+
detail: "This API is temporarily unavailable for maintenance",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return request;
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Register this module as a `custom-code-inbound` policy in `config/policies.json`
|
|
261
|
+
and add it to the routes you want to gate, exactly as in pattern 1.
|
|
262
|
+
|
|
263
|
+
The same pattern works for any provider payload that isn't user-specific — for
|
|
264
|
+
example a Statsig gate evaluated against a fixed global user, or the Flagsmith
|
|
265
|
+
environment document. One BackgroundLoader per provider keeps each payload on
|
|
266
|
+
its own refresh cadence.
|
|
267
|
+
|
|
268
|
+
:::note
|
|
269
|
+
|
|
270
|
+
The `defaultValue` shortcut covers simple on/off flags. Flags with targeting
|
|
271
|
+
rules or percentage rollouts need an evaluator — either evaluate remotely
|
|
272
|
+
(pattern 1) or pass the definitions to the provider's JavaScript SDK.
|
|
273
|
+
|
|
274
|
+
:::
|
|
275
|
+
|
|
276
|
+
## More providers
|
|
277
|
+
|
|
278
|
+
The live-evaluation pattern is the same everywhere; only the endpoint, headers,
|
|
279
|
+
and response shape change. Swap the fetch call inside the cached helper — and,
|
|
280
|
+
for providers like PostHog that evaluate all flags in one call, cache the whole
|
|
281
|
+
map keyed by user instead of a boolean keyed by gate and user.
|
|
282
|
+
|
|
283
|
+
### PostHog
|
|
284
|
+
|
|
285
|
+
PostHog evaluates **all** flags for a user in one call, so cache the whole map
|
|
286
|
+
per `distinct_id`. Use your project token — it's safe for this public endpoint,
|
|
287
|
+
though you should still store it in an environment variable.
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
const response = await fetch("https://us.i.posthog.com/flags?v=2", {
|
|
291
|
+
method: "POST",
|
|
292
|
+
headers: { "Content-Type": "application/json" },
|
|
293
|
+
body: JSON.stringify({
|
|
294
|
+
api_key: environment.POSTHOG_PROJECT_TOKEN,
|
|
295
|
+
distinct_id: userId,
|
|
296
|
+
person_properties: { plan: request.user?.data?.plan },
|
|
297
|
+
}),
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const data = await response.json();
|
|
301
|
+
// Cache the whole map: cache.put(`posthog:${userId}`, data.flags, 60)
|
|
302
|
+
// data.flags["new-checkout"] = { key, enabled, variant, metadata: { payload } }
|
|
303
|
+
const enabled = data.flags?.["new-checkout"]?.enabled === true;
|
|
304
|
+
const variant = data.flags?.["new-checkout"]?.variant; // multivariate flags
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Use `https://eu.i.posthog.com` if your PostHog project is hosted in the EU.
|
|
308
|
+
|
|
309
|
+
### Flagsmith
|
|
310
|
+
|
|
311
|
+
Flagsmith's edge API evaluates flags for an identity with a simple GET. Use the
|
|
312
|
+
client-side environment key (not a server-side SDK key) — it's designed to be
|
|
313
|
+
public.
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
const response = await fetch(
|
|
317
|
+
`https://edge.api.flagsmith.com/api/v1/identities/?identifier=${encodeURIComponent(userId)}`,
|
|
318
|
+
{
|
|
319
|
+
headers: { "X-Environment-Key": environment.FLAGSMITH_ENVIRONMENT_KEY },
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const data = await response.json();
|
|
324
|
+
const flag = data.flags?.find((f) => f.feature.name === "new-checkout");
|
|
325
|
+
const enabled = flag?.enabled === true;
|
|
326
|
+
const value = flag?.feature_state_value; // remote config value
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Statsig, fully local
|
|
330
|
+
|
|
331
|
+
If you need many gates evaluated per request with zero provider calls, use
|
|
332
|
+
Statsig's `@statsig/serverless-client` SDK, which is built for short-lived
|
|
333
|
+
worker processes. Unlike the HTTP API examples above (which use the server
|
|
334
|
+
secret key), the serverless client evaluates on-device and authenticates with
|
|
335
|
+
the publishable client SDK key.
|
|
336
|
+
|
|
337
|
+
Zuplo runs a custom JavaScript engine rather than Node.js, so third-party
|
|
338
|
+
packages must be bundled into your project before you can import them — see
|
|
339
|
+
[Node Modules](../programmable-api/node-modules.mdx). Bundle the SDK once, then
|
|
340
|
+
import it from the bundled path:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
npm install @statsig/serverless-client
|
|
344
|
+
npx tsdown ./node_modules/@statsig/serverless-client --format esm --platform browser --out-dir ./modules/third-party/statsig
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Initialize the client once at module level so the configuration download happens
|
|
348
|
+
on cold start, not per request. Evaluate gates locally, and flush exposure
|
|
349
|
+
events with
|
|
350
|
+
[`context.waitUntil`](../programmable-api/zuplo-context.mdx#waituntil) so
|
|
351
|
+
analytics never add latency to a response:
|
|
352
|
+
|
|
353
|
+
```ts title="modules/statsig-local-policy.ts"
|
|
354
|
+
import {
|
|
355
|
+
HttpProblems,
|
|
356
|
+
ZuploContext,
|
|
357
|
+
ZuploRequest,
|
|
358
|
+
environment,
|
|
359
|
+
} from "@zuplo/runtime";
|
|
360
|
+
import { StatsigServerlessClient } from "./third-party/statsig/index.mjs";
|
|
361
|
+
|
|
362
|
+
const client = new StatsigServerlessClient(environment.STATSIG_CLIENT_KEY);
|
|
363
|
+
// Kicks off the config download once per worker process, not per request
|
|
364
|
+
const ready = client.initializeAsync();
|
|
365
|
+
|
|
366
|
+
export default async function policy(
|
|
367
|
+
request: ZuploRequest,
|
|
368
|
+
context: ZuploContext,
|
|
369
|
+
) {
|
|
370
|
+
await ready;
|
|
371
|
+
const enabled = client.checkGate("new-beta-endpoint", {
|
|
372
|
+
userID: request.user?.sub,
|
|
373
|
+
});
|
|
374
|
+
context.waitUntil(client.flush());
|
|
375
|
+
|
|
376
|
+
if (!enabled) {
|
|
377
|
+
return HttpProblems.forbidden(request, context, {
|
|
378
|
+
detail: "This feature is not enabled for your account",
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
return request;
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### What about LaunchDarkly?
|
|
386
|
+
|
|
387
|
+
LaunchDarkly is the least HTTP-friendly option. Its server-side SDK holds a
|
|
388
|
+
persistent streaming connection open and relies on Node.js APIs that don't run
|
|
389
|
+
in Zuplo's engine, and LaunchDarkly doesn't document a plain-HTTP endpoint for
|
|
390
|
+
server-side evaluation. Its
|
|
391
|
+
[edge SDKs](https://launchdarkly.com/docs/sdk/server-side) evaluate locally but
|
|
392
|
+
assume platform-specific storage bindings (Cloudflare KV, Vercel Edge Config,
|
|
393
|
+
and similar), so they don't drop directly into a Zuplo project either. If
|
|
394
|
+
LaunchDarkly is your provider, the practical paths are evaluating flags in your
|
|
395
|
+
backend behind the gateway, or syncing flag state into a store your Zuplo code
|
|
396
|
+
reads with the patterns above.
|
|
397
|
+
|
|
398
|
+
## Gate an MCP server with flags
|
|
399
|
+
|
|
400
|
+
If you're fronting an MCP server with the
|
|
401
|
+
[MCP Gateway](../mcp-gateway/introduction.mdx), the same policies apply — MCP
|
|
402
|
+
routes run the standard inbound policy chain before the handler. Add your
|
|
403
|
+
feature-gate policy to the MCP route to control access per user or plan, for
|
|
404
|
+
example to make MCP access a paid-tier feature or to turn off a beta server.
|
|
405
|
+
|
|
406
|
+
For curating _which tools_ a server exposes, start with the static
|
|
407
|
+
[capability filtering](../mcp-gateway/capability-filtering.mdx) policy, which
|
|
408
|
+
needs no code. Feature flags complement it when the availability of the server
|
|
409
|
+
itself must change dynamically per user without a redeploy.
|
|
410
|
+
|
|
411
|
+
## Best practices
|
|
412
|
+
|
|
413
|
+
1. **Decide how to fail.** If the provider is unreachable, fail closed (deny the
|
|
414
|
+
feature) for rollouts of risky functionality, or fail open (allow) for
|
|
415
|
+
cosmetic features. The examples above fail closed and log the error.
|
|
416
|
+
2. **Keep per-user TTLs short.** 30–120 seconds balances provider traffic
|
|
417
|
+
against flag-change propagation. Global definitions can tolerate longer.
|
|
418
|
+
3. **Cache the smallest useful value.** A boolean per gate per user stays well
|
|
419
|
+
within memory limits; don't cache whole provider responses you don't need.
|
|
420
|
+
Remember a Zuplo process has roughly 120 MB of memory — see
|
|
421
|
+
[Lazy Load Configuration](./lazy-load-configuration-into-cache.mdx) for the
|
|
422
|
+
caching trade-offs.
|
|
423
|
+
4. **Store keys in environment variables.** Use secret
|
|
424
|
+
[environment variables](./environment-variables.mdx) for server keys like
|
|
425
|
+
Statsig's server secret; never hardcode them.
|
|
426
|
+
5. **Log exposures in the background.** Providers use exposure events for
|
|
427
|
+
analytics and experiments. Send them with
|
|
428
|
+
[`context.waitUntil`](../programmable-api/zuplo-context.mdx#waituntil) (as in
|
|
429
|
+
the Statsig SDK example) so analytics never add latency to a request.
|
|
430
|
+
|
|
431
|
+
## Next steps
|
|
432
|
+
|
|
433
|
+
- [BackgroundLoader](../programmable-api/background-loader.mdx) — the primitive
|
|
434
|
+
behind pattern 2
|
|
435
|
+
- [MemoryZoneReadThroughCache](../programmable-api/memory-zone-read-through-cache.mdx)
|
|
436
|
+
and [ZoneCache](../programmable-api/zone-cache.mdx) — the caching layers
|
|
437
|
+
- [Custom code policies](../policies/custom-code-inbound.mdx) — how inbound
|
|
438
|
+
policies are wired and run
|
|
439
|
+
- [Lazy Load Configuration](./lazy-load-configuration-into-cache.mdx) — the
|
|
440
|
+
general config-caching pattern this guide builds on
|
|
441
|
+
- [Curate the tools an upstream exposes](../mcp-gateway/how-to/curate-tools.mdx)
|
|
442
|
+
— static MCP tool curation to combine with flags
|
|
@@ -3,12 +3,12 @@ title: Secure a GCP Backend with Zuplo Upstream Auth
|
|
|
3
3
|
sidebar_label: GCP Backend
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
When using any
|
|
7
|
-
traffic that originates from the
|
|
6
|
+
When using any gateway as your API's entry point, it's critical that only
|
|
7
|
+
traffic that originates from the gateway is allowed to call your backend. There
|
|
8
8
|
are [many way to this](./securing-your-backend.mdx) depending on your
|
|
9
9
|
requirements and how your backend is hosted. This article will explain how to
|
|
10
10
|
use Google IAM to secure a GKE cluster so that only requests made through your
|
|
11
|
-
Zuplo
|
|
11
|
+
Zuplo Gateway will be allowed to call your GKE ingress.
|
|
12
12
|
|
|
13
13
|
Using GCP IAM to authorize your Zuplo Gateway to make requests to your backend
|
|
14
14
|
utilizes Googles core IAM system known as
|
|
@@ -33,11 +33,11 @@ set up behind an HTTP Load Balancer can be used.
|
|
|
33
33
|
The diagram below shows how the end-to-end system interacts. The important steps
|
|
34
34
|
to this process are:
|
|
35
35
|
|
|
36
|
-
1. The client makes a request to your Zuplo
|
|
37
|
-
1. The Zuplo
|
|
36
|
+
1. The client makes a request to your Zuplo Gateway
|
|
37
|
+
1. The Zuplo Gateway enforces any policies (for example authentication,
|
|
38
38
|
authorization, rate limiting)
|
|
39
|
-
1. The Zuplo
|
|
40
|
-
|
|
39
|
+
1. The Zuplo Gateway proxies the request to the public IP address of your GCP
|
|
40
|
+
Load Balancer. Zuplo adds authorization information to the request that
|
|
41
41
|
identify the request as coming from Zuplo
|
|
42
42
|
1. GCP Identity Aware proxy validates that the request comes from an authorized
|
|
43
43
|
Service Account identity. Unauthorized requests are rejected.
|
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
title: Health Check Handler
|
|
3
3
|
sidebar_label: Health Checks
|
|
4
4
|
description:
|
|
5
|
-
Learn how to set up health check endpoints to monitor your
|
|
5
|
+
Learn how to set up health check endpoints to monitor your Zuplo Gateway and
|
|
6
6
|
backend services.
|
|
7
7
|
tags:
|
|
8
8
|
- observability
|
|
9
9
|
- backends
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
Part of running a reliable
|
|
12
|
+
Part of running a reliable Zuplo Gateway is ensuring that the service is healthy
|
|
13
13
|
and available. With Zuplo, it's easy to set up a health check endpoint that can
|
|
14
14
|
be used to ensure the health of both your Zuplo Gateway and your backend (as
|
|
15
15
|
well as the connectivity between them).
|
|
16
16
|
|
|
17
|
-
A typical health check endpoint on your
|
|
17
|
+
A typical health check endpoint on your Zuplo Gateway will exercise some of the
|
|
18
18
|
most important policies (such as authentication) and then make a simple request
|
|
19
19
|
to your backend to ensure that it's reachable and functioning.
|
|
20
20
|
|
|
@@ -32,7 +32,7 @@ OpenAPI file this would look like:
|
|
|
32
32
|
"/health": {
|
|
33
33
|
"get": {
|
|
34
34
|
"summary": "Health Check",
|
|
35
|
-
"description": "Checks the health of the
|
|
35
|
+
"description": "Checks the health of the Zuplo Gateway and backend.",
|
|
36
36
|
"responses": {
|
|
37
37
|
"200": {
|
|
38
38
|
"description": "OK"
|
|
@@ -32,7 +32,7 @@ your service hosted to meet any sovereignty or data residency concerns.
|
|
|
32
32
|
|
|
33
33
|
Managed Dedicated is ideal for organizations that need:
|
|
34
34
|
|
|
35
|
-
- To run your
|
|
35
|
+
- To run your Zuplo Gateway on a specific cloud provider
|
|
36
36
|
- Custom networking configurations, such as restricting access to the public
|
|
37
37
|
internet
|
|
38
38
|
- Geographical deployment requirements where Managed Edge isn't feasible
|
|
@@ -50,7 +50,7 @@ Run Zuplo on your own infrastructure in any cloud or private data center. Zuplo
|
|
|
50
50
|
Self-Hosted runs exclusively on Kubernetes and is installed with a single Helm
|
|
51
51
|
chart into your cluster.
|
|
52
52
|
|
|
53
|
-
In the standard hybrid deployment model, you run the Zuplo
|
|
53
|
+
In the standard hybrid deployment model, you run the Zuplo Gateway and its
|
|
54
54
|
management plane on your Kubernetes cluster, while a small set of Zuplo cloud
|
|
55
55
|
services provides supporting features such as deployment configuration and API
|
|
56
56
|
key management. All API traffic to your gateways stays on your infrastructure.
|
|
@@ -5,7 +5,7 @@ sidebar_label: Environment variables
|
|
|
5
5
|
|
|
6
6
|
For security reasons, your local development doesn't have access to the
|
|
7
7
|
environment variables that you have configured on the Zuplo Portal. Instead,
|
|
8
|
-
your local Zuplo
|
|
8
|
+
your local Zuplo Gateway will load environment variables from a .env file.
|
|
9
9
|
|
|
10
10
|
1. Create a .env file in the root of your project.
|
|
11
11
|
2. Follow the following format
|
|
@@ -28,7 +28,7 @@ Zuplo supports the following metrics:
|
|
|
28
28
|
|
|
29
29
|
- request latency
|
|
30
30
|
- This measures the total time (in milliseconds) that a request takes once it
|
|
31
|
-
has entered the
|
|
31
|
+
has entered the Zuplo Gateway. It includes any outbound calls from the
|
|
32
32
|
gateway.
|
|
33
33
|
- request content length
|
|
34
34
|
- The content length of the request as reported by the content-length header.
|
|
@@ -3,7 +3,7 @@ title: Migrate to Zuplo from Other API Gateways
|
|
|
3
3
|
sidebar_label: Migration Overview
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Moving to Zuplo from another
|
|
6
|
+
Moving to Zuplo from another gateway is straightforward. Zuplo is
|
|
7
7
|
OpenAPI-native, so you can import your existing API definitions and start
|
|
8
8
|
configuring policies in minutes. This section provides migration guides for the
|
|
9
9
|
most common API gateways.
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
title: Deploying Zuplo from a Monorepo
|
|
3
3
|
sidebar_label: Monorepo Deployment
|
|
4
4
|
description:
|
|
5
|
-
"Deploy a Zuplo
|
|
6
|
-
|
|
5
|
+
"Deploy a Zuplo Gateway from a monorepo subdirectory using the Zuplo CLI and
|
|
6
|
+
GitHub Actions."
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
If your Zuplo
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
If your Zuplo Gateway lives inside a monorepo alongside other services, you can
|
|
10
|
+
deploy it using the [Zuplo CLI](../cli/overview.mdx) and your CI/CD provider.
|
|
11
|
+
Zuplo's [built-in GitHub integration](./source-control-setup-github.mdx)
|
|
12
|
+
connects each project to a dedicated repository and deploys automatically on
|
|
13
|
+
every push. Because it doesn't natively support projects located in a
|
|
14
|
+
subdirectory, you need to use the Zuplo CLI with a
|
|
15
|
+
[custom CI/CD pipeline](./custom-ci-cd.mdx) to deploy from the correct
|
|
16
|
+
directory.
|
|
17
17
|
|
|
18
18
|
This guide covers the project structure requirements, CI/CD configuration, local
|
|
19
19
|
development, and common troubleshooting steps for monorepo setups.
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Performance Testing Your
|
|
2
|
+
title: Performance Testing Your Gateway
|
|
3
3
|
sidebar_label: Performance Testing
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
<!-- vale Vale.Spelling["Blazemeter","JMeter","wrk"] = NO -->
|
|
7
7
|
|
|
8
8
|
Performance testing is critical for understanding the real-world performance of
|
|
9
|
-
your API when using
|
|
10
|
-
and accurate performance tests that properly measure latency and
|
|
9
|
+
your API when using a unified gateway like Zuplo. This guide helps you create
|
|
10
|
+
fair and accurate performance tests that properly measure latency and
|
|
11
|
+
throughput.
|
|
11
12
|
|
|
12
13
|
## Creating Fair Comparison Tests
|
|
13
14
|
|
|
14
|
-
When evaluating
|
|
15
|
+
When evaluating gateway performance, it's essential to ensure your tests
|
|
15
16
|
accurately reflect real-world conditions and provide a fair comparison between
|
|
16
17
|
direct backend calls and calls through your gateway.
|
|
17
18
|
|
|
@@ -53,7 +54,7 @@ traffic experiences significantly higher jitter (variance), making response
|
|
|
53
54
|
times less predictable.
|
|
54
55
|
|
|
55
56
|
This artificial performance boost from testing within the same cloud provider
|
|
56
|
-
can make it appear that
|
|
57
|
+
can make it appear that a gateway adds substantially more latency than it
|
|
57
58
|
actually does in real-world scenarios where traffic crosses network boundaries.
|
|
58
59
|
|
|
59
60
|
### Ensure Test Equality
|
|
@@ -106,14 +107,14 @@ impact in your analysis.
|
|
|
106
107
|
|
|
107
108
|
## Understanding Gateway Latency
|
|
108
109
|
|
|
109
|
-
|
|
110
|
+
Gateways necessarily add some latency to process requests. For Zuplo:
|
|
110
111
|
|
|
111
112
|
- **Base latency:** Approximately 20-30ms with no policies
|
|
112
113
|
- **Per policy:** Most policies add 1-5ms each
|
|
113
114
|
- **Complex policies:** Authentication, rate limiting, or custom code can add
|
|
114
115
|
5-15ms
|
|
115
116
|
|
|
116
|
-
This latency is the trade-off for the benefits
|
|
117
|
+
This latency is the trade-off for the benefits a gateway provides:
|
|
117
118
|
|
|
118
119
|
- Centralized authentication and authorization
|
|
119
120
|
- Rate limiting and quota management
|
|
@@ -299,7 +300,7 @@ Managed Dedicated, Zuplo can be deployed directly to:
|
|
|
299
300
|
|
|
300
301
|
This deployment model provides:
|
|
301
302
|
|
|
302
|
-
- **Minimal latency:** Your
|
|
303
|
+
- **Minimal latency:** Your gateway runs in the same cloud network as your
|
|
303
304
|
backend
|
|
304
305
|
- **Predictable performance:** Consistent sub-10ms latency for intra-region
|
|
305
306
|
traffic
|
|
@@ -364,7 +365,7 @@ periods.
|
|
|
364
365
|
## Summary
|
|
365
366
|
|
|
366
367
|
Creating fair performance tests requires careful attention to test conditions,
|
|
367
|
-
understanding of network topology, and realistic expectations about
|
|
368
|
+
understanding of network topology, and realistic expectations about gateway
|
|
368
369
|
overhead. By following these guidelines, you'll get accurate measurements that
|
|
369
370
|
help you make informed decisions about your API architecture.
|
|
370
371
|
|
|
@@ -4,7 +4,7 @@ sidebar_label: Azure Blob
|
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
This plugin pushes request/response logs to Azure Blob Storage. This can be used
|
|
7
|
-
to save request data generated by your
|
|
7
|
+
to save request data generated by your Zuplo Gateway to use for monitoring,
|
|
8
8
|
analytics, auditing, or debugging purposes.
|
|
9
9
|
|
|
10
10
|
<EnterpriseFeature name="Custom logging" />
|
|
@@ -4,8 +4,8 @@ sidebar_label: Azure Event Hubs
|
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
This plugin pushes request/response logs to Azure Event Hubs. This can be used
|
|
7
|
-
to stream the request data generated by your
|
|
8
|
-
analytics, auditing, or debugging purposes.
|
|
7
|
+
to stream the request data generated by your Zuplo Gateway to use for
|
|
8
|
+
monitoring, analytics, auditing, or debugging purposes.
|
|
9
9
|
|
|
10
10
|
<EnterpriseFeature name="Custom logging" />
|
|
11
11
|
|