FGA as an API Gateway Enforcement Point, Part 2: Apigee, AWS, and Zuplo
Part 1 of this series built a real Cloudflare Worker as a policy enforcement point in front of a real Auth0 FGA store, verifying a JWT and calling FGA's Check API before forwarding a single request. This post extends that one. In this post I will detail how the same policy decision point, policy enforcement point split looks when the enforcement point is one of three other gateways instead of Cloudflare: AWS API Gateway with a Lambda authorizer, Zuplo, and Apigee.
Auth0 FGA is Auth0's hosted authorisation service, built on and fully compatible with the open-source OpenFGA project, so the model, the Check API, and the SDKs referenced throughout this post are the same whichever name you see on a given piece of documentation. This one is desk research, not a build. Nothing here has been deployed or run against a live tenant the way Part 1's demo was; every claim traces back to a vendor doc or a vendor blog post, and I have called out the gaps where the published examples leave something unspecified rather than filling them in with a guess.
The steps we will cover in this post are as follows.
- The same model, a different gateway
- AWS API Gateway: a Lambda authorizer that calls Check directly
- Zuplo: FGA as a named policy, not a pattern
- Apigee: no connector, two generic primitives
- Who owns fail-closed
The Same Model, a Different Gateway
Nothing about the FGA model from Part 1 changes here. A document type with owner, team, viewer, and editor relations, can_read and can_write as the only two relations any gateway ever checks, and member from team as the indirect-relationship pattern that lets team membership propagate access without a userset tuple on every document. What changes across the three platforms in this post is only the shape of the enforcement point, the thing standing between the request and FGA's Check call.
Part 1's own closing line said Cloudflare was the right place to start "not Apigee or another full API management platform," for reasons that had nothing to do with features, deployment speed and cost for a demo's call volume. That's still true. What follows isn't an argument that any of these three beats Cloudflare, it's a look at what each one's own documented pattern actually looks like, and specifically whether fail-closed semantics, a 503 that never gets mistaken for a 403, come with the platform or have to be built by hand every time.
AWS API Gateway: a Lambda Authorizer That Calls Check Directly
AWS API Gateway's Lambda authorizer is the closest sibling to Part 1's Worker, a function that runs before the backend integration and returns an allow or deny decision. Both Auth0's own writeup and the AWS Partner Network blog covering the same integration walk through wiring Okta FGA into exactly this spot.

The published example reads relation, user, and object straight off the incoming request headers, builds an FGA client with client-credentials, and calls Check. It uses the openfga_sdk package directly, the same SDK Auth0 FGA tenants talk to under the hood:
user_obj = {
'relation': event['headers']['relation'],
'user': event['headers']['user'],
'object': event['headers']['object'],
}
body = ClientCheckRequest(
user=f"user:{user_obj['user']}",
relation=user_obj['relation'],
object=user_obj['object'],
)
response = await fga_client.check(body, options)
response = {
"isAuthorized": result.allowed,
}First, there's no JWT validation anywhere in that snippet, the relation and object arrive as plain header values the caller sets directly, which is a reasonable simplification for a demonstration but is exactly the kind of client-asserted mapping Part 1's Worker deliberately refused to trust, deriving the relation and object from the actual HTTP method and path instead of a value the caller could set to whatever it wanted. Second, neither article mentions whether or how the authorizer's own result gets cached. API Gateway supports a configurable authorizer result TTL out of the box, and that's exactly the kind of setting Part 1 called out as a deliberate design decision, no caching at all, because a cached allow surviving a tuple deletion would mean a revoked user still gets through on the next request. Neither published example says which way that TTL is set for this integration, so it's worth checking explicitly rather than assuming either default is the one you want.
The isAuthorized boolean the function returns maps to API Gateway's simple authorizer response format: true lets the request proceed to the backend integration, which then produces whatever status code the backend itself returns, and false makes API Gateway return a 403 directly without ever reaching the backend. Neither source shows what happens when the FGA call itself fails rather than returning a clean allow or deny; that outcome depends on how the function's own error handling is written, and the published examples don't cover that branch.
Zuplo: FGA as a Named Policy, Not a Pattern
Zuplo ships a first-party FGA authorization policy, named openfga-authz-inbound in Zuplo's own configuration (the policy predates Auth0's hosted offering and still carries the open-source project's name), as a product feature rather than a pattern you assemble from general-purpose primitives. The policy's config lives in config/policies.json and points at your Auth0 FGA store the same way it would point at a self-hosted OpenFGA instance, same apiUrl, storeId, and authorizationModelId fields either way:
{
"name": "openfga-authz",
"policyType": "openfga-authz-inbound",
"handler": {
"export": "OpenFGAAuthZInboundPolicy",
"module": "$import(@zuplo/runtime)",
"options": {
"apiUrl": "https://api.us1.fga.dev",
"storeId": "01ABCDEF...",
"authorizationModelId": "01ABCDEF...",
"credentials": {
"clientId": "...",
"clientSecret": "...",
"apiAudience": "https://api.us1.fga.dev/",
"oauthTokenEndpointUrl": "https://auth.fga.dev/oauth/token"
}
}
}
}
The actual user, relation, and object for a given request get set programmatically, a small, route-specific policy runs ahead of the FGA policy in the chain and calls OpenFGAAuthZInboundPolicy.setContextChecks(context, { user, relation, object }), pulling the user from the JWT claim (`user:${request.user.sub}`) and the object from a route parameter. Zuplo's docs show two variants of that upstream policy, one with a hard-coded relation per route, another that reads custom x-authz metadata off the matched OpenAPI operation to derive the relation and object dynamically. Either way, the route's policy chain ends up looking like ["jwt-auth", "authz-can-read-folder", "openfga-authz"], authentication, then relation/object resolution, then the actual Check.
Zuplo's policy has an allowUnauthorizedRequests option that defaults to false. A request the policy doesn't authorise denies by default unless you deliberately flip that flag, which is a meaningful difference from AWS's example above, where that same default is left entirely to whatever the authorizer function's own code happens to do. Zuplo's docs don't spell out whether a transport failure or a timed-out Check call is treated the same way as an explicit deny, so I wouldn't assume the allowUnauthorizedRequests default extends to every possible failure mode without checking that behaviour directly. This is listed as an Enterprise-tier feature in Zuplo's own docs, free for development and testing, with production use gated through a conversation with their sales team.
Apigee: No Connector, Two Generic Primitives
I didn't find an Apigee-documented FGA-specific policy, nothing comparable to Zuplo's named policy type or even AWS's documented authorizer pattern. What exists instead is a general pattern for calling any external authorization service mid-flow, assembled from two of Apigee's standard building blocks: a ServiceCallout policy makes a synchronous HTTP call to an external service during request processing, and a conditional RaiseFault policy short-circuits the flow when that call's result says to deny.

Dino Chiesa's Apigee-External-Authorization-1 repo demonstrates this shape for an attribute-based access control variant, not FGA specifically, but the high-level pattern carries over regardless of which decision service sits on the other end of the callout. The proxy's PreFlow runs a callout to fetch the applicable rules (in that example, via a shared flow that itself wraps a ServiceCallout to an external data source), evaluates the request against them in a JavaScript callout, and conditionally raises a fault if the evaluation denies. Replacing "fetch and locally evaluate ABAC rules" with "call FGA's Check endpoint and read back allowed" keeps the same broad shape, ServiceCallout into a decision service, a parsing step to pull the result out of the response body, then a conditional RaiseFault, but that middle step still has to exist. Nothing hands you a parsed, ready-to-branch-on boolean the way Zuplo's setContextChecks() or AWS's isAuthorized field does.
Apigee gives you the primitives, not the policy. There's no fga-check step to drop into a flow the way Zuplo's FGA policy just exists, and I didn't find an Apigee-published worked example comparable to the AWS posts cited above. Every decision Part 1 had to make explicitly for the Worker, how to map the method and path to a relation and object, what to do when the Check call itself fails, whether that failure renders as a deny or something else entirely, is equally unaddressed by anything Apigee ships. A ServiceCallout that times out or errors still needs its own fault-handling path designed in Apigee's own policy configuration, same as the Worker needed one written in TypeScript; neither platform hands you that branch for free.
Who Owns Fail-Closed
The one thread running through all four gateways now, Cloudflare from Part 1 plus the three in this post, is who's actually responsible for the distinction between "the check said no" and "the check didn't answer." Here's where that responsibility sits on each one.
- Cloudflare (Part 1): entirely on the integrator. Part 1's Worker built the 401/403/503 split by hand, as a deliberate, tested design decision, because nothing in the Workers runtime or the FGA SDK gives you that distinction for free.
- AWS API Gateway: on the integrator, and the published example doesn't show it being handled at all. The authorizer function's own code decides what an unreachable FGA store turns into; neither vendor's writeup shows that branch.
- Zuplo: partly on the platform.
allowUnauthorizedRequestsdefaults tofalse, so a request the policy doesn't authorise denies by default without the integrator writing anything for that specific case. Whether that same default covers a transport failure or a timed-out Check call isn't spelled out in the docs, so a 503-shaped failure collapsing into the same outcome as an explicit 403 is a real possibility worth checking rather than assuming. - Apigee: entirely on the integrator, same as Cloudflare and AWS, because there's no FGA-aware policy at all, just a
ServiceCalloutand aRaiseFaultwhose fault logic is whatever you write.
A managed policy that fails closed by default is a real convenience if the default suits you; full control over every branch is a real advantage if it doesn't. What's consistent across all four is that the policy decision point itself, FGA's Check call, never changes shape. Only how much of the enforcement discipline around it you get for free is what actually varies.
Final Thoughts
"Does this gateway support FGA" is the wrong first question, because the answer is almost always some version of "you can call Check from anywhere that can make an HTTP request." The better question is how much of the fail-closed, single-choke-point discipline Part 1 had to build explicitly comes with the platform versus how much is still on you. Zuplo ships a documented default for one specific failure case, denying an unauthorised request, that AWS and Apigee leave entirely to the integrator's own code. If you're picking a gateway for a production deployment rather than a demo, that's the question worth asking the vendor directly rather than assuming any API gateway handles it the same way.