Public exposure and CORS
Public exposure is an installation-wide setting that decides which public API routes the gateway serves on which host, and which browser origins (CORS) may call them. Use it to narrow what an installation shows to the outside world, for example to serve only one biome's routes on a partner-facing host.
The gateway applies this setting to every request it serves. The setting is optional: an installation that never writes it runs on the defaults below.
What it can and cannot do
- Exposure authority stays with each biome. Only routes a biome declares public can ever be served. The setting only narrows; it never widens, and it cannot make a non-public route reachable.
- The setting is installation-wide. One document covers all hosts of the installation.
- Only the installation owner writes it, through the setup plane (see Writing the setting). A setting can open a new host, so it is not delegated.
Defaults
With no setting written:
- Every API host the deployment declares, plus the deployment's own web origin at
<base>/api, serves all public routes. - Any other host serves nothing.
- CORS allows the host's own origin plus the deployment's registered frontend origins.
The setting
The setting is a list of entries. Each entry binds a host and a path prefix to a selection of public routes.
| Field | Required | What it is |
|---|---|---|
host | yes | The host the entry applies to. |
pathPrefix | yes | The path prefix on that host. The next path segment after the prefix is the service name. |
select | yes | Which public routes the entry serves. See below. |
corsOrigins | no | The browser origins allowed to call the entry's routes. Absent means the default; present means exactly that list. An empty list allows no origin. |
Two entries on one host cannot share or nest prefixes. Credentialed cross-origin requests are only allowed for the deployment's own web origin.
Selecting routes
select has a kind:
kind | Extra fields | Serves |
|---|---|---|
all | none | Every public route under the prefix. |
biomes | biomes: list of biome ids | Only the public routes of those installed biomes. |
globs | allow, deny: lists of patterns | Public routes matching an allow pattern and no deny pattern. Deny wins. |
An empty selection serves nothing, which closes the entry.
Glob grammar
A pattern has the form /<service>/<template>.
*matches exactly one path segment.**matches the rest of the path and is allowed only as the final segment.- There is no regex.
- A pattern is at most 256 characters and 32 segments.
Limits
An installation holds at most 64 entries, and each entry at most 64 patterns.
Validation
A write is refused as a whole if any part is invalid: a bad glob, an unknown biome id, a duplicate or nested prefix on one host, or a malformed origin. Nothing is partially applied.
How requests are matched
The gateway matches each request on one canonical path. A request whose path is ambiguous, or that uses an encoding trick, is refused with 400 before any matching happens. This covers encoded slashes (%2F), backslashes, dot segments, double encoding, ; in the path, and method-override headers.
A request that matches no entry, or whose route the entry does not select, gets 404. If two entries, or two route templates, give conflicting answers for the same request, the request also gets 404. Static assets and webhook routes are not governed by this setting.
Writing the setting
The setting is a platform-scoped resource of kind public-exposure, with the single id installation. Two surfaces write it, and both go through the setup plane.
- Every write carries the current revision. A write against a stale revision is refused with 409; read the current revision and retry.
- Every change is audited, with the actor and the revision before and after.
- Removing the resource from a stack stops managing it. It never resets the setting; the last written value stays in force.
In a stack
1apiVersion: xema/v1
2stack: production
3resources:
4 - kind: public-exposure
5 id: installation
6 spec:
7 entries:
8 - host: api.acme.dev
9 pathPrefix: /api
10 select:
11 kind: globs
12 allow:
13 - /orders/**
14 - /catalog/products/*
15 deny:
16 - /orders/admin/**
17 corsOrigins:
18 - https://shop.acme.dev
19 - host: partners.acme.dev
20 pathPrefix: /api
21 select:
22 kind: biomes
23 biomes:
24 - acme-partner-portalApply it with xema apply, as described in Xema-as-Code.
In Terraform
1resource "xema_public_exposure" "installation" {
2 entries = [
3 {
4 host = "api.acme.dev"
5 path_prefix = "/api"
6 select = {
7 kind = "globs"
8 allow = ["/orders/**", "/catalog/products/*"]
9 deny = ["/orders/admin/**"]
10 }
11 cors_origins = ["https://shop.acme.dev"]
12 },
13 {
14 host = "partners.acme.dev"
15 path_prefix = "/api"
16 select = {
17 kind = "biomes"
18 biomes = ["acme-partner-portal"]
19 }
20 },
21 ]
22}As with every provider resource, HCL attributes are the snake_case spelling of the stack spec fields.
The resource goes through the org-admin surface like every other Xema-as-Code resource, so configure the provider with an org; the write still succeeds only when the token belongs to the installation owner.
Browser sign-in at the session edge
A frontend served by the gateway signs users in through the gateway's session edge, so the browser never handles tokens. The edge sets an HTTP-only session cookie; every answer from it is Cache-Control: no-store. The routes sit at the host root of the frontend, under a prefix named for the edge (/.xema for the Xema web app), and anything else under the prefix is 404 (405 for a known path with the wrong method).
| Route | Method | What it does |
|---|---|---|
/.xema/session | GET | Returns the session view: signed out, or the signed-in user, the user's active organizations, the active organization and when the session ends. It never returns a token. |
/.xema/session/login?returnTo=<path> | GET | Starts sign-in: a 302 to the identity provider, and sets a short-lived cookie that ties the sign-in to this browser. Only returnTo is accepted, and it must be a same-origin relative path. |
/.xema/session/callback | GET | Finishes sign-in: a 303 to returnTo. On failure it redirects to /?xema-session-error=<code>. |
/.xema/session/logout | POST | Ends the session, clears the cookie and returns { "endSessionUrl": ... } for the browser to visit. |
/.xema/session/logged-out | GET | The landing after the identity provider signs the user out; 303 to returnTo. |
/.xema/session/active-org | PUT | Sets the session's default organization ({ "orgId": ... }) and returns the session view. |
The xema-session-error redirect carries one of SIGN_IN_REJECTED (the identity provider refused or returned an error), SIGN_IN_EXPIRED (the sign-in is unknown, used, expired, or was begun in another browser; start again), SIGN_IN_REFUSED (the identity boundary refused the user, for example for not being a member) and SIGN_IN_UNAVAILABLE (a component the sign-in needs did not answer; retry).
An outage of the session service answers 503 with Retry-After and never signs anyone in or out.
Gateway problem codes
Session and API refusals at the edge are problem documents (application/problem+json style) with a code from this closed list. Branch on the code, not the text.
| Code | Status | Meaning |
|---|---|---|
SESSION_EXPIRED | 401 | The session ended or is not this host's. On API calls and streams. |
AUTHORITY_CHANGED | 401 | A receiving service refused the request because the user's authority changed. On API calls and streams. |
SESSION_ORIGIN_REFUSED | 403 | A cookie request without same-origin evidence for this host. |
PRINCIPAL_CONTEXT_REFUSED | 403 | The identity boundary refused the user for the named organization. |
AMBIGUOUS_CREDENTIALS | 400 | The session cookie and an Authorization header on one request. Send one. |
RETURN_TO_REFUSED | 400 | returnTo is not a same-origin relative path. |
ORG_AMBIGUOUS | 400 | A stream request names two different organizations. |
SESSION_REQUEST_INVALID | 400 | A session route's query or body does not match its closed schema. |
SIGN_IN_RATE_LIMITED | 429 | Too many sign-in attempts from this network. Carries Retry-After. |
BROWSER_SESSION_UNAVAILABLE | 503 | The session service has no ready instance. Carries Retry-After. |
SESSION_REFRESH_CONTENDED | 503 | Another request is refreshing the session. Retry after Retry-After. |
FRONTEND_UNAVAILABLE | 503 | The frontend's page upstream has no ready instance. |
Inspecting the live route table
Platform admins can see what the public gateway is dispatching on at /system/gateway-routes (Gateway Routes, in the Admin menu). The page lists the merged route table with each route's host and path match, owner service and upstream target, auth scheme and websocket support, and whether the route is static configuration or a dynamic registration owned by a service. A dynamic registration is self-evicting. The page reads GET /bff/routes, which requires the platform owner or platform admin role.
Related
- Xema-as-Code — stacks,
xema apply, and the Terraform provider. - Biomes — where a biome declares which of its routes are public.
Previous: ← Xema-as-Code