Service Registry Contract
discovery specs/discovery/service-registry-contract.kmd
The runtime service-discovery contract of the Koder Cloud: how a service instance registers/renews/deregisters its endpoint, how the registry expires dead instances, how a consumer opts a service into edge routing, and the health semantics — the single source of truth that the PRODUCER (Koder DNS registry, dns#015) stores and the CONSUMER (Koder Jet, jet#027) reads. Ratified by infra-RFC-007 (Slice 3 = this contract). Replaces hand-edited endpoint maps (koder-service-ports.toml + jet sites.toml) with a registered source of truth.
Quando esta spec se aplica
Triggers primários
- Implement or change service discovery / endpoint registration in the Koder Cloud
Todos os triggers
- Register/expose a Koder service endpoint for runtime discovery
- Consume the service registry (route to / resolve a Koder backend)
- Change the ServiceInstance wire shape, the jet.domain opt-in, or TTL/health semantics
Corpo da especificação
Spec — Service Registry Contract
Status: v1.0.0. Formalizes the contract already implemented by the producer (Koder DNS,
dns#015) and the consumer (Koder Jet,jet#027); both predate this doc and agreed by convention — this spec makes that agreement explicit, versioned, and conformance-testable so the two ends cannot drift (the failure mode behind thevoice.koder.devincident in infra-RFC-007).
Roles
- Producer = the Koder DNS service registry (
infra/net/dns): stores instances, serves them (SRV + the REST API below), and reaps dead ones. The single source of truth for "where is service X right now?". - Registrant = any Koder service, which registers its own endpoint on (re)start (auto-register-on-deploy) and heart-beats while alive.
- Consumer = anything that routes to / resolves a backend (the Koder Jet
edge,
jet#027; future: other clients).
R1 — ServiceInstance (canonical wire shape)
A registered instance is a JSON object with these fields (the producer's
discovery.ServiceInstance):
| Field | JSON | Type | Notes |
|---|---|---|---|
| ID | id | string | Stable instance id; defaults to <name>-<address>-<port> when omitted |
| Name | name | string | Logical service name (required) |
| Address | address | string | Host/IP reachable by consumers (required) |
| Port | port | int | Required, non-zero |
| Protocol | protocol | string | default tcp |
| Tags | tags | string[] | key=value tags also accepted (see R5) |
| Metadata | metadata | map<string,string> | see R5 |
| Weight / Priority | weight/priority | int | SRV weighting; defaults 100 / 10 |
| Healthy | healthy | bool | R6 |
| TTL | ttl_ns | int64 (ns) | lease; 0 → registry default (R3) |
| RegisteredAt / UpdatedAt | registered_at/updated_at | RFC3339 | server-stamped |
R2 — Registration is an idempotent upsert
POST /api/v1/services/register with a ServiceInstance body. Registering an ID
that already exists refreshes its mutable fields and renews its lease (it does
NOT error). This is what makes auto-register-on-deploy safe to call on every
(re)start without leaking duplicate or stale entries. Response: {id, created}
(created=true only on first registration). name+address+port are required.
R3 — Lease, heartbeat, expiry
- Each instance has a TTL lease (its
ttl_ns, or the registry default — currently 90s — when unset/non-positive). A registry default of 0 means "never expire" (a static entry). POST /api/v1/services/heartbeat {id}renews the lease (bumpsupdated_at). Renewing an unknown/expired id returns 410 Gone — the registrant must re-register.- The registry reaps any instance whose
updated_at + effectiveTTLhas passed (reaper interval currently 30s). A service that dies stops heart-beating and disappears on its own — the registry never serves a dead endpoint.
R4 — Deregistration
POST /api/v1/services/deregister {id} removes the instance immediately. 404 if
unknown. Registrants SHOULD deregister on graceful shutdown (the reaper is the
backstop for ungraceful exits).
R5 — Edge-routing opt-in + topology metadata
- A service opts into edge routing by setting
jet.domain(inmetadata, or as ajet.domain=<host>tag) to its public vhost (e.g.voice.koder.dev). An instance withoutjet.domainis internal-only — registered + discoverable but not routed by the edge. (Analogue of Traefik'skoder.jet.enable.) metadata.container/metadata.service(orcontainer=tag) carry the topology labels used to GENERATEkoder-service-ports.toml(R8).
R6 — Health
healthy reflects the producer's health checks (infra/net/dns HealthChecker:
tcp/http). Consumers MUST honour it (route only to healthy instances for new
traffic); the flag is preserved end-to-end so a consumer can display/treat
unhealthy instances distinctly. GET /api/v1/services?healthy_only=… is the
producer's filter; SRV queries return healthy instances only.
R7 — REST API surface (authenticated, X-API-Key)
| Method + path | Purpose |
|---|---|
POST /api/v1/services/register | upsert an instance (R2) |
POST /api/v1/services/heartbeat | renew a lease (R3) |
POST /api/v1/services/deregister | remove an instance (R4) |
GET /api/v1/services | list service names; ?detail=1 → full instances |
GET /api/v1/services/export | render the registry as koder-service-ports.toml (R8) |
(SRV/A/TXT DNS resolution of the same registry is the producer's existing
service.dns domain — out of scope for this REST contract.)
R8 — Generated artifacts
koder-service-ports.toml (meta/docs/stack/registries/) becomes generated
from the registry (/services/export); the hand-maintained file is deprecated.
A drift check compares the live registry against a static reference and reports
missing / extra / owner_mismatch per port.
Conformance
- Producer —
infra/net/dns(dns#015): R1–R8. Tests:infra/net/dns/internal/discovery/registry_slice1_test.go(upsert/renew/expire/ default-TTL/export/drift) +internal/api/server_slice1_test.go(register→ heartbeat→export→deregister, 410 on expired). - Consumer —
infra/net/jet(jet#027): R1, R5, R6. Tests:infra/net/jet/internal/discovery/koder_registry_test.go(opt-in viajet.domainmetadata/tag; internal-only excluded; Healthy preserved; hot-reload).
Non-goals
- Sidecar/eBPF mesh data-plane (that's Koder Mesh's own scope; this contract is the registry, not per-hop proxying).
- Per-service identity / mTLS — see
stack-RFC-009(complementary, not here).
Referências
meta/docs/stack/rfcs/infra-RFC-007-service-discovery-and-edge-routing.kmdinfra/net/dns/internal/discovery/registry.goinfra/net/jet/internal/discovery/koder_registry.gometa/docs/stack/registries/koder-service-ports.toml