dev-overview/02-endpoint-io.md

6.8 KiB

Endpoint I/O

Each product NATS address: what it is for, who sends, who receives, how to attach.

Connect with a long-lived client (reconnect-per-message is the anti-pattern):

NATS_URL=nats://10.10.10.21:4222,nats://10.10.10.22:4222,nats://10.10.10.23:4222

Public edge (token in /etc/nats/public-edge.token on NS1): nats://<token>@70.88.205.138:4222. Do not put that URL in Zapier or in Verae.


verae.zapier.jobs.watch

For Work queue: “poll Verae until this jobId is terminal.”
In from middleware enqueueWatch after POST /api/timestamp (or mock) returns jobId
Out to job-poller worker (queue job-poller, durable on ZAPIER_JOBS)
Then poller calls Verae GET /api/status/{jobId} over HTTPS, not NATS

Payload: tenantId, veraeUserId, jobId, tokenRef, enqueuedAt, attempt, maxAttempts, intervalMs, traceId. JWT stays in middleware (tokenRef); never on Zapier.

Connect Verae-side: do not subscribe this from api.veraetime.net. The poller is the Verae HTTP client. To add another watcher, start another job-poller consumer in the same queue group.


verae.zapier.jobs.events

For Terminal outcomes: timestamp.completed / failed / timeout
In from job-poller after Verae status is not pending
Out to (1) middleware waitForJobEvent (wait HTTP); (2) webhook router → webhooks.deliver; (3) optional verae-jobs-events mailbox :3030

If JOBS_EVENTS_EXCLUSIVE=1, only verae-jobs-events is the durable consumer and republishes verae.internal.jobs.events. Default lab: middleware wait + webhook worker share the stream.

Payload: event, tenantId, jobId, status, traceId, emittedAt.


verae.zapier.webhooks.deliver

For HTTPS POST to hooks.zapier.com (REST Hook)
In from webhook worker on completed/failed events
Out to Zapier cloud HTTPS, never NATS

Payload: hookId, tenantId, targetUrl, event, payload, attempt, traceId. Slow consumer: keep replica floors on the worker if you split it out of middleware.


verae.zapier.usage

For Optional meter of Zap actions on NATS (JetStream)
In from designed: middleware / edge
Out to designed: USAGE_WRITER → books
Running stream exists; no writer; middleware uses local usage files

Prepaid meter that is wired uses verae.billing.usage.recorded (core pub), not this stream. Finish: either publish here from recordUsage or drop the stream.


Billing (verae.billing.*) — account-balance source of truth

Request-reply unless noted. Queue account-balance. Not JetStream.

Address For In from Out to
statement.get prepaid, credits, usage, payments portal, CS, sales, accounting, access-web/api reply to requester
balance.get cents only internal reply
balance.adjust CS credit or portal reload CS, access-web (reload kind only) books + reply
usage.recorded meter pub zappier-edge meter() onUsage when NATS_URL set account-balance subscriber (debits prepaid)
payment.recorded reload/invoice pub portal books
credit.applied goodwill pub CS/admin books
customer.put upsert customer edge ledger books

Authz must pass verae.access.authz.check before these from a public plane. Leaf plane cannot call billing (policy).

Middleware recordUsage still writes local tenant files and does not publish usage.recorded. Two meters until that is unified.


Access planes

Address For In Out
verae.access.authz.check allow/deny { plane, subject, principal } every plane + edge billing-nats.ts { allow, reason }; deny also pubs authz.deny
verae.access.authz.deny audit authz on deny nowhere yet (no SIEM)
verae.access.web.in / verae.access.web.billing.* browser portal access-web HTTP statement.get / balance.adjust kind=reload after authz
verae.access.api.* customer x-api-key access-api statement; HTTPS to middleware for jobs
verae.access.zapier.* Zapier HTTPS only access-zapier jobs.watch via HTTP to middleware, not Zapier NATS; usage.recorded allowed by policy
verae.access.leaf.in S2S { token, target, payload } remote leaf archive + jobs after authz; never billing
verae.access.staff.* CS/sales/accounting access-staff billing after authz

Ingress mapping: accessAddress(plane, 'verae.billing.statement.get') → verae.access.<plane>.billing.statement.get.


Identity

Address For In Out
verae.identity.bind bind email/customerId → veraeUserId portal / middleware reply
verae.identity.lookup lookup by customerId or veraeUserId same reply

Queue identity. Fleet keepFloor.


Archive / tree

Address For In Out
verae.archive.put WORM/tree write (kind: publicMeta / privateMeta / file / tree) splitter (in-process from middleware today) every worm/tree (JS)
verae.archive.query broadcast lookup aggregator all worm/tree, no queue group
verae.archive.reply.<correlationId> bloom hit only worm/tree aggregator

Silence = miss. Chain seal does not wait on archive. Bytes never go to the chain.


Template / unused

Address For In Out Running
verae.splitter.in future split-out splitter process middleware splitter no
verae.example.process.in template worker any example-process Q no
verae.example.process.out template fan-out template peers no
verae.example.process.reply.<id> template reply template requester no
verae.internal.jobs.events exclusive mailbox republish jobs-events wait/webhook no unless env

How to connect Verae API to NATS

api.veraetime.net is HTTPS. It does not subscribe. The glue is middleware:

  1. Set VERAE_API_BASE_URL=https://api.veraetime.net and MOCK_VERAE=false.
  2. Middleware login (username/password) → JWT stored as tokenRef.
  3. POST /api/timestamp → 202 { jobId }.
  4. Publish verae.zapier.jobs.watch.
  5. Poller GET /api/status/{jobId} until completed / failed.
  6. Publish verae.zapier.jobs.events.
  7. Wait HTTP and/or REST Hook.

Do not point Verae at NATS_URL. Do not put NATS in Zapier env. A new worker that must see seals uses jobs.events or HTTP GET /zapier/v1/status/{jobId}.

Template for a new addressed process: clone verae-nats-process, rename verae.example.process.*, add the subject to verae-access-authz policy for the plane that may call it.