Gateway logic in TypeScript, not a DSL
Write custom inbound policies, outbound policies, and full request handlers in typed TypeScript. Web-standard APIs (Fetch, Crypto, Streams). V8 isolates across 300+ POPs. Compose other routes with `context.invokeRoute`. Share state with `context.custom`. No proprietary syntax, no separate Lambdas.
403 Forbidden for sanctioned regions
policy executes at the edge · type-safe imports from @zuplo/runtime
Every gateway DSL bottoms out the same way: write a Lambda
Whatever the gateway can't express becomes a sidecar service. Once you're stitching sidecars, the gateway has stopped being a gateway and started being a routing layer in front of your real architecture.
Proprietary DSLs that fight you on day two
The first rule looks easy. The third rule is a Stack Overflow excavation. The seventh requires a vendor consultant. You wanted gateway logic; you got a custom programming language with no debugger.
“We need a Lambda for that”
Custom logic the gateway can't express moves to a Lambda. Now there's a network hop, a separate deploy pipeline, a separate IAM role, and a separate place to look when something breaks at 2am.
Gateway code lives outside Git history
Policies were clicked together in a UI. There's no diff for the change that broke production. Reverting means clicking through the audit trail, hoping nothing else changed at the same time.
Test? In production, mostly
There's no way to unit-test the proprietary policy language. "Testing" is deploying to staging and hitting endpoints. The fast feedback loop your application code has at the gateway tier doesn't exist.
Real code, real APIs, real performance
TypeScript, the language you already use
Custom policies are typed TypeScript functions imported from `@zuplo/runtime`. Code review them in the same PR as your application code. Unit-test with Jest or Vitest. Debug them with the same instincts you use everywhere else.
Full request and response context
Mutate the request before the handler. Transform the response before send. Read API key metadata, consumer attributes, geo, environment vars. Pass enrichment data between policies via `context.custom`. Log structured events with `context.log`.
Runs at the edge in 300+ POPs
Custom code runs in the same V8 isolates as Zuplo's built-in policies, on the same edge runtime, in 300+ data centers. Most policies add 1–5 ms. No extra network hop, no Lambda, no separate deploy.
Inbound, outbound, handler — all just TypeScript
Build an inbound policy that enriches every request with consumer metadata. Compose a BFF that aggregates three downstream routes in one handler. All in TypeScript, typed end-to-end, deployed via GitOps.
The skills you have, applied to the gateway tier
Web-standard APIs, not a vendor SDK
fetch, Headers, Request, Response, Web Crypto, Streams, URLPattern, TextEncoder/Decoder. Anything that runs in modern Chrome's Worker context generally runs in Zuplo. Skills transfer. Stack Overflow answers transfer.
context.invokeRoute · compose without leaving the gateway
Build a BFF in one handler that aggregates orders, profile, and billing routes — each one running through its own policies, all in-process. Build a multi-step MCP tool that orchestrates real APIs. No HTTP hops, no parallel infrastructure.
Type-safe options end-to-end
Declare a `TOptions` type in your policy. Reference the same JSON shape in `config/policies.json`. The types are validated at build, the values at deploy. Misconfiguring a policy is a build error, not a runtime surprise.
Share policies as npm packages
Publish your team's custom policies as an internal npm package. Consume them from any Zuplo project. Version-pin, semver, automate updates with Renovate or Dependabot — everything you already do for app code, applied to gateway code.
What teams use this for
“We need a tier-aware response transformer.”
Inbound policy reads the consumer's tier from API key metadata and writes it to `context.custom.tier`. Outbound policy reads `context.custom.tier` and redacts PII fields if the tier is free. Two policies, ~20 lines each, no separate service.
“The mobile app needs an aggregated dashboard payload.”
Custom handler calls `context.invokeRoute("/v1/orders")`, `context.invokeRoute("/v1/profile")`, and `context.invokeRoute("/v1/billing")` in parallel — each one running through its own auth, rate-limit, and validation policies — then assembles the response. No BFF service to maintain.
“We need to enrich every request with internal customer data.”
Inbound policy hits your internal `/customers/{sub}` endpoint via `fetch`, attaches the result to `context.custom.customer`, and forwards. The handler and downstream policies read it without re-fetching. Cache the lookup with a Map for warm-worker reuse.
“Compliance wants every request body archived to S3.”
Outbound policy stringifies request + response, posts to your S3 bucket via the AWS SDK or a signed URL — same pattern documented for Azure Blob and S3. Async, no impact on response latency. Audit trail lives in your bucket, queryable with Athena.
Frequently Asked Questions
Common questions about programmable gateway logic on Zuplo.
Stop bottoming out at “write a Lambda”
Free Zuplo project, write your first custom inbound policy in TypeScript, and ship it to the edge in an afternoon.