dev-overview/README.html

2518 lines
80 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<meta name="generator" content="pandoc" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
<title>dev-overview — NATS routes and developer credentials</title>
<style>
/* Default styles provided by pandoc.
** See https://pandoc.org/MANUAL.html#variables-for-html for config info.
*/
html {
color: #1a1a1a;
background-color: #fdfdfd;
}
body {
margin: 0 auto;
max-width: 36em;
padding-left: 50px;
padding-right: 50px;
padding-top: 50px;
padding-bottom: 50px;
hyphens: auto;
overflow-wrap: break-word;
text-rendering: optimizeLegibility;
font-kerning: normal;
}
@media (max-width: 600px) {
body {
font-size: 0.9em;
padding: 12px;
}
h1 {
font-size: 1.8em;
}
}
@media print {
html {
background-color: white;
}
body {
background-color: transparent;
color: black;
font-size: 12pt;
}
p, h2, h3 {
orphans: 3;
widows: 3;
}
h2, h3, h4 {
page-break-after: avoid;
}
}
p {
margin: 1em 0;
}
a {
color: #1a1a1a;
}
a:visited {
color: #1a1a1a;
}
img {
max-width: 100%;
}
svg {
height: auto;
max-width: 100%;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 1.4em;
}
h5, h6 {
font-size: 1em;
font-style: italic;
}
h6 {
font-weight: normal;
}
ol, ul {
padding-left: 1.7em;
margin-top: 1em;
}
li > ol, li > ul {
margin-top: 0;
}
blockquote {
margin: 1em 0 1em 1.7em;
padding-left: 1em;
border-left: 2px solid #e6e6e6;
color: #606060;
}
code {
font-family: Menlo, Monaco, Consolas, 'Lucida Console', monospace;
font-size: 85%;
margin: 0;
hyphens: manual;
}
pre {
margin: 1em 0;
overflow: auto;
}
pre code {
padding: 0;
overflow: visible;
overflow-wrap: normal;
}
.sourceCode {
background-color: transparent;
overflow: visible;
}
hr {
border: none;
border-top: 1px solid #1a1a1a;
height: 1px;
margin: 1em 0;
}
table {
margin: 1em 0;
border-collapse: collapse;
width: 100%;
overflow-x: auto;
display: block;
font-variant-numeric: lining-nums tabular-nums;
}
table caption {
margin-bottom: 0.75em;
}
tbody {
margin-top: 0.5em;
border-top: 1px solid #1a1a1a;
border-bottom: 1px solid #1a1a1a;
}
th {
border-top: 1px solid #1a1a1a;
padding: 0.25em 0.5em 0.25em 0.5em;
}
td {
padding: 0.125em 0.5em 0.25em 0.5em;
}
header {
margin-bottom: 4em;
text-align: center;
}
#TOC li {
list-style: none;
}
#TOC ul {
padding-left: 1.3em;
}
#TOC > ul {
padding-left: 0;
}
#TOC a:not(:hover) {
text-decoration: none;
}
code{white-space: pre-wrap;}
span.smallcaps{font-variant: small-caps;}
div.columns{display: flex; gap: min(4vw, 1.5em);}
div.column{flex: auto; overflow-x: auto;}
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
/* The extra [class] is a hack that increases specificity enough to
override a similar rule in reveal.js */
ul.task-list[class]{list-style: none;}
ul.task-list li input[type="checkbox"] {
font-size: inherit;
width: 0.8em;
margin: 0 0.8em 0.2em -1.6em;
vertical-align: middle;
}
.display.math{display: block; text-align: center; margin: 0.5rem auto;}
/* CSS for syntax highlighting */
html { -webkit-text-size-adjust: 100%; }
pre > code.sourceCode { white-space: pre; position: relative; }
pre > code.sourceCode > span { display: inline-block; line-height: 1.25; }
pre > code.sourceCode > span:empty { height: 1.2em; }
.sourceCode { overflow: visible; }
code.sourceCode > span { color: inherit; text-decoration: inherit; }
div.sourceCode { margin: 1em 0; }
pre.sourceCode { margin: 0; }
@media screen {
div.sourceCode { overflow: auto; }
}
@media print {
pre > code.sourceCode { white-space: pre-wrap; }
pre > code.sourceCode > span { text-indent: -5em; padding-left: 5em; }
}
pre.numberSource code
{ counter-reset: source-line 0; }
pre.numberSource code > span
{ position: relative; left: -4em; counter-increment: source-line; }
pre.numberSource code > span > a:first-child::before
{ content: counter(source-line);
position: relative; left: -1em; text-align: right; vertical-align: baseline;
border: none; display: inline-block;
-webkit-touch-callout: none; -webkit-user-select: none;
-khtml-user-select: none; -moz-user-select: none;
-ms-user-select: none; user-select: none;
padding: 0 4px; width: 4em;
color: #aaaaaa;
}
pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa; padding-left: 4px; }
div.sourceCode
{ }
@media screen {
pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
}
code span.al { color: #ff0000; font-weight: bold; } /* Alert */
code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
code span.at { color: #7d9029; } /* Attribute */
code span.bn { color: #40a070; } /* BaseN */
code span.bu { color: #008000; } /* BuiltIn */
code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
code span.ch { color: #4070a0; } /* Char */
code span.cn { color: #880000; } /* Constant */
code span.co { color: #60a0b0; font-style: italic; } /* Comment */
code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
code span.do { color: #ba2121; font-style: italic; } /* Documentation */
code span.dt { color: #902000; } /* DataType */
code span.dv { color: #40a070; } /* DecVal */
code span.er { color: #ff0000; font-weight: bold; } /* Error */
code span.ex { } /* Extension */
code span.fl { color: #40a070; } /* Float */
code span.fu { color: #06287e; } /* Function */
code span.im { color: #008000; font-weight: bold; } /* Import */
code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
code span.kw { color: #007020; font-weight: bold; } /* Keyword */
code span.op { color: #666666; } /* Operator */
code span.ot { color: #007020; } /* Other */
code span.pp { color: #bc7a00; } /* Preprocessor */
code span.sc { color: #4070a0; } /* SpecialChar */
code span.ss { color: #bb6688; } /* SpecialString */
code span.st { color: #4070a0; } /* String */
code span.va { color: #19177c; } /* Variable */
code span.vs { color: #4070a0; } /* VerbatimString */
code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
</style>
</head>
<body>
<header id="title-block-header">
<h1 class="title">dev-overview — NATS routes and developer
credentials</h1>
</header>
<h1 id="dev-overview">dev-overview</h1>
<p>Developer map of the Verae Time × Zapier <strong>NATS message
fabric</strong>: what was designed, what is actually running on NS1, how
a Zapier timestamp job reaches <code>api.veraetime.net</code> (or the
mock) and comes back, and what is still unwired.</p>
<p><strong>Forgejo:</strong>
https://git.georgelambert.org/marchon/dev-overview<br />
<strong>SSH:</strong>
<code>ssh://git@git.georgelambert.org:2223/marchon/dev-overview.git</code><br />
<strong>Catalog:</strong>
https://zapier.georgelambert.org/packages/dev-overview/<br />
<strong>Date:</strong> 2026-09-14</p>
<p>Zapier cloud <strong>never</strong> connects to NATS. It talks HTTPS
to <code>https://zap.zapier.georgelambert.org</code> (access-zapier)
then middleware. <code>api.veraetime.net</code> is <strong>HTTPS
only</strong> — middleware is the NATS client.</p>
<h2 id="contents">Contents</h2>
<table>
<colgroup>
<col style="width: 45%" />
<col style="width: 54%" />
</colgroup>
<thead>
<tr>
<th>Doc</th>
<th>What</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="00-sources.md">00-sources.md</a></td>
<td>Every other doc that names a NATS address or TCP route</td>
</tr>
<tr>
<td><a href="01-nats-routes.md">01-nats-routes.md</a></td>
<td>Transport ports + every <code>verae.*</code> address: designed vs
running</td>
</tr>
<tr>
<td><a href="02-endpoint-io.md">02-endpoint-io.md</a></td>
<td>Per-address: purpose, inbound from, outbound to, how to connect
Verae API</td>
</tr>
<tr>
<td><a href="03-zapier-verae-path.md">03-zapier-verae-path.md</a></td>
<td>Zapier → middleware → Verae timestamp server → poll → events →
Zapier</td>
</tr>
<tr>
<td><a href="04-gaps-and-finish.md">04-gaps-and-finish.md</a></td>
<td>Modules not connected; billing/event/aux; ordered finish list</td>
</tr>
<tr>
<td><a
href="05-developer-credentials.md">05-developer-credentials.md</a></td>
<td>URLs, tokens, lab API keys, staff/IAM, git, env cheat sheet</td>
</tr>
</tbody>
</table>
<h2 id="cluster-running">Cluster (running)</h2>
<pre class="text"><code>nats://10.10.10.21:4222,nats://10.10.10.22:4222,nats://10.10.10.23:4222</code></pre>
<p>JetStream streams (replicas=3, file): <code>ZAPIER_JOBS</code>,
<code>ZAPIER_EVENTS</code>, <code>ZAPIER_WEBHOOKS</code>,
<code>ZAPIER_USAGE</code>, <code>VERAE_ARCHIVE</code>.</p>
<p>Public token-gated <strong>client</strong> edge (not cluster routes):
<code>nats://&lt;token&gt;@70.88.205.138:4222</code>,
<code>wss://nats.zapier.georgelambert.org</code>. Do not put this in
Zapier.</p>
<h1 id="sources-other-documentation-that-names-routes">Sources (other
documentation that names routes)</h1>
<p>This inventory is compiled from code and from these docs. Do not
invent extra <code>verae.*</code> names.</p>
<h2 id="master-maps">Master maps</h2>
<table>
<colgroup>
<col style="width: 27%" />
<col style="width: 72%" />
</colgroup>
<thead>
<tr>
<th>Path</th>
<th>What it names</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>packages/docs-master/modules-and-nats.md</code></td>
<td>Module catalog, subject table, HTTPS vs NATS</td>
</tr>
<tr>
<td><code>packages/docs-master/MESSAGE-FLOWS.md</code></td>
<td>Create/wait/includeAttached/batch Merkle</td>
</tr>
<tr>
<td><code>packages/docs-master/archive-nats.md</code></td>
<td>Archive put/query/reply</td>
</tr>
<tr>
<td><code>docs/02-architecture/nats-subjects.md</code></td>
<td><code>verae.zapier.*</code> payloads + streams</td>
</tr>
<tr>
<td><code>docs/02-architecture/modules-and-nats.md</code></td>
<td>Same catalog as docs-master</td>
</tr>
<tr>
<td><code>docs/02-architecture/overview.md</code></td>
<td>Poller / events / webhooks</td>
</tr>
<tr>
<td><code>packages/overview/03-nats-cluster.md</code></td>
<td>Cluster URLs, who may connect</td>
</tr>
<tr>
<td><code>packages/overview/06-address-routing.md</code></td>
<td>Address pattern + plane mapping</td>
</tr>
<tr>
<td><code>packages/verae-ops/06-linking-services.md</code></td>
<td>How services attach</td>
</tr>
<tr>
<td><code>packages/verae-nats-cluster/scripts/ensure-streams.sh</code></td>
<td>Live stream create</td>
</tr>
<tr>
<td><code>packages/verae-nats-cluster/conf/nats.conf.tmpl</code></td>
<td><code>:4222</code> / <code>:6222</code> / <code>:7422</code> /
monitor <code>:8222</code></td>
</tr>
<tr>
<td><code>packages/verae-nats-cluster/conf/public-edge.conf.tmpl</code></td>
<td>Public leaf <code>:4222</code>, WS <code>:14422</code></td>
</tr>
<tr>
<td><code>packages/verae-fleet/overlays/ns1/fleet.json</code></td>
<td>Enabled vs in-process workers</td>
</tr>
<tr>
<td><code>packages/verae-keep</code>
<code>units.px-worker.json</code></td>
<td>Worm/tree on LXC 510</td>
</tr>
<tr>
<td><code>packages/verae-zapier-middleware/src/nats/subjects.js</code></td>
<td>Product subjects + consumers</td>
</tr>
<tr>
<td><code>packages/zappier-account-balance/src/subjects.js</code></td>
<td>Billing subjects</td>
</tr>
<tr>
<td><code>packages/verae-access-authz/src/subjects.js</code> +
<code>policy.js</code></td>
<td>Authz + plane allow lists</td>
</tr>
<tr>
<td><code>packages/zappier/src/billing-nats.ts</code></td>
<td>Edge billing client</td>
</tr>
</tbody>
</table>
<h2
id="per-package-nats.md-forgejo-module-copies-also-live-under-docs-mastermodules">Per-package
<code>NATS.md</code> (Forgejo module copies also live under
<code>docs-master/modules/</code>)</h2>
<table>
<colgroup>
<col style="width: 45%" />
<col style="width: 55%" />
</colgroup>
<thead>
<tr>
<th>Package</th>
<th>Addresses</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>verae-zapier-middleware</code></td>
<td><code>jobs.watch</code> out, <code>jobs.events</code> in,
archive.query via aggregator</td>
</tr>
<tr>
<td><code>verae-jobs-events</code></td>
<td><code>jobs.events</code>; exclusive →
<code>verae.internal.jobs.events</code></td>
</tr>
<tr>
<td><code>verae-request-splitter</code></td>
<td><code>archive.put</code></td>
</tr>
<tr>
<td><code>verae-archive-worm</code></td>
<td><code>archive.put</code> / <code>query</code> /
<code>reply.&lt;id&gt;</code></td>
</tr>
<tr>
<td><code>verae-tree-node</code></td>
<td>same, <code>kind: tree</code></td>
</tr>
<tr>
<td><code>verae-archive-aggregator</code></td>
<td><code>query</code> out, <code>reply.&lt;id&gt;</code> in</td>
</tr>
<tr>
<td><code>zappier-account-balance</code></td>
<td><code>verae.billing.*</code></td>
</tr>
<tr>
<td><code>zappier</code> (edge)</td>
<td>package <code>NATS.md</code> is stale (“does not publish”);
<strong>code</strong> publishes usage/payment/credit/customer.put when
<code>NATS_URL</code> is set. Use
<code>docs-master/modules/zappier/NATS.md</code></td>
</tr>
<tr>
<td><code>zappier-customer-service</code></td>
<td><code>statement.get</code>, <code>balance.adjust</code></td>
</tr>
<tr>
<td><code>zappier-sales-pricing</code></td>
<td><code>statement.get</code></td>
</tr>
<tr>
<td><code>zappier-accounting-export</code></td>
<td><code>statement.get</code></td>
</tr>
<tr>
<td><code>zappier-identity</code></td>
<td><code>identity.bind</code> / <code>lookup</code></td>
</tr>
<tr>
<td><code>verae-access-authz</code></td>
<td><code>authz.check</code> / <code>authz.deny</code></td>
</tr>
<tr>
<td><code>verae-access-web</code></td>
<td><code>access.web.in</code> / <code>access.web.billing.*</code></td>
</tr>
<tr>
<td><code>verae-access-api</code></td>
<td><code>access.api.*</code></td>
</tr>
<tr>
<td><code>verae-access-zapier</code></td>
<td><code>access.zapier.*</code> then HTTPS to middleware</td>
</tr>
<tr>
<td><code>verae-access-leaf</code></td>
<td><code>access.leaf.in</code></td>
</tr>
<tr>
<td><code>verae-access-staff</code></td>
<td><code>access.staff.*</code></td>
</tr>
<tr>
<td><code>verae-nats-process</code></td>
<td><code>verae.example.process.*</code> (template)</td>
</tr>
<tr>
<td><code>verae-nats-accounts</code></td>
<td>policy only (INTERNAL vs LEAF)</td>
</tr>
<tr>
<td><code>verae-zapier-simulator</code></td>
<td>in-process replay of product addresses</td>
</tr>
<tr>
<td><code>verae-keep</code> / <code>verae-fleet</code> /
<code>verae-uptime</code> / <code>verae-backup</code></td>
<td>no product subjects</td>
</tr>
</tbody>
</table>
<p>Simulator <code>src/pipeline.js</code> and
<code>src/monitors.js</code> list the same product addresses for traces;
they are not live subscribers.</p>
<h1 id="designed-vs-running-nats-routes">Designed vs running NATS
routes</h1>
<p><strong>Designed</strong> = named in docs or in code.<br />
<strong>Running</strong> = a process on NS1 is subscribed or publishing
it <strong>today</strong>, and (for durable work) a JetStream stream
exists.</p>
<p>Cluster name <code>verae</code>. Internal clients use the three LXC
on <code>vmbr1</code>. Public edge is a <strong>leaf</strong>, not a
second cluster.</p>
<p>Legend: <strong>JS</strong> = JetStream stream; <strong>RR</strong> =
core request-reply; <strong>pub</strong> = core or JS fire-and-forget;
<strong>Q</strong> = queue group.</p>
<h2 id="transport-tcp-leaf-public">Transport (TCP / leaf / public)</h2>
<p>These are NATS <strong>server</strong> routes, not
<code>verae.*</code> subjects.</p>
<table>
<colgroup>
<col style="width: 29%" />
<col style="width: 29%" />
<col style="width: 26%" />
<col style="width: 14%" />
</colgroup>
<thead>
<tr>
<th>Endpoint</th>
<th>Designed</th>
<th>Running</th>
<th>Who</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>10.10.10.21–23:4222</code> client</td>
<td>yes</td>
<td><strong>yes</strong></td>
<td>middleware, access-*, billing, worm/tree, identity,
CS/sales/acct</td>
</tr>
<tr>
<td><code>10.10.10.21–23:6222</code> cluster routes</td>
<td>yes</td>
<td><strong>yes</strong> (private <code>vmbr1</code> only)</td>
<td>nats-a/b/c to each other</td>
</tr>
<tr>
<td><code>10.10.10.21–23:7422</code> leaf hub</td>
<td>yes (public-edge)</td>
<td><strong>yes</strong></td>
<td>public edge remotes</td>
</tr>
<tr>
<td><code>127.0.0.1:8222</code> monitor on each LXC</td>
<td>yes</td>
<td><strong>yes</strong></td>
<td>operators on the guest</td>
</tr>
<tr>
<td><code>70.88.205.138:4222</code> public client</td>
<td>yes (token)</td>
<td><strong>yes</strong></td>
<td>leaf edge; <strong>not</strong> Zapier</td>
</tr>
<tr>
<td><code>127.0.0.1:14422</code> WS on edge</td>
<td>yes</td>
<td><strong>yes</strong></td>
<td>Caddy → <code>wss://nats.zapier.georgelambert.org</code></td>
</tr>
<tr>
<td><code>127.0.0.1:8229</code> edge monitor</td>
<td>yes</td>
<td><strong>yes</strong></td>
<td>host loopback</td>
</tr>
<tr>
<td><code>:6222</code> / <code>:7422</code> / <code>:8222</code> on
<code>vmbr0</code></td>
<td><strong>no</strong></td>
<td><strong>must stay closed</strong></td>
<td>—</td>
</tr>
<tr>
<td>Host leftover <code>127.0.0.1:4222</code> (docker-proxy) +
<code>10.10.10.1:4222</code> socat</td>
<td>lab leftover</td>
<td>present</td>
<td><strong>do not</strong> point product <code>NATS_URL</code>
here</td>
</tr>
<tr>
<td>MQTT <code>:1883</code></td>
<td>study only</td>
<td><strong>removed</strong></td>
<td>—</td>
</tr>
</tbody>
</table>
<p>Internal keep/fleet/middleware URL:</p>
<pre class="text"><code>nats://10.10.10.21:4222,nats://10.10.10.22:4222,nats://10.10.10.23:4222</code></pre>
<p>Public (token in <code>/etc/nats/public-edge.token</code> on NS1):
<code>nats://&lt;token&gt;@70.88.205.138:4222</code>. Isolation on the
hub is still <code>$G</code> + private LAN. nkeys/mTLS sketched in
<code>verae-nats-accounts</code>, <strong>not</strong> flipped.</p>
<h2 id="jetstream-streams-running">JetStream streams (running)</h2>
<p>Created by <code>verae-nats-cluster/scripts/ensure-streams.sh</code>
(middleware <code>ensureStreams</code> also creates the first
three).</p>
<table>
<colgroup>
<col style="width: 15%" />
<col style="width: 19%" />
<col style="width: 19%" />
<col style="width: 17%" />
<col style="width: 27%" />
</colgroup>
<thead>
<tr>
<th>Stream</th>
<th>Subjects</th>
<th>Replicas</th>
<th>Storage</th>
<th>Who consumes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ZAPIER_JOBS</code></td>
<td><code>verae.zapier.jobs.watch</code></td>
<td>3</td>
<td>file</td>
<td>job-poller (in middleware process), Q <code>job-poller</code></td>
</tr>
<tr>
<td><code>ZAPIER_EVENTS</code></td>
<td><code>verae.zapier.jobs.events</code></td>
<td>3</td>
<td>file</td>
<td>middleware wait + webhook router
(<code>event-webhook-router</code>); optional exclusive
<code>jobs-events</code></td>
</tr>
<tr>
<td><code>ZAPIER_WEBHOOKS</code></td>
<td><code>verae.zapier.webhooks.deliver</code></td>
<td>3</td>
<td>file</td>
<td>webhook-deliver (in middleware), Q <code>webhook-deliver</code></td>
</tr>
<tr>
<td><code>ZAPIER_USAGE</code></td>
<td><code>verae.zapier.usage</code></td>
<td>3</td>
<td>file</td>
<td><strong>no durable consumer</strong> (<code>USAGE_WRITER</code>
named only)</td>
</tr>
<tr>
<td><code>VERAE_ARCHIVE</code></td>
<td><code>verae.archive.&gt;</code></td>
<td>3</td>
<td>file</td>
<td>worm + tree on LXC 510 (keep); aggregator <strong>not</strong>
fleet-enabled</td>
</tr>
</tbody>
</table>
<p>Billing is <strong>core request-reply / pub</strong>, not a JetStream
stream.</p>
<h2 id="address-inventory">Address inventory</h2>
<table>
<colgroup>
<col style="width: 21%" />
<col style="width: 14%" />
<col style="width: 24%" />
<col style="width: 21%" />
<col style="width: 17%" />
</colgroup>
<thead>
<tr>
<th>Address</th>
<th>Kind</th>
<th>Designed</th>
<th>Running</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>verae.zapier.jobs.watch</code></td>
<td>JS work Q <code>job-poller</code></td>
<td>yes</td>
<td><strong>yes</strong></td>
<td>middleware publishes; in-process poller consumes</td>
</tr>
<tr>
<td><code>verae.zapier.jobs.events</code></td>
<td>JS events</td>
<td>yes</td>
<td><strong>yes</strong></td>
<td>poller publishes; wait() subscribes; webhook worker routes</td>
</tr>
<tr>
<td><code>verae.zapier.webhooks.deliver</code></td>
<td>JS work Q <code>webhook-deliver</code></td>
<td>yes</td>
<td><strong>yes</strong></td>
<td>same Node process as HTTP</td>
</tr>
<tr>
<td><code>verae.zapier.usage</code></td>
<td>JS optional</td>
<td>yes</td>
<td>stream <strong>yes</strong>, consumer <strong>no</strong></td>
<td>middleware entitlement is <strong>local files</strong></td>
</tr>
<tr>
<td><code>verae.internal.jobs.events</code></td>
<td>core republish</td>
<td>yes (<code>jobs-events</code> exclusive)</td>
<td><strong>partial</strong></td>
<td>only if <code>JOBS_EVENTS_EXCLUSIVE=1</code></td>
</tr>
<tr>
<td><code>verae.billing.statement.get</code></td>
<td>RR Q <code>account-balance</code></td>
<td>yes</td>
<td><strong>yes</strong></td>
<td>portal, CS, sales, accounting, access-web/api</td>
</tr>
<tr>
<td><code>verae.billing.balance.get</code></td>
<td>RR</td>
<td>yes</td>
<td><strong>yes</strong></td>
<td>account-balance</td>
</tr>
<tr>
<td><code>verae.billing.balance.adjust</code></td>
<td>RR</td>
<td>yes</td>
<td><strong>yes</strong></td>
<td>CS credits, portal reload</td>
</tr>
<tr>
<td><code>verae.billing.usage.recorded</code></td>
<td>pub</td>
<td>yes</td>
<td><strong>yes if edge <code>NATS_URL</code> set</strong></td>
<td><code>zappier/src/app.ts</code> <code>onUsage</code> →
<code>natsPublish</code>; account-balance subscribes</td>
</tr>
<tr>
<td><code>verae.billing.payment.recorded</code></td>
<td>pub</td>
<td>yes</td>
<td><strong>yes if <code>NATS_URL</code></strong></td>
<td>portal reload (<code>portal.ts</code>)</td>
</tr>
<tr>
<td><code>verae.billing.credit.applied</code></td>
<td>pub</td>
<td>yes</td>
<td><strong>yes if <code>NATS_URL</code></strong></td>
<td>admin/CS (<code>admin.ts</code>)</td>
</tr>
<tr>
<td><code>verae.billing.customer.put</code></td>
<td>pub</td>
<td>yes</td>
<td><strong>yes if <code>NATS_URL</code></strong></td>
<td><code>ledger.ts</code>; account-balance applies</td>
</tr>
<tr>
<td><code>verae.access.authz.check</code></td>
<td>RR Q <code>access-authz</code></td>
<td>yes</td>
<td><strong>yes</strong></td>
<td>fleet keepFloor</td>
</tr>
<tr>
<td><code>verae.access.authz.deny</code></td>
<td>pub audit</td>
<td>yes</td>
<td><strong>yes</strong> (on deny)</td>
<td>no SIEM sink</td>
</tr>
<tr>
<td><code>verae.access.web.in</code> /
<code>verae.access.web.billing.*</code></td>
<td>ingress</td>
<td>yes</td>
<td><strong>yes</strong></td>
<td>mapped after authz,
e.g. <code>verae.access.web.billing.statement.get</code></td>
</tr>
<tr>
<td><code>verae.access.api.*</code></td>
<td>ingress</td>
<td>yes</td>
<td><strong>yes</strong></td>
<td></td>
</tr>
<tr>
<td><code>verae.access.zapier.*</code></td>
<td>ingress</td>
<td>yes</td>
<td><strong>yes</strong></td>
<td>HTTPS plane; Zapier never speaks NATS</td>
</tr>
<tr>
<td><code>verae.access.leaf.in</code> /
<code>verae.access.leaf.*</code></td>
<td>ingress</td>
<td>yes</td>
<td><strong>yes</strong></td>
<td>no billing</td>
</tr>
<tr>
<td><code>verae.access.staff.*</code></td>
<td>ingress</td>
<td>yes</td>
<td><strong>yes</strong></td>
<td>CS/sales/accounting</td>
</tr>
<tr>
<td><code>verae.identity.bind</code></td>
<td>RR Q <code>identity</code></td>
<td>yes</td>
<td><strong>yes</strong></td>
<td>fleet</td>
</tr>
<tr>
<td><code>verae.identity.lookup</code></td>
<td>RR Q <code>identity</code></td>
<td>yes</td>
<td><strong>yes</strong></td>
<td></td>
</tr>
<tr>
<td><code>verae.archive.put</code></td>
<td>JS (<code>VERAE_ARCHIVE</code>)</td>
<td>yes</td>
<td><strong>partial</strong></td>
<td>splitter <strong>in-process</strong> in middleware; worm/tree on 510
consume</td>
</tr>
<tr>
<td><code>verae.archive.query</code></td>
<td>broadcast (no Q)</td>
<td>yes</td>
<td><strong>partial</strong></td>
<td>live aggregator hook <strong>TBD</strong> in middleware (simulator
has it)</td>
</tr>
<tr>
<td><code>verae.archive.reply.&lt;id&gt;</code></td>
<td>replies on bloom hit</td>
<td>yes</td>
<td><strong>partial</strong></td>
<td>worm/tree on 510 can reply; aggregator not fleet-enabled</td>
</tr>
<tr>
<td><code>verae.splitter.in</code></td>
<td>future</td>
<td>yes (docs)</td>
<td><strong>no</strong></td>
<td>splitter stays a library</td>
</tr>
<tr>
<td><code>verae.example.process.in</code> / <code>.out</code> /
<code>.reply.*</code></td>
<td>template</td>
<td>yes</td>
<td><strong>no</strong></td>
<td><code>verae-nats-process</code> is a model</td>
</tr>
<tr>
<td><code>bench.*</code></td>
<td>lab only</td>
<td>n/a</td>
<td>ephemeral</td>
<td>speed studies; not product</td>
</tr>
</tbody>
</table>
<p>Plane mapping (<code>verae-access-authz</code>): internal
<code>verae.billing.statement.get</code> + plane <code>web</code> →
ingress <code>verae.access.web.billing.statement.get</code>. Planes
never publish internal subjects until <code>authz.check</code> returns
allow.</p>
<h2 id="durable-consumers-named-in-middleware">Durable consumers (named
in middleware)</h2>
<table>
<colgroup>
<col style="width: 18%" />
<col style="width: 54%" />
<col style="width: 27%" />
</colgroup>
<thead>
<tr>
<th>Name</th>
<th>Stream / subject</th>
<th>Running</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>job-poller</code></td>
<td><code>ZAPIER_JOBS</code> / <code>jobs.watch</code></td>
<td><strong>yes</strong> (in-process)</td>
</tr>
<tr>
<td><code>event-webhook-router</code></td>
<td><code>ZAPIER_EVENTS</code> / <code>jobs.events</code></td>
<td><strong>yes</strong> (in-process)</td>
</tr>
<tr>
<td><code>webhook-deliver</code></td>
<td><code>ZAPIER_WEBHOOKS</code> / <code>webhooks.deliver</code></td>
<td><strong>yes</strong> (in-process)</td>
</tr>
<tr>
<td><code>usage-writer</code></td>
<td><code>ZAPIER_USAGE</code> / <code>verae.zapier.usage</code></td>
<td><strong>no</strong></td>
</tr>
<tr>
<td><code>jobs-events-exclusive</code></td>
<td><code>ZAPIER_EVENTS</code></td>
<td><strong>no</strong> unless <code>JOBS_EVENTS_EXCLUSIVE=1</code></td>
</tr>
</tbody>
</table>
<h2 id="fleet-overlay-vs-in-process">Fleet overlay vs in-process</h2>
<p><code>packages/verae-fleet/overlays/ns1/fleet.json</code>
(2026-09-14):</p>
<table>
<colgroup>
<col style="width: 23%" />
<col style="width: 44%" />
<col style="width: 31%" />
</colgroup>
<thead>
<tr>
<th>Service</th>
<th>fleet <code>enabled</code></th>
<th>Actual lab</th>
</tr>
</thead>
<tbody>
<tr>
<td>middleware-http</td>
<td>true</td>
<td><strong>running</strong>; starts job-poller + webhook workers
in-process when <code>NATS_ENABLED</code></td>
</tr>
<tr>
<td>job-poller</td>
<td><strong>false</strong></td>
<td>covered by middleware; do not spawn a second copy</td>
</tr>
<tr>
<td>webhook-deliver</td>
<td><strong>false</strong></td>
<td>same</td>
</tr>
<tr>
<td>archive-aggregator</td>
<td><strong>false</strong></td>
<td><strong>not</strong> running as a fleet worker</td>
</tr>
<tr>
<td>archive-worm / tree-node</td>
<td><strong>false</strong> on host fleet</td>
<td><strong>running on LXC 510</strong> via keep
<code>units.px-worker.json</code> (1 worm + 1 tree)</td>
</tr>
<tr>
<td>account-balance, access-*, identity, jobs-events, CS/sales/acct,
staff-iam/session, zappier-edge</td>
<td>true keepFloor</td>
<td>keepFloor</td>
</tr>
<tr>
<td>zapier-simulator</td>
<td>true, keepFloor false</td>
<td>optional</td>
</tr>
<tr>
<td>nats</td>
<td>managed false</td>
<td>3-node cluster + leftover host loopback</td>
</tr>
</tbody>
</table>
<h1 id="endpoint-io">Endpoint I/O</h1>
<p>Each product NATS address: <strong>what it is for</strong>,
<strong>who sends</strong>, <strong>who receives</strong>, <strong>how
to attach</strong>.</p>
<p>Connect with a <strong>long-lived</strong> client
(reconnect-per-message is the anti-pattern):</p>
<pre class="text"><code>NATS_URL=nats://10.10.10.21:4222,nats://10.10.10.22:4222,nats://10.10.10.23:4222</code></pre>
<p>Public edge (token in <code>/etc/nats/public-edge.token</code> on
NS1): <code>nats://&lt;token&gt;@70.88.205.138:4222</code>. Do not put
that URL in Zapier or in Verae.</p>
<hr />
<h2
id="verae.zapier.jobs.watch"><code>verae.zapier.jobs.watch</code></h2>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr>
<td>For</td>
<td>Work queue: “poll Verae until this <code>jobId</code> is
terminal.”</td>
</tr>
<tr>
<td>In from</td>
<td>middleware <code>enqueueWatch</code> after
<code>POST /api/timestamp</code> (or mock) returns
<code>jobId</code></td>
</tr>
<tr>
<td>Out to</td>
<td>job-poller worker (queue <code>job-poller</code>, durable on
<code>ZAPIER_JOBS</code>)</td>
</tr>
<tr>
<td>Then</td>
<td>poller calls Verae <code>GET /api/status/{jobId}</code> over
<strong>HTTPS</strong>, not NATS</td>
</tr>
</tbody>
</table>
<p>Payload: <code>tenantId</code>, <code>veraeUserId</code>,
<code>jobId</code>, <code>tokenRef</code>, <code>enqueuedAt</code>,
<code>attempt</code>, <code>maxAttempts</code>, <code>intervalMs</code>,
<code>traceId</code>. JWT stays in middleware (<code>tokenRef</code>);
never on Zapier.</p>
<p><strong>Connect Verae-side:</strong> do <strong>not</strong>
subscribe this from <code>api.veraetime.net</code>. The poller is the
Verae HTTP client. To add another watcher, start another
<code>job-poller</code> consumer in the same queue group.</p>
<hr />
<h2
id="verae.zapier.jobs.events"><code>verae.zapier.jobs.events</code></h2>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr>
<td>For</td>
<td>Terminal outcomes: <code>timestamp.completed</code> /
<code>failed</code> / <code>timeout</code></td>
</tr>
<tr>
<td>In from</td>
<td>job-poller after Verae status is not <code>pending</code></td>
</tr>
<tr>
<td>Out to</td>
<td>(1) middleware <code>waitForJobEvent</code> (wait HTTP); (2) webhook
router → <code>webhooks.deliver</code>; (3) optional
<code>verae-jobs-events</code> mailbox <code>:3030</code></td>
</tr>
</tbody>
</table>
<p>If <code>JOBS_EVENTS_EXCLUSIVE=1</code>, only
<code>verae-jobs-events</code> is the durable consumer and republishes
<code>verae.internal.jobs.events</code>. Default lab: middleware wait +
webhook worker share the stream.</p>
<p>Payload: <code>event</code>, <code>tenantId</code>,
<code>jobId</code>, <code>status</code>, <code>traceId</code>,
<code>emittedAt</code>.</p>
<hr />
<h2
id="verae.zapier.webhooks.deliver"><code>verae.zapier.webhooks.deliver</code></h2>
<table>
<tbody>
<tr>
<td>For</td>
<td>HTTPS POST to <code>hooks.zapier.com</code> (REST Hook)</td>
</tr>
<tr>
<td>In from</td>
<td>webhook worker on completed/failed events</td>
</tr>
<tr>
<td>Out to</td>
<td>Zapier cloud <strong>HTTPS</strong>, never NATS</td>
</tr>
</tbody>
</table>
<p>Payload: <code>hookId</code>, <code>tenantId</code>,
<code>targetUrl</code>, <code>event</code>, <code>payload</code>,
<code>attempt</code>, <code>traceId</code>. Slow consumer: keep replica
floors on the worker if you split it out of middleware.</p>
<hr />
<h2 id="verae.zapier.usage"><code>verae.zapier.usage</code></h2>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr>
<td>For</td>
<td>Optional meter of Zap actions on NATS (JetStream)</td>
</tr>
<tr>
<td>In from</td>
<td>designed: middleware / edge</td>
</tr>
<tr>
<td>Out to</td>
<td>designed: <code>USAGE_WRITER</code> → books</td>
</tr>
<tr>
<td>Running</td>
<td><strong>stream exists; no writer; middleware uses local usage
files</strong></td>
</tr>
</tbody>
</table>
<p>Prepaid meter that <strong>is</strong> wired uses
<code>verae.billing.usage.recorded</code> (core pub), not this stream.
Finish: either publish here from <code>recordUsage</code> or drop the
stream.</p>
<hr />
<h2 id="billing-verae.billing.-account-balance-source-of-truth">Billing
(<code>verae.billing.*</code>) — account-balance source of truth</h2>
<p>Request-reply unless noted. Queue <code>account-balance</code>.
<strong>Not</strong> JetStream.</p>
<table>
<colgroup>
<col style="width: 29%" />
<col style="width: 16%" />
<col style="width: 29%" />
<col style="width: 25%" />
</colgroup>
<thead>
<tr>
<th>Address</th>
<th>For</th>
<th>In from</th>
<th>Out to</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>statement.get</code></td>
<td>prepaid, credits, usage, payments</td>
<td>portal, CS, sales, accounting, access-web/api</td>
<td>reply to requester</td>
</tr>
<tr>
<td><code>balance.get</code></td>
<td>cents only</td>
<td>internal</td>
<td>reply</td>
</tr>
<tr>
<td><code>balance.adjust</code></td>
<td>CS credit or portal reload</td>
<td>CS, access-web (reload kind only)</td>
<td>books + reply</td>
</tr>
<tr>
<td><code>usage.recorded</code></td>
<td>meter pub</td>
<td>zappier-edge <code>meter()</code> <code>onUsage</code> when
<code>NATS_URL</code> set</td>
<td>account-balance subscriber (debits prepaid)</td>
</tr>
<tr>
<td><code>payment.recorded</code></td>
<td>reload/invoice pub</td>
<td>portal</td>
<td>books</td>
</tr>
<tr>
<td><code>credit.applied</code></td>
<td>goodwill pub</td>
<td>CS/admin</td>
<td>books</td>
</tr>
<tr>
<td><code>customer.put</code></td>
<td>upsert customer</td>
<td>edge ledger</td>
<td>books</td>
</tr>
</tbody>
</table>
<p>Authz <strong>must</strong> pass
<code>verae.access.authz.check</code> before these from a public plane.
Leaf plane <strong>cannot</strong> call billing (policy).</p>
<p>Middleware <code>recordUsage</code> still writes <strong>local tenant
files</strong> and does <strong>not</strong> publish
<code>usage.recorded</code>. Two meters until that is unified.</p>
<hr />
<h2 id="access-planes">Access planes</h2>
<table>
<colgroup>
<col style="width: 39%" />
<col style="width: 21%" />
<col style="width: 17%" />
<col style="width: 21%" />
</colgroup>
<thead>
<tr>
<th>Address</th>
<th>For</th>
<th>In</th>
<th>Out</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>verae.access.authz.check</code></td>
<td>allow/deny <code>{ plane, subject, principal }</code></td>
<td>every plane + edge <code>billing-nats.ts</code></td>
<td><code>{ allow, reason }</code>; deny also pubs
<code>authz.deny</code></td>
</tr>
<tr>
<td><code>verae.access.authz.deny</code></td>
<td>audit</td>
<td>authz on deny</td>
<td><strong>nowhere yet</strong> (no SIEM)</td>
</tr>
<tr>
<td><code>verae.access.web.in</code> /
<code>verae.access.web.billing.*</code></td>
<td>browser portal</td>
<td>access-web HTTP</td>
<td><code>statement.get</code> / <code>balance.adjust</code> kind=reload
after authz</td>
</tr>
<tr>
<td><code>verae.access.api.*</code></td>
<td>customer <code>x-api-key</code></td>
<td>access-api</td>
<td>statement; HTTPS to middleware for jobs</td>
</tr>
<tr>
<td><code>verae.access.zapier.*</code></td>
<td>Zapier HTTPS only</td>
<td>access-zapier</td>
<td>jobs.watch <strong>via HTTP to middleware</strong>, not Zapier NATS;
usage.recorded allowed by policy</td>
</tr>
<tr>
<td><code>verae.access.leaf.in</code></td>
<td>S2S <code>{ token, target, payload }</code></td>
<td>remote leaf</td>
<td>archive + jobs after authz; <strong>never billing</strong></td>
</tr>
<tr>
<td><code>verae.access.staff.*</code></td>
<td>CS/sales/accounting</td>
<td>access-staff</td>
<td>billing after authz</td>
</tr>
</tbody>
</table>
<p>Ingress mapping:
<code>accessAddress(plane, 'verae.billing.statement.get')</code> →
<code>verae.access.&lt;plane&gt;.billing.statement.get</code>.</p>
<hr />
<h2 id="identity">Identity</h2>
<table>
<colgroup>
<col style="width: 39%" />
<col style="width: 21%" />
<col style="width: 17%" />
<col style="width: 21%" />
</colgroup>
<thead>
<tr>
<th>Address</th>
<th>For</th>
<th>In</th>
<th>Out</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>verae.identity.bind</code></td>
<td>bind email/customerId → <code>veraeUserId</code></td>
<td>portal / middleware</td>
<td>reply</td>
</tr>
<tr>
<td><code>verae.identity.lookup</code></td>
<td>lookup by customerId or veraeUserId</td>
<td>same</td>
<td>reply</td>
</tr>
</tbody>
</table>
<p>Queue <code>identity</code>. Fleet keepFloor.</p>
<hr />
<h2 id="archive-tree">Archive / tree</h2>
<table>
<colgroup>
<col style="width: 39%" />
<col style="width: 21%" />
<col style="width: 17%" />
<col style="width: 21%" />
</colgroup>
<thead>
<tr>
<th>Address</th>
<th>For</th>
<th>In</th>
<th>Out</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>verae.archive.put</code></td>
<td>WORM/tree write (<code>kind</code>: publicMeta / privateMeta / file
/ tree)</td>
<td>splitter (in-process from middleware today)</td>
<td>every worm/tree (JS)</td>
</tr>
<tr>
<td><code>verae.archive.query</code></td>
<td>broadcast lookup</td>
<td>aggregator</td>
<td><strong>all</strong> worm/tree, <strong>no</strong> queue group</td>
</tr>
<tr>
<td><code>verae.archive.reply.&lt;correlationId&gt;</code></td>
<td>bloom <strong>hit only</strong></td>
<td>worm/tree</td>
<td>aggregator</td>
</tr>
</tbody>
</table>
<p>Silence = miss. Chain seal does not wait on archive. Bytes never go
to the chain.</p>
<hr />
<h2 id="template-unused">Template / unused</h2>
<table>
<colgroup>
<col style="width: 28%" />
<col style="width: 15%" />
<col style="width: 12%" />
<col style="width: 15%" />
<col style="width: 28%" />
</colgroup>
<thead>
<tr>
<th>Address</th>
<th>For</th>
<th>In</th>
<th>Out</th>
<th>Running</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>verae.splitter.in</code></td>
<td>future split-out splitter process</td>
<td>middleware</td>
<td>splitter</td>
<td><strong>no</strong></td>
</tr>
<tr>
<td><code>verae.example.process.in</code></td>
<td>template worker</td>
<td>any</td>
<td><code>example-process</code> Q</td>
<td><strong>no</strong></td>
</tr>
<tr>
<td><code>verae.example.process.out</code></td>
<td>template fan-out</td>
<td>template</td>
<td>peers</td>
<td><strong>no</strong></td>
</tr>
<tr>
<td><code>verae.example.process.reply.&lt;id&gt;</code></td>
<td>template reply</td>
<td>template</td>
<td>requester</td>
<td><strong>no</strong></td>
</tr>
<tr>
<td><code>verae.internal.jobs.events</code></td>
<td>exclusive mailbox republish</td>
<td>jobs-events</td>
<td>wait/webhook</td>
<td><strong>no</strong> unless env</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="how-to-connect-verae-api-to-nats">How to connect <strong>Verae
API</strong> to NATS</h2>
<p><code>api.veraetime.net</code> is <strong>HTTPS</strong>. It does not
subscribe. The glue is middleware:</p>
<ol type="1">
<li>Set <code>VERAE_API_BASE_URL=https://api.veraetime.net</code> and
<code>MOCK_VERAE=false</code>.</li>
<li>Middleware login (username/password) → JWT stored as
<code>tokenRef</code>.</li>
<li><code>POST /api/timestamp</code> → 202 <code>{ jobId }</code>.</li>
<li>Publish <code>verae.zapier.jobs.watch</code>.</li>
<li>Poller <code>GET /api/status/{jobId}</code> until
<code>completed</code> / <code>failed</code>.</li>
<li>Publish <code>verae.zapier.jobs.events</code>.</li>
<li>Wait HTTP and/or REST Hook.</li>
</ol>
<p>Do <strong>not</strong> point Verae at <code>NATS_URL</code>. Do
<strong>not</strong> put NATS in Zapier env. A new worker that must see
seals uses <code>jobs.events</code> or HTTP
<code>GET /zapier/v1/status/{jobId}</code>.</p>
<p>Template for a new addressed process: clone
<code>verae-nats-process</code>, rename
<code>verae.example.process.*</code>, add the subject to
<code>verae-access-authz</code> policy for the plane that may call
it.</p>
<h1 id="zapier-verae-api-zapier">Zapier → Verae API → Zapier</h1>
<p>How a timestamp job is submitted, watched, and returned. Lab today:
<code>MOCK_VERAE=true</code>. Live path is the same HTTP contract with
<code>MOCK_VERAE=false</code>.</p>
<h2 id="rule">Rule</h2>
<pre class="text"><code>Zapier cloud --HTTPS only--&gt; zap.zapier.georgelambert.org (access-zapier)
|
v HTTPS
middleware :3100 /zapier/v1
|
+---------------+----------------+
| HTTPS | NATS (private cluster)
v v
api.veraetime.net jobs.watch → poller
POST /api/timestamp GET /api/status/{jobId}
GET /api/status/{jobId} |
POST /api/verify v
jobs.events
|
+--------------------+--------------------+
| wait subscriber | webhook-deliver
v v
HTTP response to Zap POST hooks.zapier.com</code></pre>
<p>Zapier never has <code>NATS_URL</code>. Verae never has
<code>NATS_URL</code>.</p>
<h2 id="submit-async-create">1. Submit (async create)</h2>
<ol type="1">
<li>Zap action <strong>Create Timestamp</strong> (or curl)
<code>POST https://zap.zapier.georgelambert.org/zapier/v1/timestamp</code>
with portal API key <code>zmw_…</code> (not a Verae JWT).</li>
<li>access-zapier / edge: authz, then <code>meter()</code> records
cents; if <code>NATS_URL</code> is set,
<code>natsPublish(verae.billing.usage.recorded)</code>.</li>
<li>Forwards to middleware <code>POST /zapier/v1/timestamp</code>.</li>
<li>Middleware <code>checkEntitlement</code> (local plan limits
today).</li>
<li><code>veraeClient.createTimestamp</code>:
<ul>
<li><strong>Mock:</strong> in-process job, fake <code>jobId</code>.</li>
<li><strong>Live:</strong>
<code>POST https://api.veraetime.net/api/timestamp</code> with JWT →
<strong>202</strong> <code>{ jobId }</code>.</li>
</ul></li>
<li>Splitter (in-process): hash vs attachments; optional
<code>verae.archive.put</code> (bytes never go to chain).</li>
<li><code>enqueueWatch</code> → JetStream
<strong><code>verae.zapier.jobs.watch</code></strong>.</li>
<li>HTTP <strong>202</strong> <code>{ jobId, sha256, existing }</code>
back to Zapier immediately.</li>
</ol>
<p>Same SHA-256 is idempotent (<code>existing: true</code>).</p>
<h2 id="monitor-job-poller">2. Monitor (job poller)</h2>
<p>The poller is <strong>in the middleware Node process</strong> when
<code>NATS_ENABLED=true</code> (<code>startJobPollerWorker</code>).
Fleet does <strong>not</strong> spawn a second copy
(<code>job-poller.enabled: false</code>).</p>
<ol type="1">
<li>Durable consumer <code>job-poller</code> on <code>ZAPIER_JOBS</code>
/ <code>verae.zapier.jobs.watch</code>.</li>
<li>Resolve <code>tokenRef</code> → tenant Verae password →
<code>POST</code> login → JWT (stays in middleware).</li>
<li><code>GET /api/status/{jobId}</code> (mock or live timestamp
server).</li>
<li><code>pending</code> → <code>nak</code> after
<code>intervalMs</code> (retry).</li>
<li><code>completed</code> → publish <code>jobs.events</code>
<code>{ event: timestamp.completed, status, jobId, tenantId, traceId }</code>.</li>
<li><code>failed</code> → <code>timestamp.failed</code>. Too many
attempts → <code>timestamp.timeout</code>.</li>
<li><code>ack</code> the watch message.</li>
</ol>
<p>Live OpenAPI: poll until <code>completed</code> /
<code>failed</code>. Hash lookup is <strong>501</strong> on live
(<code>lookupHash</code>). Map <code>certificate</code>,
<code>timestamp</code>, <code>blockIndex</code>, <code>sha256</code>
into the receipt Zap samples expect.</p>
<h2 id="wait-sync-zap-step">3. Wait (sync Zap step)</h2>
<p><strong>Create Timestamp and Wait</strong> →
<code>POST /zapier/v1/timestamp/wait</code>.</p>
<ol type="1">
<li>Same as submit (watch is already on NATS).</li>
<li><code>waitForJobEvent(jobId, WAIT_TIMEOUT_MS)</code> subscribes to
<strong><code>verae.zapier.jobs.events</code></strong>.</li>
<li>On matching <code>jobId</code> event: return status JSON
(certificate / receipt).</li>
<li>On timeout: <code>{ status: pending, jobId }</code> so the REST Hook
can finish the Zap.</li>
</ol>
<p><code>includeAttached</code>: after seal, aggregator should
<code>verae.archive.query</code>; bloom hits merge extra receipts.
<strong>Simulator does this; live aggregator hook is TBD.</strong></p>
<h2 id="push-back-to-zapier">4. Push back to Zapier</h2>
<ul>
<li><strong>Wait response:</strong> HTTP body on the same Zap step.</li>
<li><strong>REST Hook:</strong> Zapier subscribes
<code>POST /zapier/v1/hooks</code>. On <code>jobs.events</code>,
middleware enqueues
<strong><code>verae.zapier.webhooks.deliver</code></strong>. Worker
POSTs <code>targetUrl</code> (<code>hooks.zapier.com</code>).</li>
<li><strong>Poll status:</strong> Zap action <strong>Find Job
Status</strong> → <code>GET /zapier/v1/status/{jobId}</code> →
middleware <code>GET</code> Verae (or last event).</li>
</ul>
<h2 id="verify">5. Verify</h2>
<p><code>POST /zapier/v1/verify</code> → middleware → Verae
<code>POST /api/verify</code>. No job watch.</p>
<h2 id="batch-merkle-designed-same-bus">6. Batch Merkle (designed; same
bus)</h2>
<p>From <code>MESSAGE-FLOWS.md</code>:</p>
<ol type="1">
<li><code>POST /v1/timestamp/batch</code> — one item per line.</li>
<li>Middleware hashes leaves, builds Merkle tree.</li>
<li>Chain seals <strong>only the root</strong>
(<code>POST /api/timestamp</code> on the root).</li>
<li>Each leaf proof → <code>verae.archive.put</code>
<code>kind: tree</code>.</li>
<li>Central hash lookup of a leaf misses unless
<code>includeTree=true</code>.</li>
<li>Aggregator broadcasts <code>verae.archive.query</code>
<code>kinds=["tree"]</code>.</li>
<li>Tree node bloom hit → <code>archive.reply.&lt;id&gt;</code> with
proof.</li>
</ol>
<p>Live wait path does not yet call the aggregator, so tree-leaf
receipts are not merged on NS1.</p>
<h2 id="connect-live-api.veraetime.net">Connect live
<code>api.veraetime.net</code></h2>
<p>Still blocked on operator bind credentials. When ready:</p>
<div class="sourceCode" id="cb5"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="co"># middleware env</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="va">MOCK_VERAE</span><span class="op">=</span>false</span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="va">VERAE_API_BASE_URL</span><span class="op">=</span>https://api.veraetime.net</span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="va">NATS_ENABLED</span><span class="op">=</span>true</span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a><span class="va">NATS_URL</span><span class="op">=</span>nats://10.10.10.21:4222,nats://10.10.10.22:4222,nats://10.10.10.23:4222</span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a><span class="va">JETSTREAM_REPLICAS</span><span class="op">=</span>3</span></code></pre></div>
<p>Then: working Verae login, one live seal, confirm 202/status/verify,
map receipt fields, REST Hook to a private Zap. See
<code>zapier-decisions/STATUS.md</code> “Once MOCK_VERAE=false”.</p>
<h2 id="billing-on-this-path">Billing on this path</h2>
<table>
<colgroup>
<col style="width: 20%" />
<col style="width: 41%" />
<col style="width: 37%" />
</colgroup>
<thead>
<tr>
<th>Hop</th>
<th>Designed</th>
<th>Running</th>
</tr>
</thead>
<tbody>
<tr>
<td>Edge meter</td>
<td><code>verae.billing.usage.recorded</code></td>
<td><strong>coded</strong>; fires when edge has
<code>NATS_URL</code></td>
</tr>
<tr>
<td>Account-balance</td>
<td>subscribe + debit prepaid; 402 if empty</td>
<td><strong>subscriber running</strong>; 402 from books not proven on
every Zap</td>
</tr>
<tr>
<td>Middleware quota</td>
<td>local files via <code>recordUsage</code></td>
<td><strong>running</strong>; not NATS</td>
</tr>
<tr>
<td><code>verae.zapier.usage</code></td>
<td>optional JS meter</td>
<td>stream only</td>
</tr>
</tbody>
</table>
<h2 id="connect-a-new-internal-process">Connect a new internal
process</h2>
<div class="sourceCode" id="cb6"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="bu">export</span> <span class="va">NATS_URL</span><span class="op">=</span>nats://10.10.10.21:4222,nats://10.10.10.22:4222,nats://10.10.10.23:4222</span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a><span class="co"># clone verae-nats-process, rename subjects, register in verae-fleet</span></span></code></pre></div>
<p>Jobs stay: HTTP in from Zapier → HTTPS to Verae → NATS watch/events →
HTTP back to Zapier.</p>
<h1 id="gaps-and-finish-list">Gaps and finish list</h1>
<p>Modules and addresses that are designed but <strong>not fully
connected, not running as fleet workers, or not on the live Verae/Zapier
path</strong>.</p>
<h2 id="blocked-on-the-operator-not-a-nats-bug">Blocked on the operator
(not a NATS bug)</h2>
<table>
<colgroup>
<col style="width: 24%" />
<col style="width: 32%" />
<col style="width: 44%" />
</colgroup>
<thead>
<tr>
<th>Item</th>
<th>State</th>
<th>To finish</th>
</tr>
</thead>
<tbody>
<tr>
<td>Zapier Platform <code>push</code></td>
<td>no login</td>
<td><code>zapier login</code> → register →
<code>MIDDLEWARE_BASE_URL=https://zap.zapier.georgelambert.org</code> →
push private app</td>
</tr>
<tr>
<td>Live Verae</td>
<td><code>MOCK_VERAE=true</code></td>
<td>credentials; <code>MOCK_VERAE=false</code>; prove one seal</td>
</tr>
<tr>
<td>nkeys / mTLS</td>
<td>cluster <code>$G</code> only</td>
<td><code>verae-nats-accounts</code> sketch; flip only after every
<code>NATS_URL</code> has creds</td>
</tr>
<tr>
<td>Hash lookup on live OpenAPI</td>
<td>501</td>
<td>Verae API gap or our store</td>
</tr>
</tbody>
</table>
<h2 id="running-on-httpnats-but-incomplete-routing">Running on HTTP/NATS
but incomplete routing</h2>
<table>
<colgroup>
<col style="width: 27%" />
<col style="width: 34%" />
<col style="width: 20%" />
<col style="width: 17%" />
</colgroup>
<thead>
<tr>
<th>Module</th>
<th>Running?</th>
<th>NATS</th>
<th>Gap</th>
</tr>
</thead>
<tbody>
<tr>
<td>middleware HTTP + in-process job-poller + webhook-deliver</td>
<td><strong>yes</strong></td>
<td>watch/events/deliver</td>
<td>live Verae + Zapier push</td>
</tr>
<tr>
<td>account-balance</td>
<td><strong>yes</strong></td>
<td>billing RR + <code>usage.recorded</code> sub</td>
<td>prove 402 from books on a Zap when prepaid is 0</td>
</tr>
<tr>
<td>zappier-edge</td>
<td><strong>yes</strong></td>
<td><code>usage.recorded</code> / <code>payment.recorded</code> /
<code>credit.applied</code> / <code>customer.put</code> <strong>if
<code>NATS_URL</code></strong></td>
<td>confirm live process env; Stripe live</td>
</tr>
<tr>
<td>access-* + authz</td>
<td><strong>yes</strong></td>
<td>authz + ingress</td>
<td>—</td>
</tr>
<tr>
<td>identity</td>
<td><strong>yes</strong> (fleet)</td>
<td>bind/lookup</td>
<td>confirm portal always binds <code>veraeUserId</code></td>
</tr>
<tr>
<td>jobs-events mailbox <code>:3030</code></td>
<td><strong>yes</strong> (fleet)</td>
<td>events counter</td>
<td>exclusive consumer off unless
<code>JOBS_EVENTS_EXCLUSIVE=1</code></td>
</tr>
<tr>
<td>CS / sales / accounting</td>
<td><strong>yes</strong></td>
<td>statement/adjust</td>
<td>staff UX</td>
</tr>
<tr>
<td>archive-worm + tree-node</td>
<td><strong>yes on LXC 510</strong> (keep)</td>
<td><code>archive.put/query/reply</code></td>
<td>only <strong>1+1</strong> copies; fleet overlay
<code>enabled: false</code> for extra replicas</td>
</tr>
<tr>
<td>archive-aggregator</td>
<td><strong>no</strong> as fleet worker</td>
<td>query/reply</td>
<td>enable overlay <strong>or</strong> finish in-process hook
(<code>includeAttached</code> TBD in middleware)</td>
</tr>
<tr>
<td>request-splitter</td>
<td>library in middleware</td>
<td><code>archive.put</code></td>
<td><code>verae.splitter.in</code> not used</td>
</tr>
<tr>
<td><code>ZAPIER_USAGE</code> / <code>USAGE_WRITER</code></td>
<td>stream yes</td>
<td><code>verae.zapier.usage</code></td>
<td>no consumer; middleware usage is local files</td>
</tr>
<tr>
<td>fleet job-poller / webhook-deliver <strong>extra</strong>
copies</td>
<td>overlay <strong>false</strong></td>
<td>same subjects</td>
<td>only enable if you want SSH-spread replicas <strong>instead
of</strong> in-process (same queue names)</td>
</tr>
</tbody>
</table>
<h2 id="designed-not-a-live-worker">Designed, not a live worker</h2>
<table>
<colgroup>
<col style="width: 27%" />
<col style="width: 31%" />
<col style="width: 41%" />
</colgroup>
<thead>
<tr>
<th>Module</th>
<th>Address</th>
<th>What to do</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>verae-nats-process</code></td>
<td><code>verae.example.process.*</code></td>
<td>template only; clone to add a function</td>
</tr>
<tr>
<td><code>verae-activate</code></td>
<td>none (HTTPS Zapier)</td>
<td>optional tiny app; needs Zapier login</td>
</tr>
<tr>
<td><code>verae-zapier</code> (full app)</td>
<td>HTTPS</td>
<td>push after login</td>
</tr>
<tr>
<td>simulator</td>
<td>in-process fake NATS</td>
<td>local <code>:3847</code>; not production</td>
</tr>
<tr>
<td><code>verae-nats-accounts</code></td>
<td>nkeys policy</td>
<td>not flipped</td>
</tr>
</tbody>
</table>
<h2 id="aux-no-product-subjects-or-http-only">Aux (no product subjects,
or HTTP only)</h2>
<table>
<colgroup>
<col style="width: 38%" />
<col style="width: 28%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr>
<th>Module</th>
<th>NATS</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>verae-keep</code></td>
<td>none</td>
<td>HTTP health; restarts units</td>
</tr>
<tr>
<td><code>verae-fleet</code></td>
<td>none</td>
<td>catalog + keepFloor</td>
</tr>
<tr>
<td><code>verae-uptime</code></td>
<td>none</td>
<td>off-box HTTPS GET of public doors</td>
</tr>
<tr>
<td><code>verae-backup</code></td>
<td>none</td>
<td>disk / JetStream snapshot; no live event subject</td>
</tr>
<tr>
<td><code>verae-bootstrap</code> / <code>verae-deploy</code> /
<code>verae-proxmox</code> / <code>verae-ops</code></td>
<td>none</td>
<td>install/link</td>
</tr>
<tr>
<td>staff-iam / staff-session / staff-ui</td>
<td>none</td>
<td>HTTP cookies</td>
</tr>
<tr>
<td>public NATS edge</td>
<td>leaf, token</td>
<td><strong>do not</strong> use from Zapier; rotate token; later nkeys
LEAF vs INTERNAL</td>
</tr>
</tbody>
</table>
<h2 id="billing-triggers-and-event-logging">Billing triggers and event
logging</h2>
<table style="width:100%;">
<colgroup>
<col style="width: 22%" />
<col style="width: 28%" />
<col style="width: 25%" />
<col style="width: 22%" />
</colgroup>
<thead>
<tr>
<th>Signal</th>
<th>Designed</th>
<th>Running</th>
<th>Finish</th>
</tr>
</thead>
<tbody>
<tr>
<td>Timestamp/verify meter (edge)</td>
<td><code>verae.billing.usage.recorded</code></td>
<td><strong>code path exists</strong> (<code>zappier/src/app.ts</code>
<code>onUsage</code>)</td>
<td>Confirm <code>NATS_URL</code> on live edge; 402 from account-balance
when prepaid empty</td>
</tr>
<tr>
<td>Middleware quota</td>
<td>local files <code>recordUsage</code></td>
<td><strong>yes</strong></td>
<td>Either publish <code>usage.recorded</code> too or drop duplicate
quota</td>
</tr>
<tr>
<td>Optional JS meter</td>
<td><code>verae.zapier.usage</code> + <code>USAGE_WRITER</code></td>
<td>stream only</td>
<td>Wire or delete</td>
</tr>
<tr>
<td>Portal reload</td>
<td><code>payment.recorded</code> + <code>balance.adjust</code></td>
<td>coded</td>
<td>confirm both fire on Stripe/test reload</td>
</tr>
<tr>
<td>CS credit</td>
<td><code>balance.adjust</code> + <code>credit.applied</code></td>
<td>coded</td>
<td>confirm applied pub on live</td>
</tr>
<tr>
<td>Customer upsert</td>
<td><code>customer.put</code></td>
<td>coded</td>
<td>—</td>
</tr>
<tr>
<td>Authz deny audit</td>
<td><code>verae.access.authz.deny</code></td>
<td>running on deny</td>
<td><strong>no SIEM sink</strong></td>
</tr>
<tr>
<td>Job lifecycle log</td>
<td><code>jobs.events</code></td>
<td>running</td>
<td>mailbox counts only — add structured log/export if needed</td>
</tr>
<tr>
<td>Archive put/query</td>
<td><code>VERAE_ARCHIVE</code></td>
<td>510 worm/tree</td>
<td>aggregator not in wait path</td>
</tr>
<tr>
<td>Public edge</td>
<td>leaf</td>
<td>running</td>
<td>rotate token; do not expose <code>:6222</code></td>
</tr>
</tbody>
</table>
<h2 id="ordered-finish-for-message-routing">Ordered finish for message
routing</h2>
<ol type="1">
<li><strong>Do not double-start</strong> job-poller: either in-process
<strong>or</strong> fleet copies, same queue
<code>job-poller</code>.</li>
<li>Confirm edge <strong><code>NATS_URL</code></strong> so
<code>usage.recorded</code> actually lands; then 402 from books. Unify
or drop middleware local <code>recordUsage</code>.</li>
<li>Enable <strong>archive-aggregator</strong> (or in-process
equivalent) so <code>includeAttached</code> and tree proofs work on
wait/status.</li>
<li>Raise worm/tree <strong>replica floors</strong> on 510/more CTs if
archive HA is required (fleet overlay min 3).</li>
<li>Operator: Zapier <code>push</code> +
<code>MOCK_VERAE=false</code>.</li>
<li>Prove: Zap create → <code>jobs.watch</code> → live
<code>GET /api/status</code> → <code>jobs.events</code> → wait body +
REST Hook.</li>
<li>nkeys/mTLS after every client has creds; then lock down the public
edge to accounts (LEAF vs INTERNAL).</li>
<li>Optional: exclusive <code>jobs-events</code> mailbox; SIEM on
<code>authz.deny</code> and <code>jobs.events</code>;
<code>verae.splitter.in</code> if splitter is split out;
<code>USAGE_WRITER</code> or delete <code>ZAPIER_USAGE</code>.</li>
</ol>
<h2 id="what-is-already-enough-for-a-mock-zap">What is already enough
for a mock Zap</h2>
<p>HTTPS zap/mw → mock create → <code>jobs.watch</code> → in-process
poller → mock status completed → <code>jobs.events</code> → wait returns
mock certificate. Archive extras and live chain are the missing
middle.</p>
<h1 id="developer-credentials-and-connection-info">Developer credentials
and connection info</h1>
<p>Lab pack for the Verae Time × Zapier stack on
<strong>NS1.GEORGELAMBERT.ORG</strong> (<code>70.88.205.138</code>).
Values below were checked <strong>2026-09-14</strong>. These are
<strong>lab defaults</strong>, not production secrets. Rotate the public
NATS token and admin secrets before treating this as a customer
environment.</p>
<p>Zapier cloud <strong>never</strong> gets a NATS URL or a Verae
password.</p>
<h2 id="who-you-are-talking-to">1. Who you are talking to</h2>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr>
<th>Host</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>70.88.205.138</code></td>
<td>NS1 Proxmox. Public HTTPS + public NATS <strong>client</strong>
edge</td>
</tr>
<tr>
<td><code>10.10.10.20</code></td>
<td>LXC 510 <code>verae-px-worker</code> (worm + tree)</td>
</tr>
<tr>
<td><code>10.10.10.21–23</code></td>
<td>LXC 511–513 <code>nats-a/b/c</code> JetStream cluster
<code>verae</code></td>
</tr>
</tbody>
</table>
<p>SSH to the box: <code>ssh marchon@70.88.205.138</code><br />
Git SSH:
<code>ssh://git@git.georgelambert.org:2223/marchon/&lt;repo&gt;.git</code>
(port <strong>2223</strong>)</p>
<h2 id="public-https-doors-live">2. Public HTTPS doors (live)</h2>
<p>Wildcard <code>*.zapier.georgelambert.org</code> →
<code>70.88.205.138</code>.</p>
<table>
<colgroup>
<col style="width: 15%" />
<col style="width: 28%" />
<col style="width: 18%" />
<col style="width: 37%" />
</colgroup>
<thead>
<tr>
<th>URL</th>
<th>Process</th>
<th>Port</th>
<th>Last check</th>
</tr>
</thead>
<tbody>
<tr>
<td>https://zapier.georgelambert.org/</td>
<td>docs catalog</td>
<td>files</td>
<td>200</td>
</tr>
<tr>
<td>https://git.georgelambert.org/</td>
<td>Forgejo</td>
<td>:443 / SSH :2223</td>
<td>200</td>
</tr>
<tr>
<td>https://portal.zapier.georgelambert.org/</td>
<td>access-web → <code>/portal/</code></td>
<td>:3021</td>
<td>302 → portal</td>
</tr>
<tr>
<td>https://api.zapier.georgelambert.org/health</td>
<td>access-api</td>
<td>:3022</td>
<td>200</td>
</tr>
<tr>
<td>https://leaf.zapier.georgelambert.org/health</td>
<td>access-leaf</td>
<td>:3023</td>
<td>200</td>
</tr>
<tr>
<td>https://zap.zapier.georgelambert.org/health</td>
<td>access-zapier</td>
<td>:3024</td>
<td>200</td>
</tr>
<tr>
<td>https://staff.zapier.georgelambert.org/</td>
<td>access-staff</td>
<td>:3025</td>
<td>302 login</td>
</tr>
<tr>
<td>https://login.zapier.georgelambert.org/</td>
<td>staff-session</td>
<td>:3027</td>
<td>200</td>
</tr>
<tr>
<td>https://iam.zapier.georgelambert.org/health</td>
<td>staff-iam</td>
<td>:3028</td>
<td>200 (<code>users: 5</code>)</td>
</tr>
<tr>
<td>https://cs.zapier.georgelambert.org/</td>
<td>customer-service</td>
<td>:3011</td>
<td>302</td>
</tr>
<tr>
<td>https://sales.zapier.georgelambert.org/</td>
<td>sales-pricing</td>
<td>:3012</td>
<td>302</td>
</tr>
<tr>
<td>https://acct.zapier.georgelambert.org/</td>
<td>accounting-export</td>
<td>:3013</td>
<td>302</td>
</tr>
<tr>
<td>https://mw.zapier.georgelambert.org/health</td>
<td>middleware</td>
<td>:3100</td>
<td>200 <code>mockVerae:true</code> <code>natsConnected:true</code></td>
</tr>
<tr>
<td>https://fleet.zapier.georgelambert.org/</td>
<td>fleet console</td>
<td>:3850</td>
<td>302</td>
</tr>
<tr>
<td>https://nats.zapier.georgelambert.org/</td>
<td>NATS WebSocket (Caddy)</td>
<td>14422</td>
<td><strong>400</strong> on GET (expected; it is WS)</td>
</tr>
</tbody>
</table>
<p>Loopback on NS1 (not public): zappier-edge
<strong><code>127.0.0.1:13000</code></strong> (admin + Stripe + SQLite).
Tunnel:</p>
<div class="sourceCode" id="cb7"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="fu">ssh</span> <span class="at">-L</span> 13000:127.0.0.1:13000 marchon@70.88.205.138</span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a><span class="co"># then http://127.0.0.1:13000/admin and /docs</span></span></code></pre></div>
<p>Operator console tunnel:
<code>ssh -L 3850:127.0.0.1:3850 marchon@70.88.205.138</code></p>
<h2 id="nats">3. NATS</h2>
<h3 id="internal-cluster-workers-keep-fleet">Internal cluster (workers,
keep, fleet)</h3>
<p>No token. Private <code>vmbr1</code> only.</p>
<pre class="text"><code>NATS_URL=nats://10.10.10.21:4222,nats://10.10.10.22:4222,nats://10.10.10.23:4222
JETSTREAM_REPLICAS=3</code></pre>
<table>
<thead>
<tr>
<th>Port</th>
<th>Bind</th>
<th>Use</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>:4222</code></td>
<td><code>10.10.10.21–23</code></td>
<td>clients</td>
</tr>
<tr>
<td><code>:6222</code></td>
<td>same, <code>vmbr1</code></td>
<td>cluster routes — <strong>do not publish</strong></td>
</tr>
<tr>
<td><code>:7422</code></td>
<td>same</td>
<td>leaf hubs for the public edge — <strong>do not publish</strong></td>
</tr>
<tr>
<td><code>:8222</code></td>
<td><code>127.0.0.1</code> on each LXC</td>
<td>monitor</td>
</tr>
</tbody>
</table>
<p>From a laptop you <strong>cannot</strong> reach
<code>10.10.10.x</code> unless you are on NS1 or you tunnel:</p>
<div class="sourceCode" id="cb9"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="fu">ssh</span> <span class="at">-L</span> 14222:10.10.10.21:4222 <span class="at">-L</span> 14223:10.10.10.22:4222 <span class="at">-L</span> 14224:10.10.10.23:4222 marchon@70.88.205.138</span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a><span class="bu">export</span> <span class="va">NATS_URL</span><span class="op">=</span>nats://127.0.0.1:14222,nats://127.0.0.1:14223,nats://127.0.0.1:14224</span></code></pre></div>
<h3 id="public-client-edge-token">Public client edge (token)</h3>
<p>Live token (file <code>/etc/nats/public-edge.token</code> on
NS1):</p>
<pre class="text"><code>4743a49383ce2ce86e99d9d41c45c0004fe75eb23ef3a2b9</code></pre>
<pre class="text"><code>nats://4743a49383ce2ce86e99d9d41c45c0004fe75eb23ef3a2b9@70.88.205.138:4222
wss://nats.zapier.georgelambert.org</code></pre>
<p><code>CONNECT</code> with that token returns <code>PONG</code>
(checked 2026-09-14). <code>auth_required: true</code>. Server name
<code>nats-public-edge</code>. <strong>Do not</strong> put this URL in
Zapier. Recreate:
<code>bash packages/verae-nats-cluster/scripts/expose-public.sh</code>
on NS1.</p>
<p>Leftover lab listeners (ignore for new clients): host
<code>127.0.0.1:4222</code> docker-proxy, <code>10.10.10.1:4222</code>
socat. zappier-edge process still has
<code>NATS_URL=nats://127.0.0.1:4222</code> — billing pubs may miss the
3-node cluster until that env is flipped.</p>
<p>nkeys/mTLS: <strong>not</strong> on. Isolation is LAN + token.</p>
<h2 id="middleware-jobs">4. Middleware (jobs)</h2>
<p>Live: <code>MOCK_VERAE=true</code>, <code>NATS_ENABLED=true</code>,
cluster URL as above, <code>ADMIN_SECRET=change-me-admin</code>
(default), <code>TOKEN_SECRET=dev-secret-change-me</code> (default).</p>
<h3 id="lab-tenant-created-2026-09-14-for-this-pack">Lab tenant (created
2026-09-14 for this pack)</h3>
<table>
<thead>
<tr>
<th>Field</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>tenantId</td>
<td><code>tenant-dev-lab-local-89ab82de</code></td>
</tr>
<tr>
<td>plan</td>
<td>free</td>
</tr>
<tr>
<td>API key</td>
<td><code>zmw_pXdwlUUyMHkAvqj3RRoQ2gOG6yAUQlun</code></td>
</tr>
<tr>
<td>mock Verae user</td>
<td><code>lab-dev</code> / <code>lab-dev-pass</code></td>
</tr>
<tr>
<td>veraeUserId</td>
<td><code>vu_20ed6029b99efb2e</code></td>
</tr>
</tbody>
</table>
<p>Proven: <code>GET /zapier/v1/auth/me</code> → valid;
<code>POST /zapier/v1/timestamp</code> → 202 <code>jobId</code>; wait →
mock certificate <code>blockIndex: 42</code>.</p>
<div class="sourceCode" id="cb12"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="va">KEY</span><span class="op">=</span>zmw_pXdwlUUyMHkAvqj3RRoQ2gOG6yAUQlun</span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a><span class="ex">curl</span> <span class="at">-sS</span> <span class="at">-H</span> <span class="st">&quot;x-api-key: </span><span class="va">$KEY</span><span class="st">&quot;</span> https://mw.zapier.georgelambert.org/zapier/v1/auth/me</span>
<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-5"><a href="#cb12-5" aria-hidden="true" tabindex="-1"></a><span class="ex">curl</span> <span class="at">-sS</span> <span class="at">-H</span> <span class="st">&quot;x-api-key: </span><span class="va">$KEY</span><span class="st">&quot;</span> <span class="at">-H</span> <span class="st">&quot;content-type: application/json&quot;</span> <span class="dt">\</span></span>
<span id="cb12-6"><a href="#cb12-6" aria-hidden="true" tabindex="-1"></a> <span class="at">-d</span> <span class="st">&#39;{&quot;sha256&quot;:&quot;aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&quot;}&#39;</span> <span class="dt">\</span></span>
<span id="cb12-7"><a href="#cb12-7" aria-hidden="true" tabindex="-1"></a> https://mw.zapier.georgelambert.org/zapier/v1/timestamp</span>
<span id="cb12-8"><a href="#cb12-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-9"><a href="#cb12-9" aria-hidden="true" tabindex="-1"></a><span class="ex">curl</span> <span class="at">-sS</span> <span class="at">-H</span> <span class="st">&quot;x-api-key: </span><span class="va">$KEY</span><span class="st">&quot;</span> <span class="at">-H</span> <span class="st">&quot;content-type: application/json&quot;</span> <span class="dt">\</span></span>
<span id="cb12-10"><a href="#cb12-10" aria-hidden="true" tabindex="-1"></a> <span class="at">-d</span> <span class="st">&#39;{&quot;sha256&quot;:&quot;bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb&quot;}&#39;</span> <span class="dt">\</span></span>
<span id="cb12-11"><a href="#cb12-11" aria-hidden="true" tabindex="-1"></a> https://mw.zapier.georgelambert.org/zapier/v1/timestamp/wait</span></code></pre></div>
<p>Customer API plane (forwards <code>x-api-key</code> to
middleware):</p>
<div class="sourceCode" id="cb13"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="ex">curl</span> <span class="at">-sS</span> <span class="at">-H</span> <span class="st">&quot;x-api-key: </span><span class="va">$KEY</span><span class="st">&quot;</span> <span class="at">-H</span> <span class="st">&quot;content-type: application/json&quot;</span> <span class="dt">\</span></span>
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a> <span class="at">-d</span> <span class="st">&#39;{&quot;sha256&quot;:&quot;aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&quot;}&#39;</span> <span class="dt">\</span></span>
<span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a> https://api.zapier.georgelambert.org/v1/timestamp</span></code></pre></div>
<p><code>https://zap.zapier.georgelambert.org</code> is the Zapier
<strong>plane</strong>. It does <strong>not</strong> currently forward
<code>x-api-key</code> on timestamp (401 if you curl it with a key).
Zapier Platform push is still waiting on login; when it is live, set
<code>MIDDLEWARE_BASE_URL=https://zap.zapier.georgelambert.org</code>
and use a <code>zmw_…</code> key in the Zap.</p>
<p>Mint another key (mock Verae accepts any username/password):</p>
<div class="sourceCode" id="cb14"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="ex">curl</span> <span class="at">-sS</span> <span class="at">-H</span> <span class="st">&quot;content-type: application/json&quot;</span> <span class="dt">\</span></span>
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a> <span class="at">-d</span> <span class="st">&#39;{&quot;email&quot;:&quot;you@lab.local&quot;,&quot;name&quot;:&quot;You&quot;,&quot;veraeUsername&quot;:&quot;you&quot;,&quot;veraePassword&quot;:&quot;you-pass&quot;}&#39;</span> <span class="dt">\</span></span>
<span id="cb14-3"><a href="#cb14-3" aria-hidden="true" tabindex="-1"></a> https://mw.zapier.georgelambert.org/zapier/v1/signup</span></code></pre></div>
<p>Admin list/provision:</p>
<div class="sourceCode" id="cb15"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a><span class="ex">curl</span> <span class="at">-sS</span> <span class="at">-H</span> <span class="st">&quot;x-admin-secret: change-me-admin&quot;</span> <span class="dt">\</span></span>
<span id="cb15-2"><a href="#cb15-2" aria-hidden="true" tabindex="-1"></a> https://mw.zapier.georgelambert.org/zapier/v1/admin/tenants</span></code></pre></div>
<h2 id="portal-zappier-edge-billing-users">5. Portal / zappier-edge
(billing users)</h2>
<p>SQLite
<code>/SSD3/moved/home-marchon/verae-stack/packages/zappier/zappier.db</code>.
Bind <code>127.0.0.1:13000</code>.</p>
<p>Seed <strong>customer API keys</strong> (<code>x-api-key</code> on
edge <code>/v1/*</code>, <strong>not</strong> middleware
<code>zmw_</code>):</p>
<table>
<thead>
<tr>
<th>Customer</th>
<th>tier</th>
<th>key</th>
</tr>
</thead>
<tbody>
<tr>
<td>Ada (free)</td>
<td>free</td>
<td><code>key-ada</code></td>
</tr>
<tr>
<td>Grace (pro)</td>
<td>pro</td>
<td><code>key-grace</code></td>
</tr>
<tr>
<td>Linus (business)</td>
<td>business</td>
<td><code>key-linus</code></td>
</tr>
<tr>
<td>Docs Capture</td>
<td>free</td>
<td><code>key-021851970d8a22c6</code></td>
</tr>
</tbody>
</table>
<p>These keys are for the <strong>edge meter / portal</strong>.
Middleware jobs need a <code>zmw_…</code> key (section 4).</p>
<p>Seed <strong>admin console</strong> (<code>/admin</code>,
<code>x-admin-key</code> or login):</p>
<table>
<thead>
<tr>
<th>username</th>
<th>password</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>admin</code></td>
<td><code>admin-dev-key</code></td>
</tr>
<tr>
<td><code>demo</code></td>
<td><code>$$$Adm1n###</code></td>
</tr>
</tbody>
</table>
<p><code>ZAPPIER_ADMIN_KEY</code> default on CS/sales/staff processes:
<code>admin-dev-key</code>.</p>
<p>Stripe: <strong>not</strong> live (lab quotes only). Env
<code>STRIPE_*</code> unset on the sampled edge process.</p>
<p>Signup more customers at
https://portal.zapier.georgelambert.org/portal/</p>
<h2 id="staff-iam">6. Staff IAM</h2>
<p>https://login.zapier.georgelambert.org/ ·
https://iam.zapier.georgelambert.org/</p>
<p>Seed users (defaults; live IAM has these five):</p>
<table>
<thead>
<tr>
<th>username</th>
<th>password</th>
<th>role</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>admin</code></td>
<td><code>admin-dev-key</code></td>
<td>owner</td>
</tr>
<tr>
<td><code>cs</code></td>
<td><code>cs-dev-key</code></td>
<td>cs</td>
</tr>
<tr>
<td><code>sales</code></td>
<td><code>sales-dev-key</code></td>
<td>sales</td>
</tr>
<tr>
<td><code>accounting</code></td>
<td><code>acct-dev-key</code></td>
<td>accounting</td>
</tr>
<tr>
<td><code>operator</code></td>
<td><code>fleet-dev-key</code></td>
<td>operator</td>
</tr>
</tbody>
</table>
<p><code>STAFF_KEY</code> / session fallback:
<code>admin-dev-key</code>.</p>
<h2 id="leaf-plane">7. Leaf plane</h2>
<p>https://leaf.zapier.georgelambert.org/health →
<code>verae.access.leaf.in</code></p>
<p>Live: <code>LEAF_TOKEN=leaf-dev-token</code></p>
<div class="sourceCode" id="cb16"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb16-1"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a><span class="ex">curl</span> <span class="at">-sS</span> <span class="at">-H</span> <span class="st">&quot;content-type: application/json&quot;</span> <span class="dt">\</span></span>
<span id="cb16-2"><a href="#cb16-2" aria-hidden="true" tabindex="-1"></a> <span class="at">-d</span> <span class="st">&#39;{&quot;token&quot;:&quot;leaf-dev-token&quot;,&quot;target&quot;:&quot;verae.archive.put&quot;,&quot;payload&quot;:{},&quot;principal&quot;:&quot;dev&quot;}&#39;</span> <span class="dt">\</span></span>
<span id="cb16-3"><a href="#cb16-3" aria-hidden="true" tabindex="-1"></a> https://leaf.zapier.georgelambert.org/</span></code></pre></div>
<p>Billing targets are <strong>denied</strong> on this plane.</p>
<h2 id="git-forgejo">8. Git (Forgejo)</h2>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr>
<td>Web</td>
<td>https://git.georgelambert.org/marchon/</td>
</tr>
<tr>
<td>SSH</td>
<td><code>ssh://git@git.georgelambert.org:2223/marchon/&lt;name&gt;.git</code></td>
</tr>
<tr>
<td>User</td>
<td><code>marchon</code></td>
</tr>
<tr>
<td>Overview of routes</td>
<td>https://git.georgelambert.org/marchon/dev-overview</td>
</tr>
<tr>
<td>Monorepo</td>
<td><code>master-zapier-plan-draft</code></td>
</tr>
<tr>
<td>Bootstrap</td>
<td>https://git.georgelambert.org/marchon/verae-bootstrap</td>
</tr>
</tbody>
</table>
<div class="sourceCode" id="cb17"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb17-1"><a href="#cb17-1" aria-hidden="true" tabindex="-1"></a><span class="va">GIT_SSH_COMMAND</span><span class="op">=</span><span class="st">&quot;ssh -p 2223&quot;</span> <span class="fu">git</span> clone ssh://git@git.georgelambert.org:2223/marchon/dev-overview.git</span></code></pre></div>
<p>Add your SSH public key on Forgejo (user <strong>marchon</strong> or
a new account). Do not commit deploy tokens.</p>
<p>Clone-all:</p>
<div class="sourceCode" id="cb18"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb18-1"><a href="#cb18-1" aria-hidden="true" tabindex="-1"></a><span class="fu">git</span> clone ssh://git@git.georgelambert.org:2223/marchon/verae-bootstrap.git</span>
<span id="cb18-2"><a href="#cb18-2" aria-hidden="true" tabindex="-1"></a><span class="bu">cd</span> verae-bootstrap</span>
<span id="cb18-3"><a href="#cb18-3" aria-hidden="true" tabindex="-1"></a><span class="bu">export</span> <span class="va">VERAE_SRC</span><span class="op">=</span><span class="va">$HOME</span>/verae-src</span>
<span id="cb18-4"><a href="#cb18-4" aria-hidden="true" tabindex="-1"></a><span class="fu">bash</span> scripts/fetch.sh ns1-all-in-one</span></code></pre></div>
<h2 id="environment-cheat-sheet-new-worker-on-the-cluster">9.
Environment cheat sheet (new worker on the cluster)</h2>
<div class="sourceCode" id="cb19"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb19-1"><a href="#cb19-1" aria-hidden="true" tabindex="-1"></a><span class="bu">export</span> <span class="va">NATS_URL</span><span class="op">=</span>nats://10.10.10.21:4222,nats://10.10.10.22:4222,nats://10.10.10.23:4222</span>
<span id="cb19-2"><a href="#cb19-2" aria-hidden="true" tabindex="-1"></a><span class="bu">export</span> <span class="va">JETSTREAM_REPLICAS</span><span class="op">=</span>3</span>
<span id="cb19-3"><a href="#cb19-3" aria-hidden="true" tabindex="-1"></a><span class="bu">export</span> <span class="va">NATS_ENABLED</span><span class="op">=</span>true</span>
<span id="cb19-4"><a href="#cb19-4" aria-hidden="true" tabindex="-1"></a><span class="bu">export</span> <span class="va">MOCK_VERAE</span><span class="op">=</span>true <span class="co"># flip to false only with live Verae login</span></span>
<span id="cb19-5"><a href="#cb19-5" aria-hidden="true" tabindex="-1"></a><span class="bu">export</span> <span class="va">VERAE_API_BASE_URL</span><span class="op">=</span>https://api.veraetime.net</span>
<span id="cb19-6"><a href="#cb19-6" aria-hidden="true" tabindex="-1"></a><span class="bu">export</span> <span class="va">AUTHZ_URL</span><span class="op">=</span>http://127.0.0.1:3020</span>
<span id="cb19-7"><a href="#cb19-7" aria-hidden="true" tabindex="-1"></a><span class="bu">export</span> <span class="va">ZAPPIER_UPSTREAM</span><span class="op">=</span>http://127.0.0.1:3100</span>
<span id="cb19-8"><a href="#cb19-8" aria-hidden="true" tabindex="-1"></a><span class="bu">export</span> <span class="va">ZAPPIER_ADMIN_URL</span><span class="op">=</span>http://127.0.0.1:13000</span>
<span id="cb19-9"><a href="#cb19-9" aria-hidden="true" tabindex="-1"></a><span class="bu">export</span> <span class="va">ZAPPIER_ADMIN_KEY</span><span class="op">=</span>admin-dev-key</span>
<span id="cb19-10"><a href="#cb19-10" aria-hidden="true" tabindex="-1"></a><span class="bu">export</span> <span class="va">LEAF_TOKEN</span><span class="op">=</span>leaf-dev-token</span>
<span id="cb19-11"><a href="#cb19-11" aria-hidden="true" tabindex="-1"></a><span class="bu">export</span> <span class="va">STAFF_IAM_URL</span><span class="op">=</span>http://127.0.0.1:3028</span></code></pre></div>
<p>Template worker: clone <code>verae-nats-process</code>, rename
<code>verae.example.process.*</code>.</p>
<h2 id="not-issued-yet-blocked">10. Not issued yet (blocked)</h2>
<table>
<colgroup>
<col style="width: 54%" />
<col style="width: 45%" />
</colgroup>
<thead>
<tr>
<th>Item</th>
<th>Why</th>
</tr>
</thead>
<tbody>
<tr>
<td>Zapier Platform deploy key / <code>zapier login</code></td>
<td>operator has not logged the CLI in</td>
</tr>
<tr>
<td>Live <code>api.veraetime.net</code> username/password</td>
<td><code>MOCK_VERAE=true</code>; JWT would stay in middleware</td>
</tr>
<tr>
<td>NATS nkeys / account creds</td>
<td>cluster is <code>$G</code> + LAN</td>
</tr>
<tr>
<td>Stripe live secret</td>
<td>lab only</td>
</tr>
<tr>
<td>lan-134 SSH worker</td>
<td>disabled</td>
</tr>
</tbody>
</table>
<p>When Zapier login exists:
<code>MIDDLEWARE_BASE_URL=https://zap.zapier.georgelambert.org</code>,
auth field <code>api_key</code> = <code>zmw_…</code>.</p>
<p>When live Verae exists: middleware <code>MOCK_VERAE=false</code> +
<code>VERAE_API_BASE_URL=https://api.veraetime.net</code> + working
login. Zapier still never sees that password.</p>
<h2 id="related-docs">11. Related docs</h2>
<ul>
<li>Routes: <a href="01-nats-routes.md">01-nats-routes.md</a></li>
<li>Endpoint I/O: <a href="02-endpoint-io.md">02-endpoint-io.md</a></li>
<li>Job path: <a
href="03-zapier-verae-path.md">03-zapier-verae-path.md</a></li>
<li>Gaps: <a href="04-gaps-and-finish.md">04-gaps-and-finish.md</a></li>
<li>Ops start:
https://zapier.georgelambert.org/packages/verae-ops/GETTING-STARTED.pdf</li>
<li>Status:
https://zapier.georgelambert.org/packages/zapier-decisions/STATUS.pdf</li>
</ul>
</body>
</html>