# Service Registration


`ServiceRegistrar` registers a microservice via NATS™ lifecycle events and
maintains a heartbeat. The BE subscribes to these events and persists the state
to the `service_registry` table. The microservice never touches the database
directly.

## Lifecycle events

<Mermaid chart={`sequenceDiagram
  participant MS as Microservice
  participant NATS as NATS
  participant BE as Backend
  MS->>NATS: service.register (code, base_url, endpoints, health)
  NATS->>BE: subscribe → persist to service_registry
  loop every 30s
    MS->>NATS: service.heartbeat (code, base_url, health)
    NATS->>BE: update last_health_check_at
  end
  MS->>NATS: service.unregister (code, base_url)
  NATS->>BE: mark service as offline
`} />

Three NATS™ subjects carry lifecycle events:

| Subject | When | Payload |
|---------|------|---------|
| `service.register` | On startup + on gateway restart | `ServiceRegisterPayload` — code, base_url, endpoints, health, metadata, capabilities |
| `service.heartbeat` | Every 30s (default) | `ServiceHeartbeatPayload` — code, base_url, health, metadata |
| `service.unregister` | On graceful shutdown | `ServiceUnregisterPayload` — code, base_url |
| `service.stale` | Watchdog-detected silence | `ServiceStalePayload` |
| `service.gateway_online` | BE startup (broadcast) | `{ at }` — every live service re-registers |

### Gateway restart re-registration

`service.register` is a request/reply handshake: the registrar retries every
5 s until the BE acks `{ registered: true }` — a missing/negative ack means
no heartbeats. After the first successful registration the service subscribes
to `service.gateway_online`: when the BE restarts it announces itself and
every live service **re-registers**, so the gateway re-ingests
identity/endpoints/capabilities and re-discovers OpenAPI without polling.

`service.gateway_online` is a broadcast — deliberately NOT a queue group —
because every running service instance must observe it.

These subjects are defined in the `SERVICE_SUBJECTS` constant.

## Usage

```ts
import { NatsClient, ServiceRegistrar } from "@primebrick/sdk";

const registrar = new ServiceRegistrar(NatsClient, {
  serviceCode: "emailsender",
  baseUrl: "http://emailsender:3001",
  endpoints: { "POST /send": {} },
  name: "Email Sender",
  description: "Sends emails via configured providers",
  author: "Primebrick",
  github_repo_url: "https://github.com/michaelsogos/primebrick-us-v3",
  service_version: "1.0.0",
  is_behind_scaler: true,
}, async () => ({
  http_healthy: true,
  checks: {
    nats: { ok: NatsClient.isConnected() },
    db: { ok: await dbPing() },
  },
}));

// Register on startup
await registrar.register();

// Start heartbeat loop (30s interval)
registrar.startHeartbeat();

// On graceful shutdown
await registrar.unregister();
registrar.stopHeartbeat();
```

## Nav declaration — validated at enrollment

A module declares its frontend surface in `endpoints.nav` (a
`ModuleNavDeclaration`): `route_prefixes`, `nav` links, and the `routes`
census. At `service.register` the BE runs `validateNavDeclaration(code,
decl, partOf)` and **rejects** the enrollment on any violation — the
declaration never reaches `service_registry`.

The enforced conventions:

| Rule | Contract |
|------|----------|
| Primary prefix | `route_prefixes[0]` must be `/{part_of}/{code}`; extra prefixes keep `code` as the URL's second segment |
| R1 — list plural | `kind:"list"` routes end in a plural leaf (`pluralize`) |
| R2 — nav leaf | a leaf nav `href` resolves to a declared route, or is itself plural |
| R3 — derived leaves | `kind:"create"` ends in `/create`; `kind:"detail"` ends in `[param]` |
| `access` | optional `access.{app}.{module}.{page}` gate — must prefix-match the module's own grant |

Route paths are **derived, never literals** — `entityPaths(prefix,
listLeaf, detailParam?)` is the single constructor of the entity-route
shape:

```ts
import { entityPaths, type ModuleNavDeclaration } from "@primebrick/sdk";

const customersPaths = entityPaths("/crm", "customers");
// → list   "/crm/customers"
// → create "/crm/customers/create"
// → detail "/crm/customers/[uuid]"  (detailParam → "[code]" etc.)

export const CRM_NAV: ModuleNavDeclaration = {
  route_prefixes: ["/crm"],
  nav: [{ id: "customers", label_key: "crm.customers.nav", href: customersPaths.list }],
  routes: [
    { route: customersPaths.list, kind: "list", entity: "customer" },
    { route: customersPaths.create, kind: "create", entity: "customer" },
  ],
};
```

Unit-test the declaration at write time — the same function the BE calls
at enrollment:

```ts
import { validateNavDeclaration } from "@primebrick/sdk";
expect(validateNavDeclaration("crm", CRM_NAV, "crm")).toBeNull();
```

## Health checks

The `healthCheckFn` (third constructor argument) is called on each heartbeat to
include the current health status. It returns `http_healthy` (boolean) and
`checks` (a record of named checks with `ok` and optional `error`).

If no `healthCheckFn` is provided, the registrar defaults to `http_healthy: true`
with empty checks.

## Configuration options

`ServiceRegistrarConfig` supports:

| Field | Required | Description |
|-------|----------|-------------|
| `serviceCode` | yes | Unique service identifier |
| `baseUrl` | yes | Service URL (used by BE for direct mode routing) |
| `endpoints` | yes | Map of endpoint signatures |
| `heartbeatIntervalMs` | no | Heartbeat interval (default: 30000) |
| `name` | no | Display name |
| `description` | no | Service description |
| `author` | no | Author name |
| `github_repo_url` | no | Repository URL |
| `service_version` | no | Semver string |
| `is_behind_scaler` | no | Whether the service is behind a scaler (default: false) |
| `icon` / `icon_type` | no | Icon for UI display |

## ServiceRegistryPort

The BE implements `ServiceRegistryPort` to persist lifecycle events. The port
interface supports both scaler mode (one row per code) and direct mode (multiple
rows per code, distinguished by `base_url`):

| Method | Mode | Description |
|--------|------|-------------|
| `findByCode(code)` | both | Find one row by code |
| `findByCodeAndBaseUrl(code, baseUrl)` | direct | Find by code + URL |
| `findAllByCode(code)` | direct | All rows for a code |
| `findAll()` | both | All rows |
| `insert(row)` | both | Insert new row |
| `updateByCode(code, row)` | scaler | Update by code |
| `updateByCodeAndBaseUrl(code, baseUrl, row)` | direct | Update by code + URL |
| `deleteByCodeAndBaseUrl(code, baseUrl)` | direct | Delete by code + URL |

## Next steps

- [NATS Client](nats-client) — the transport used by ServiceRegistrar
- [Authentication](authentication) — auth for NATS™ subscribers
- [API Reference](api-reference) — ServiceRegistrar and payload types
