Skip to content

Route registry

Admin HTTP API: desired route state in Postgres, live projection in etcd. Plat5’s only supported write path for the route registry.

The gateway loads and watches etcd.

routes.yml → POST /apply (or PUT /services/{name})
→ Postgres schema `routes` (revision)
→ etcd `edge/gateway/routes/{name}`
→ gateway watch

Postgres is the source of truth. etcd is a projection. A reconciler retries if a put/delete is missed.

Surface URL
Admin API (dev compose) http://localhost:5002
Health (internal) container :5003 (/health/live, /health/ready)

Prod compose does not publish the admin port by default.

Admin API routes require:

Authorization: Bearer <ADMIN_TOKEN>

Local default: dev-admin-token (ADMIN_TOKEN env). Required and non-empty in all environments.

Method Path Body Notes
POST /apply routes.yml shape (services: {…}) JSON or YAML Validate, expand, one PG transaction, project
GET /services — Current (non-deleted) services
GET /services/{name} — Current config plus name / revision
PUT /services/{name} ServiceConfig JSON New revision + project
DELETE /services/{name} — Tombstone revision; remove etcd key
GET /services/{name}/revisions — History (includes delete revisions)
GET /services/{name}/revisions/{rev} — One revision (config is null if delete)
POST /services/{name}/revisions/{rev}/restore — New revision copying that config

Apply is upsert of the services in the file. Services not in the file are left alone.

Named rate_limits live on each ServiceConfig. Apply and PUT /services/{name} validate shared: true policy names against all current services, not only the payload. Schema: routes.md.

Apply the identity catalog (services/identity/routes.yml) or a subset. Omitting a path hides it from the gateway. The identity process still serves that path on the network.

CLI:

plat5 routes apply ./routes.yml

curl:

curl -sS -X POST http://localhost:5002/apply \
-H "Authorization: Bearer dev-admin-token" \
-H "Content-Type: application/yaml" \
--data-binary @routes.yml
services:
widgets:
url: http://host.docker.internal:3000 # http://host:port the gateway can reach
user:
routes:
- path: /widgets
methods: [GET, POST]

url is from the gateway’s network view (in-cluster DNS, host.docker.internal, etc.). It must be exactly http://host:port (explicit port, no path or query; bare host:port, https:// and other schemes are rejected). Plat5 does not run your app process.

Success apply (200) — desired state committed:

{
"results": [
{ "service": "widgets", "status": "upserted", "revision": 1 }
]
}

Validation / auth / empty body failures use the Plat5 envelope (api-errors.md) and write nothing. A bad or missing service url is 422 VALIDATION_ERROR. A non-canonical path (trailing /, uppercase outside {params}) is 422. So is a service name, path, or path param name that an audit event cannot carry (routes.md). A method + path shape (params match params, literals match only literals) already owned by a different service rejects the whole apply with 409 ROUTE_CONFLICT (message lists each route and its owner). PUT /services/{name} and restore run exactly the same checks; see routes.md. Re-applying a service’s own routes replaces them and is not a conflict. A Postgres failure rolls the whole batch back (503). etcd projection is retried by the reconciler; apply does not fail after a successful commit.

Each write (apply/put/delete/restore) appends a revision. Restore appends a new revision that copies an old config after validating it like an apply: an old revision with a now-invalid url or path is 422, and one whose routes another service owns now is 409 ROUTE_CONFLICT.

Delete stores config: null and clears the etcd key. History remains. Restore of a delete revision is 422.

Key edge/gateway/routes/{service_name}
Value JSON ServiceConfig with full paths and list-form methods (route_prefix and nested methods maps already expanded)
Variable Default Purpose
PORT 5002 Admin API
INTERNAL_PORT 5003 Health
ETCD_URL http://localhost:2379 etcd (projection)
DATABASE_URL required Postgres; schema routes
ADMIN_TOKEN required Bearer token
SEED_ROUTES_DIR empty Dev: apply YAML for services with no history yet

Ready is 503 unless Postgres and etcd both answer.

Compose mounts services/identity/routes.yml → /seed/identity.yml and sets SEED_ROUTES_DIR. On boot, missing services (no routes.services row) are applied. Existing rows — including deleted — are not overwritten.

Prod compose does not seed. Apply identity routes yourself (CLI or curl).

Route types and validation live in each of gateway and route-registry (src/route_config.rs). Keep them aligned deliberately; etcd JSON and routes.md are the contract. Expand (route_prefix and nested methods maps) happens at write time only.