Webhook Ingress
The webhook microservice exposes a provider-agnostic HTTP ingress. It verifies an API key, confirms that the target service is enabled and online, then publishes the JSON payload to NATS JetStream. The target microservice receives the event and remains responsible for validating its business meaning, authenticating the forwarded credential, and enforcing its own permissions.
- Package:
primebrick-webhook - Service code:
webhook - Runtime: Bun (
bun --hot src/index.tsin development;bun dist/index.jsin production) - Database:
webhookschema — owns only itsconfig_entriestable; API-key and service-registry lookups are delegated to the Backend over NATS
HTTP route
The custom route is:
Code
service_code identifies the registered target microservice. The optional intent is the remaining path, forwarded as a string; the webhook service does not interpret it. The request body must be valid JSON. An empty body is forwarded as null.
For example, a request to /webhook/emailsender/delivery/status with a JSON body is queued for service emailsender with intent delivery/status.
The JetStream message body has this shape:
Code
The original Authorization header is forwarded as a NATS header so the target service can authenticate the caller again. Other HTTP headers are not forwarded.
The ingress returns HTTP 200 after JetStream acknowledges the publish:
Code
The seq value is the JetStream publish sequence serialized as a string. A publish failure returns HTTP 502 so the external provider can retry.
Authentication and authorization
Every webhook request must include an API key in the Authorization header, using either ApiKey <key> or Bearer <key> when the key starts with pbk_. The Backend verifies the key hash and returns its active state, expiry, and permission metadata over NATS request/reply. The webhook service caches positive results for 60 seconds and negative results for 10 seconds (up to 1,000 entries).
The ingress precheck is not the target service's final authorization decision. The target receives the original Authorization value and must re-verify it and apply its own RBAC rules for the requested operation.
Target routing
The service keeps an in-memory cache from the Backend's lifecycle subjects:
service.registerandservice.heartbeatupdate the service's online state.service.unregisterremoves the cached service.service.stalemarks it stale.
A positive online cache entry is routable. Other states are checked against the Backend with the service.registry.get request/reply subject using { "code": "<service_code>" }. The registry row must be enabled and online. Unknown service codes or lookups that cannot resolve a registry row return HTTP 404; known but disabled or unavailable services return HTTP 502.
NATS delivery
At startup, the service ensures a JetStream stream named WEBHOOK with the subject filter webhook.>. For each accepted request, it publishes to:
Code
The stream's delivery consumers belong to the target microservices. This ingress does not call a target service's business endpoint directly and does not provide provider-specific signature validation.
HTTP errors
Errors use the RFC 7807-style JSON shape with type, title, status, detail, internal_code, and severity fields.
| Status | Condition |
|---|---|
| 400 | Request body is not valid JSON |
| 401 | Authorization header is missing, invalid, inactive, or expired |
| 404 | Target service code is unknown, or the registry lookup could not resolve a service row |
| 500 | API-key dependencies were not initialized |
| 502 | Target service is known but disabled/unavailable, the Backend lookup fails, or JetStream publish fails |
Configuration and deployment
The service uses createMicroservice. Following the env policy, only the
database bootstrap comes from env:
| Variable | Required | Default | Purpose |
|---|---|---|---|
DATABASE_URL | yes | — | PostgreSQL connection string (owns the webhook.config_entries table) |
DATABASE_SCHEMA | yes | — | Declared schema domain (webhook; CSV/JSON array for multi-schema — [0] is owned) |
Everything else lives in webhook.config_entries and is loaded by
ConfigLoader at startup: nats_url, service_code, http_port,
service_base_url (public URL advertised in the registry), and
service_client_shield_key.
For local development and build commands:
Code
The SDK exposes the public /health route and manages service registration, periodic heartbeats, NATS connection lifecycle, and graceful shutdown. The service owns the webhook PG schema (migrations under db-meta/patches/), so the health adapter checks the database via the DAL; NATS connectivity is monitored by the SDK.
The service's HTTP surface is declared once in src/controllers/http/router.ts via makeServiceRoutes — the same webhookRoutes table drives both the ingress router and the OpenAPI spec served at GET /api/v1/openapi.json (client-shielded; the BE fetches it during service discovery).