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.

FieldRequiredWhat it is
hostyesThe host the entry applies to.
pathPrefixyesThe path prefix on that host. The next path segment after the prefix is the service name.
selectyesWhich public routes the entry serves. See below.
corsOriginsnoThe 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:

kindExtra fieldsServes
allnoneEvery public route under the prefix.
biomesbiomes: list of biome idsOnly the public routes of those installed biomes.
globsallow, deny: lists of patternsPublic 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

yaml
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-portal

Apply it with xema apply, as described in Xema-as-Code.

In Terraform

hcl
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).

RouteMethodWhat it does
/.xema/sessionGETReturns 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>GETStarts 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/callbackGETFinishes sign-in: a 303 to returnTo. On failure it redirects to /?xema-session-error=<code>.
/.xema/session/logoutPOSTEnds the session, clears the cookie and returns { "endSessionUrl": ... } for the browser to visit.
/.xema/session/logged-outGETThe landing after the identity provider signs the user out; 303 to returnTo.
/.xema/session/active-orgPUTSets 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.

CodeStatusMeaning
SESSION_EXPIRED401The session ended or is not this host's. On API calls and streams.
AUTHORITY_CHANGED401A receiving service refused the request because the user's authority changed. On API calls and streams.
SESSION_ORIGIN_REFUSED403A cookie request without same-origin evidence for this host.
PRINCIPAL_CONTEXT_REFUSED403The identity boundary refused the user for the named organization.
AMBIGUOUS_CREDENTIALS400The session cookie and an Authorization header on one request. Send one.
RETURN_TO_REFUSED400returnTo is not a same-origin relative path.
ORG_AMBIGUOUS400A stream request names two different organizations.
SESSION_REQUEST_INVALID400A session route's query or body does not match its closed schema.
SIGN_IN_RATE_LIMITED429Too many sign-in attempts from this network. Carries Retry-After.
BROWSER_SESSION_UNAVAILABLE503The session service has no ready instance. Carries Retry-After.
SESSION_REFRESH_CONTENDED503Another request is refreshing the session. Retry after Retry-After.
FRONTEND_UNAVAILABLE503The 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.


  • 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