Console
Sign in
Docs for Repository maintainer
On this page
  1. Before you start
  2. Steps
  3. 1. Write the route
  4. 2. Validate and publish the table
  5. 3. Give your service the public key
  6. 4. Verify the signed identity on every request
  7. 5. Test the verifier against the gateway's signer
  8. 6. Grant callers access in Access
  9. Check that it worked
  10. If something goes wrong
  11. Related

Put a service behind the gateway

Expose your own HTTP service through the gateway: describe it as a route, publish the route table, make the service check the identity the gateway signs, grant callers access in Access, and prove the whole path works. Developers who own the service do most of it (developer); an operator with control-plane rights publishes the table (sysops).

Read byPlatform operatorApplication developer

Before you start#

You needWhyWhere it is described
A running deployment: gateway, Access, and the control plane when routes live in the shared stateThe route is served by the gateway and decided by AccessPlatform operations, Deploy the platform
Your service on an address only the gateway can reachThe signature proves the gateway sent a request; network exposure would let others tryGateway operations: deployment review
A control-plane token with the config scope, or write access to the route-table filePublishing a table is a config writegateway spec §12.7
The deployment's public identity keyYour service verifies signatures with itUpstream identity: key
An Access administration credential and the tenant's principalsCallers need a grant on your routeManage access
platformctl with a profile for the deployment (optional)Every step below has a CLI formCLI usage

Steps#

1. Write the route#

A route is one entry in the routes list of the route table. Pick an id (lowercase kebab-case, unique, at most 63 characters): Access receives it as routeId, so grants and logs name your service by it.

json
{
  "id": "reports-api",
  "match": { "host": "api.example.com", "path": "/reports/{workspaceId}/" },
  "upstream": "http://reports.internal:3000",
  "auth": { "mode": "any", "workspace": { "param": "workspaceId", "requireUuid": true } },
  "forwardAuthorization": false
}

Decide each part on purpose:

PartChoiceReference
match.hostAn exact host, *.example.com for subdomains, or *. The most specific host always wins.gateway spec §3.3
match.pathLiteral segments plus placeholders. Add match.methods only if the route must refuse the other methods.gateway spec §3.1
Workspace placeholderauth.workspace.param names the placeholder Access is asked about; requireUuid turns a non-UUID into 400 bad_request.gateway spec §6
No workspaceLeave auth.workspace out and set accessCheck: true so Access still decides on subject, route and roles.gateway spec §6.3
auth.modesession for browsers, jwt for API clients, any for both, none only for public content.gateway spec §5.1
upstreamOne URL, or a list tried in order when a resendable request cannot connect.gateway spec §11.4
rewriteSet it when your service expects another path than the public one, for example /{rest} to drop the prefix.gateway spec §11.1.1
forwardAuthorizationfalse unless your service really needs the caller's bearer token.gateway spec §7
crossSitestrict when your service changes state on a GET.gateway spec §8
timeouts, maxBodyBytesRaise them for slow answers or large uploads; keep the ingress limits in step.gateway spec §11.2
accessCacheTtlMs, accessStaleGraceMs0 cache for immediate revocation; a grace to ride out short Access outages.Gateway configuration: route-level overrides
websockettrue to accept upgrades; the upstream must then speak plain ws.gateway spec §10

The gateway refuses an unknown key, a route that is ambiguous with another, and accessCheck on a none route, so mistakes show up at validation rather than in traffic (gateway spec §3.2).

2. Validate and publish the table#

The gateway always replaces the whole table, never one route. Start from the active table, add your route to its routes list, and save the result as routes.json.

With the CLI (PLATFORMCTL_CONTROL_TOKEN holds the token):

bash
platformctl routes get                      # active revision and table
platformctl routes validate ./routes.json   # errors, conflicts, warnings, diff; writes nothing
platformctl routes apply ./routes.json      # new revision, compare-and-swap on the active one
platformctl routes replicas                 # every replica should report the new revision

Over HTTP, against the control plane's internal address:

bash
CONTROL=http://127.0.0.1:9090
curl -s -H "Authorization: Bearer $TOKEN" $CONTROL/v1/config | jq '.revision'
curl -s -H "Authorization: Bearer $TOKEN" --data-binary @routes.json $CONTROL/v1/config/validate
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H 'If-Match: "41"' \
  --data-binary @routes.json $CONTROL/v1/config
curl -s -H "Authorization: Bearer $TOKEN" $CONTROL/v1/replicas

A 409 means the table moved since you read it, or you touched a protected route: read it again and reapply your change. The full procedure, with rollback, is in Gateway operations: changing the route table.

A deployment that keeps routes in a file (GATEWAY_CONFIG_SOURCE=file) has no control plane write: edit the GATEWAY_CONFIG file instead, and the gateway reloads it atomically (gateway spec §12.3).

Read the warnings the validation and the running-policy log report for your route, such as authorization_forwarded or no_workspace_check. Each one names a permissive setting you should be able to justify (gateway spec §12.2).

3. Give your service the public key#

Get the public half of the deployment key from whoever runs the gateway, and load it into your service's configuration:

bash
bun run identity:key --public

Your service only ever holds this public key. It never needs the private key or the session secret, and a public key cannot sign an identity (Upstream identity: key).

4. Verify the signed identity on every request#

The gateway removes any x-gateway-* header a client sent and writes its own: x-gateway-user, x-gateway-email, x-gateway-roles, x-gateway-workspace and x-gateway-signature (Upstream identity: headers). Your service must check the signature before it reads any of them.

The quickest correct path is to copy the standalone reference verifier, examples/upstream-identity/verify.ts, whose only import is jose, and call it from your request handler:

ts
const result = await verifyIdentity({ request, headers, publicKey: key })
if (!result.ok) {
  log.warn({ reason: result.reason }, 'identity refused')
  return new Response('forbidden', { status: 403 })
}

The complete TypeScript and Go examples are in Upstream identity: code. Three inputs trip up most implementations:

InputTake it from
PathThe path your service received, after any rewrite
QueryThe raw query string, with its leading ?, or an empty string
Hostx-forwarded-host, lower-cased

A mode: none route forwards requests with no identity and no signature, so decide per route whether your service accepts them (Upstream identity: failed verification).

The roles in x-gateway-roles come from the caller's token, which was issued at login. If your service needs the caller's current permissions, it can read them from Access's context contract with a credential of its own, as Data does (Access API: access context).

5. Test the verifier against the gateway's signer#

The reference verifier ships with verify.test.ts, which signs with the gateway's real signer. Run the same kind of test against your own implementation, and pin the gateway revision you test against (Upstream identity: compatibility).

6. Grant callers access in Access#

The gateway asks Access whether the caller's principal may use the workspace on route reports-api. A grant answers yes when its workspace and route match or are left empty (null covers every workspace or every route). The permission key is up to you, as long as it is enabled (Access spec §8.3).

bash
platformctl access grants put --tenant acme --user-id <principal id> \
  --permission reports.read \
  --workspace-id 11111111-1111-4111-8111-111111111111 --route-id reports-api

The same grant over HTTP:

bash
curl -sS -X PUT https://access.example.com/v1/admin/grants \
  -H "authorization: Bearer $ACCESS_ADMIN_TOKEN" -H "content-type: application/json" \
  -d '{"tenant":"acme","userId":"<principal id>","permission":"reports.read",
       "workspaceId":"11111111-1111-4111-8111-111111111111","routeId":"reports-api"}'

In the console, an operator does the same on the tenant's Rights page (console user guide). Grants to groups, roles and expiry dates are covered in Manage access and the Access API.

Check that it worked#

  1. Put your route's decision to Access before any traffic arrives. A denial ends with exit status 6:
bash
   platformctl access decide --subject <principal id> \
     --workspace 11111111-1111-4111-8111-111111111111 --route reports-api
  1. Sign in through the gateway with a redirect to your route, for example https://api.example.com/auth/login?redirect=/reports/11111111-1111-4111-8111-111111111111/, then call it again with the session cookie:
bash
   curl -s -i "https://api.example.com/reports/11111111-1111-4111-8111-111111111111/summary" \
     -H "cookie: gateway_session=<value from the browser>"
  1. Your service's log should show a verified identity whose subject is the principal id. The gateway's log line for the response's x-request-id names route reports-api, the subject and the workspace (gateway spec §12.2).
  2. Try the refusals on purpose. A principal without a grant gets 403 workspace_forbidden from the gateway. A request sent straight to your service's address, without the gateway, must get your own 403.
  3. On /metrics, gateway_requests_total{route="reports-api"} counts your traffic and gateway_upstream_failures_total stays flat (gateway spec §12.1).

If something goes wrong#

SymptomLook here
Validation or apply refuses the table, or the change has no effectGateway troubleshooting: route-table change, control plane statuses
404 no_route for your pathGateway troubleshooting
401 unauthorized or a login loopGateway troubleshooting: 401
403 workspace_forbiddenGateway troubleshooting, then Access: decisions denied
400 bad_request on the workspace pathGateway troubleshooting
Your service refuses every signatureGateway troubleshooting: identity signature
502 bad_gateway or 504 gateway_timeoutGateway troubleshooting: upstreams
Anything elseTroubleshoot a request