Write a service
A Plat5 service is an HTTP server in any language. It listens on the paths Plat5 rewrites to, and reads ids from them. The starters (Go, Node, Bun) are complete examples.
1. Write the handler
Section titled “1. Write the handler”Listen on the path with the owner’s id in it, and scope every lookup by that id.
// GET /organizations/:organization_id/projectsfunc (h *Handler) List(c fiber.Ctx) error { orgID := c.Params("organization_id") list, err := h.store.ListByOrg(c.Context(), orgID) if err != nil { return errors.InternalError() } return c.JSON(fiber.Map{"projects": list})}There is no auth code. By the time this runs, Plat5 has signed the caller in, checked the route’s permission, applied the rate limit and recorded the request.
2. Publish its routes
Section titled “2. Publish its routes”services: api: organization: routes: - path: /org/projects upstream: /organizations/{subject.organization_id}/projects methods: GET: POST: required_labels: [projects:write] - path: /org/projects/{project_id} upstream: /organizations/{subject.organization_id}/projects/{path.project_id} methods: GET: PATCH: required_labels: [projects:write]pathis what clients call.upstreamis what your service receives.{subject.organization_id}is filled from the credential.{path.project_id}is copied from the client’s path.required_labelsare the permissions a member’s role must grant. See Roles and permissions.- Add
rate_limit: { requests: 30, window_seconds: 60 }to a route to set its own limit.
Locally, list the file under routes: in plat5.yml and say where the service listens under upstreams::
upstreams: api: "3000" # your service on the host, port 3000routes: - ./routes.identity.yml - ./routes.audit.yml - ./routes.ymlThen plat5 routes apply, or plat5 start, publishes them. The gateway picks up changes without a restart.
3. Say what changed
Section titled “3. Say what changed”On a write, tell the audit log what changed with a response header. Plat5 stores it and removes it before the response reaches the client.
HTTP/1.1 200 OKX-Plat5-Audit-Details: {"name":{"from":"Launch","to":"Launch v2"}}On a create, also send the new id as X-Plat5-Audit-Created. Never put a secret in either header.
4. Answer like every other service
Section titled “4. Answer like every other service”Plat5 has one way to do the parts every service shares. The starters already follow it, so keep their shape as you add endpoints:
- Errors use the envelope:
{"error":{"type","code","message","request_id","details"}}, with amessageyou’d show a person. - Lists take
limitandstarting_after, and return the collection withhas_more. - Health and metrics stay on the internal port:
/health/live,/health/ready,/metrics.
The full set is in Standards.
5. Follow the rules
Section titled “5. Follow the rules”- Trust the path. Don’t parse
AuthorizationorX-API-Key(Plat5 removes them), and don’t validate tokens. - Stay private. The path is only trustworthy if nothing but the gateway can reach your service.
- Enforce state rules, like “an archived project is read-only”. Those hold no matter who asks.
- Log the
X-Request-IDand passtraceparenton to anything you call. Don’t setX-Request-IDyourself. - Store owners, not authors. Your service isn’t told who called. If who did something matters to your product, make it a record the member owns, created on a
memberroute. Otherwise, it’s in the audit log.
6. Clean up when things go away
Section titled “6. Clean up when things go away”When an organization or member is removed, your service may hold data for it. Read the lifecycle feed and act on organization.deleted and member.removed:
GET /internal/lifecycle-events?starting_after=<last id you processed>X-Plat5-Internal-Token: <INTERNAL_AUTH_TOKEN>The feed is on identity’s internal port, not the gateway. Acting on an event must be safe to repeat.
More: gateway-contract.md, routes.md, telemetry.md.