#!/usr/bin/env python3
"""Emit architecture SVGs for the Verae Time x Zapier getting-started guide."""
from __future__ import annotations
from pathlib import Path
OUT = Path(__file__).resolve().parents[1] / "docs" / "diagrams"
OUT.mkdir(parents=True, exist_ok=True)
# Palette matches getting-started.pdf
NAVY = "#0F2744"
TEAL = "#1A6B6B"
TEAL_DK = "#134848"
SLATE = "#334155"
MUTED = "#64748B"
LINE = "#94A3B8"
PAPER = "#FFFFFF"
BG = "#F8FAFC"
PUBLIC = "#E8F1F8"
PRIVATE = "#F3EDE0"
VERAE = "#E8F6EE"
ZAPIER = "#FFF1E6"
ACCENT = "#C2410C"
OK = "#15803D"
WARN = "#B45309"
class Svg:
def __init__(self, w: int, h: int, title: str):
self.w = w
self.h = h
self.title = title
self.parts: list[str] = []
def raw(self, s: str) -> None:
self.parts.append(s)
def rect(
self,
x,
y,
w,
h,
fill=PAPER,
stroke=NAVY,
sw=1.4,
r=8,
opacity=1,
) -> None:
self.raw(
f''
)
def text(
self,
x,
y,
s,
size=12,
fill=NAVY,
anchor="start",
weight="500",
family="Helvetica, Arial, sans-serif",
) -> None:
esc = (
s.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
)
self.raw(
f'{esc}'
)
def lines(self, x, y, lines, size=11, fill=SLATE, anchor="start", leading=14, weight="400"):
for i, line in enumerate(lines):
self.text(x, y + i * leading, line, size=size, fill=fill, anchor=anchor, weight=weight)
def arrow(self, x1, y1, x2, y2, color=TEAL, sw=1.8, label="", label_off=-10) -> None:
self.raw(
f''
)
if label:
mx, my = (x1 + x2) / 2, (y1 + y2) / 2 + label_off
self.text(mx, my, label, size=10, fill=TEAL_DK, anchor="middle", weight="600")
def varrow(self, x, y1, y2, color=TEAL, sw=1.8, label="", lx=8) -> None:
self.arrow(x, y1, x, y2, color=color, sw=sw)
if label:
self.text(x + lx, (y1 + y2) / 2 + 4, label, size=10, fill=TEAL_DK, weight="600")
def harrow(self, x1, x2, y, color=TEAL, sw=1.8, label="", label_off=-11) -> None:
self.arrow(x1, y, x2, y, color=color, sw=sw, label=label, label_off=label_off)
def badge(self, x, y, w, h, text, fill=TEAL, color=PAPER) -> None:
self.rect(x, y, w, h, fill=fill, stroke=fill, r=4, sw=0)
self.text(x + w / 2, y + h / 2 + 4, text, size=10, fill=color, anchor="middle", weight="700")
def save(self, name: str) -> Path:
path = OUT / name
body = "\n ".join(self.parts)
svg = f"""
"""
path.write_text(svg, encoding="utf-8")
print(f"wrote {path.relative_to(OUT.parent.parent)} ({path.stat().st_size} bytes)")
return path
def high_level():
s = Svg(1100, 680, "High-level architecture: Zapier to Verae via middleware")
s.text(24, 28, "HIGH-LEVEL ARCHITECTURE", size=11, fill=TEAL, weight="700")
s.text(24, 50, "Verae Time x Zapier", size=20, fill=NAVY, weight="700")
s.text(
24,
70,
"Zapier never calls api.veraetime.net and never speaks NATS. HTTPS only at the public edge.",
size=12,
fill=MUTED,
)
# Zones
s.rect(20, 90, 1060, 150, fill=ZAPIER, stroke="#F0D2B8", sw=1.2, r=12)
s.text(36, 112, "PUBLIC · Zapier cloud", size=11, fill=ACCENT, weight="700")
s.rect(20, 258, 700, 280, fill=PUBLIC, stroke="#C5D6E8", sw=1.2, r=12)
s.text(36, 280, "YOUR INFRASTRUCTURE · public HTTPS edge + private workers", size=11, fill=TEAL, weight="700")
s.rect(740, 258, 340, 280, fill=PRIVATE, stroke="#E0D2B4", sw=1.2, r=12)
s.text(756, 280, "PRIVATE NETWORK", size=11, fill=WARN, weight="700")
s.rect(20, 556, 700, 104, fill=VERAE, stroke="#B7DCC7", sw=1.2, r=12)
s.text(36, 578, "VERAE TIMESTAMPING SERVICE", size=11, fill=OK, weight="700")
s.rect(740, 556, 340, 104, fill=ZAPIER, stroke="#F0D2B8", sw=1.2, r=12)
s.text(756, 578, "ZAPIER REST HOOKS", size=11, fill=ACCENT, weight="700")
# User
s.rect(40, 128, 150, 88, fill=PAPER, stroke=NAVY)
s.text(115, 158, "User", size=14, fill=NAVY, anchor="middle", weight="700")
s.lines(115, 178, ["Zap editor", "connection form"], size=11, fill=SLATE, anchor="middle")
# Zapier app
s.rect(250, 122, 360, 100, fill=PAPER, stroke=NAVY, sw=1.8)
s.text(430, 148, "Zapier Platform CLI app", size=14, fill=NAVY, anchor="middle", weight="700")
s.lines(
430,
168,
["scratch/veraetime (session TS) or verae-zapier (zmw_ JS)", "runs on Zapier cloud when a Zap step executes"],
size=11,
fill=SLATE,
anchor="middle",
leading=15,
)
s.rect(660, 128, 180, 88, fill=PAPER, stroke=LINE)
s.text(750, 158, "Zap trigger", size=13, fill=NAVY, anchor="middle", weight="700")
s.lines(750, 178, ["Timestamp Completed", "hooks.zapier.com"], size=11, fill=SLATE, anchor="middle")
s.harrow(190, 250, 172, label="configure / run")
s.harrow(610, 660, 172, label="hook fires Zap")
# Middleware
s.rect(48, 300, 644, 120, fill=PAPER, stroke=NAVY, sw=1.8)
s.text(370, 326, "verae-zapier-middleware /zapier/v1/*", size=14, fill=NAVY, anchor="middle", weight="700")
s.lines(
370,
348,
[
"Auth bridge · tenants · entitlements · rate limits · REST Hook store",
"GET /health POST /auth/login GET /auth/me POST /timestamp[/wait|/batch]",
"POST /verify GET /status/{jobId} POST/DELETE /webhooks/*",
],
size=11,
fill=SLATE,
anchor="middle",
leading=16,
)
s.varrow(430, 222, 300, label="HTTPS Bearer zmw_ / zmt_")
# NATS
s.rect(760, 300, 300, 92, fill=PAPER, stroke=NAVY, sw=1.8)
s.text(910, 328, "NATS JetStream", size=14, fill=NAVY, anchor="middle", weight="700")
s.lines(
910,
348,
["ZAPIER_JOBS · EVENTS · WEBHOOKS", "never on the public internet"],
size=11,
fill=SLATE,
anchor="middle",
)
s.harrow(692, 760, 346, label="publish (private)")
# Workers
s.rect(760, 412, 300, 108, fill=PAPER, stroke=NAVY)
s.text(910, 438, "Workers", size=14, fill=NAVY, anchor="middle", weight="700")
s.lines(
910,
458,
["job-poller jobs.watch", "event-router jobs.events", "webhook-deliver webhooks.deliver"],
size=11,
fill=SLATE,
anchor="middle",
leading=15,
)
s.varrow(910, 392, 412)
# Verae
s.rect(48, 596, 644, 50, fill=PAPER, stroke=NAVY, sw=1.8)
s.text(370, 616, "api.veraetime.net", size=12, fill=NAVY, anchor="middle", weight="700")
s.text(
370,
634,
"POST /auth/login POST /api/timestamp 202 {jobId} GET /api/status/{jobId} POST /api/verify",
size=11,
fill=SLATE,
anchor="middle",
)
s.varrow(220, 420, 596, label="sync HTTPS")
s.arrow(820, 520, 370, 596, label="poll / create")
# Hooks
s.rect(760, 596, 300, 50, fill=PAPER, stroke=NAVY)
s.text(910, 626, "POST hooks.zapier.com timestamp.completed", size=11, fill=NAVY, anchor="middle", weight="600")
s.varrow(910, 520, 596, color=ACCENT, label="HTTPS egress")
return s.save("01-high-level-architecture.svg")
def security():
s = Svg(1100, 520, "Security boundaries")
s.text(24, 28, "SECURITY BOUNDARIES", size=11, fill=TEAL, weight="700")
s.text(24, 50, "What is public, what stays private", size=20, fill=NAVY, weight="700")
s.rect(20, 80, 1060, 200, fill=ZAPIER, stroke="#F0D2B8", r=12)
s.text(40, 106, "PUBLIC INTERNET", size=13, fill=ACCENT, weight="700")
s.rect(48, 124, 300, 130, fill=PAPER, stroke=NAVY)
s.text(198, 158, "Zapier cloud", size=14, fill=NAVY, anchor="middle", weight="700")
s.lines(198, 180, ["CLI app performs", "z.request only", "no NATS, no Verae JWT"], size=12, fill=SLATE, anchor="middle")
s.rect(400, 124, 300, 130, fill=PAPER, stroke=NAVY, sw=1.8)
s.text(550, 158, "Middleware HTTPS", size=14, fill=NAVY, anchor="middle", weight="700")
s.lines(550, 180, ["/zapier/v1 only", "TLS required in prod", "rate limit + entitlements"], size=12, fill=SLATE, anchor="middle")
s.rect(752, 124, 300, 130, fill=PAPER, stroke=NAVY)
s.text(902, 158, "Zapier hook URLs", size=14, fill=NAVY, anchor="middle", weight="700")
s.lines(902, 180, ["untrusted egress", "timeouts required", "SSRF allowlist = Phase 15"], size=12, fill=SLATE, anchor="middle")
s.harrow(348, 400, 189, label="HTTPS in")
s.harrow(700, 752, 189, label="HTTPS out")
s.rect(20, 300, 1060, 196, fill=PRIVATE, stroke="#E0D2B4", r=12)
s.text(40, 326, "PRIVATE · do not expose ports 4222 / store / TOKEN_SECRET", size=13, fill=WARN, weight="700")
s.rect(48, 348, 320, 126, fill=PAPER, stroke=NAVY)
s.text(208, 380, "NATS JetStream", size=14, fill=NAVY, anchor="middle", weight="700")
s.lines(208, 402, ["tokenRef, not raw JWT", "private network + auth", "mTLS is Phase 15"], size=12, fill=SLATE, anchor="middle")
s.rect(400, 348, 320, 126, fill=PAPER, stroke=NAVY)
s.text(560, 380, "Workers + file store", size=14, fill=NAVY, anchor="middle", weight="700")
s.lines(560, 402, ["STORE_PATH store.json", "single-node MVP", "Postgres/Redis = Phase 15"], size=12, fill=SLATE, anchor="middle")
s.rect(752, 348, 300, 126, fill=PAPER, stroke=NAVY)
s.text(902, 380, "api.veraetime.net", size=14, fill=NAVY, anchor="middle", weight="700")
s.lines(902, 402, ["Verae JWT stays here", "server-side login only", "MOCK_VERAE for tests"], size=12, fill=SLATE, anchor="middle")
s.harrow(368, 400, 411)
s.harrow(720, 752, 411)
return s.save("02-security-boundaries.svg")
def auth():
s = Svg(1100, 560, "Two-hop authentication")
s.text(24, 28, "AUTHENTICATION", size=11, fill=TEAL, weight="700")
s.text(24, 50, "Two hops · Zapier never sees the Verae JWT", size=20, fill=NAVY, weight="700")
# Hop 1
s.rect(20, 80, 1060, 220, fill=PUBLIC, stroke="#C5D6E8", r=12)
s.text(40, 106, "HOP 1 · end user → middleware", size=13, fill=TEAL, weight="700")
s.rect(48, 126, 220, 148, fill=PAPER, stroke=NAVY)
s.text(158, 156, "User in Zapier", size=13, fill=NAVY, anchor="middle", weight="700")
s.lines(
158,
178,
["username + password", "or zmw_ API key", "api_base_url", "default :3100"],
size=12,
fill=SLATE,
anchor="middle",
)
s.rect(320, 126, 280, 148, fill=PAPER, stroke=NAVY, sw=1.8)
s.text(460, 156, "POST /zapier/v1/auth/login", size=13, fill=NAVY, anchor="middle", weight="700")
s.lines(
460,
178,
["loginWithCredentials", "or loginWithApiKey", "returns accessToken", "stored as sessionKey (TS)"],
size=12,
fill=SLATE,
anchor="middle",
)
s.rect(650, 126, 200, 148, fill=PAPER, stroke=NAVY)
s.text(750, 156, "Tokens issued", size=13, fill=NAVY, anchor="middle", weight="700")
s.lines(750, 178, ["zmw_ tenant API key", "zmt_ HMAC session", "TOKEN_SECRET", "never commit"], size=12, fill=SLATE, anchor="middle")
s.rect(880, 126, 176, 148, fill=PAPER, stroke=NAVY)
s.text(968, 156, "Auth test", size=13, fill=NAVY, anchor="middle", weight="700")
s.lines(968, 178, ["GET /auth/me", "tenant + plan", "usage", "401 → refresh"], size=12, fill=SLATE, anchor="middle")
s.harrow(268, 320, 200)
s.harrow(600, 650, 200)
s.harrow(850, 880, 200)
# Hop 2
s.rect(20, 320, 1060, 216, fill=VERAE, stroke="#B7DCC7", r=12)
s.text(40, 346, "HOP 2 · middleware → Verae (server-side only)", size=13, fill=OK, weight="700")
s.rect(48, 366, 300, 148, fill=PAPER, stroke=NAVY)
s.text(198, 396, "veraeClient", size=13, fill=NAVY, anchor="middle", weight="700")
s.lines(
198,
418,
["POST /auth/login on Verae", "or MOCK_VERAE=true", "JWT held in process", "redacted in DEBUG_VERAE"],
size=12,
fill=SLATE,
anchor="middle",
)
s.rect(400, 366, 300, 148, fill=PAPER, stroke=NAVY, sw=1.8)
s.text(550, 396, "api.veraetime.net", size=13, fill=NAVY, anchor="middle", weight="700")
s.lines(
550,
418,
["Authorization: Bearer ", "timestamp / status / verify", "source of truth", "not reachable from Zapier"],
size=12,
fill=SLATE,
anchor="middle",
)
s.rect(752, 366, 300, 148, fill=PAPER, stroke=ACCENT, sw=1.8)
s.text(902, 396, "Do not leak", size=13, fill=ACCENT, anchor="middle", weight="700")
s.lines(
902,
418,
["no Verae JWT in Zapier", "no JWT in NATS (use tokenRef)", "no secrets in git", "no secrets in debug logs"],
size=12,
fill=SLATE,
anchor="middle",
)
s.harrow(348, 400, 440)
s.harrow(700, 752, 440)
return s.save("03-auth-two-hop.svg")
def flow_async():
s = Svg(1100, 620, "Create Timestamp async plus REST Hook")
s.text(24, 28, "FLOW A", size=11, fill=TEAL, weight="700")
s.text(24, 50, "Create Timestamp · async job + hook", size=20, fill=NAVY, weight="700")
cols = [
(70, "Zapier app"),
(290, "Middleware"),
(510, "NATS"),
(730, "Workers"),
(950, "Verae / Hooks"),
]
for x, name in cols:
s.rect(x - 80, 78, 160, 36, fill=NAVY, stroke=NAVY, r=6)
s.text(x, 101, name, size=12, fill=PAPER, anchor="middle", weight="700")
s.raw(f'')
s.harrow(70, 290, 148, label="1 POST /timestamp")
s.harrow(290, 950, 190, label="2 entitlement + POST /api/timestamp")
s.harrow(950, 290, 232, label="3 202 { jobId }", label_off=14)
s.harrow(290, 70, 274, label="4 return jobId to Zap", label_off=14)
s.harrow(290, 510, 322, label="5 publish jobs.watch")
s.harrow(510, 730, 364, label="6 job-poller")
s.harrow(730, 950, 406, label="7 GET /api/status/{jobId}")
s.harrow(950, 730, 448, label="8 pending: Nak + delay", label_off=14)
s.harrow(730, 510, 490, label="9 terminal → jobs.events", label_off=14)
s.harrow(510, 730, 532, label="10 match webhooks")
s.harrow(730, 950, 574, label="11 POST timestamp.completed")
s.text(
24,
612,
"Pair this action with the Timestamp Completed hook trigger. Zapier does not poll Verae.",
size=11,
fill=MUTED,
)
return s.save("04-flow-async-timestamp.svg")
def flow_wait():
s = Svg(1100, 480, "Create Timestamp and Wait")
s.text(24, 28, "FLOW B", size=11, fill=TEAL, weight="700")
s.text(24, 50, "Create Timestamp and Wait", size=20, fill=NAVY, weight="700")
s.text(24, 72, "In-process path is live today. Wait-via-NATS is Phase 9.", size=12, fill=MUTED)
s.rect(40, 100, 200, 80, fill=PAPER, stroke=NAVY)
s.text(140, 132, "Zapier", size=13, fill=NAVY, anchor="middle", weight="700")
s.lines(140, 152, ["POST /timestamp/wait"], size=11, fill=SLATE, anchor="middle")
s.rect(320, 100, 260, 80, fill=PAPER, stroke=NAVY, sw=1.8)
s.text(450, 132, "Middleware", size=13, fill=NAVY, anchor="middle", weight="700")
s.lines(450, 152, ["create job, then wait"], size=11, fill=SLATE, anchor="middle")
s.harrow(240, 320, 140, label="HTTPS")
# fork
s.rect(320, 230, 260, 100, fill=PUBLIC, stroke=TEAL)
s.text(450, 260, "NATS_ENABLED=false", size=12, fill=TEAL, anchor="middle", weight="700")
s.lines(450, 280, ["in-process poller (Phase 6)", "returns StatusResponse"], size=11, fill=SLATE, anchor="middle")
s.rect(640, 230, 400, 100, fill=PRIVATE, stroke=WARN)
s.text(840, 260, "NATS_ENABLED=true · Phase 9 (open)", size=12, fill=WARN, anchor="middle", weight="700")
s.lines(
840,
280,
["subscribe to jobs.events", "hard timeout → { jobId, status: pending }"],
size=11,
fill=SLATE,
anchor="middle",
)
s.varrow(450, 180, 230)
s.arrow(580, 180, 840, 230)
s.rect(40, 380, 1020, 72, fill=VERAE, stroke="#B7DCC7")
s.text(550, 410, "Return to Zapier", size=13, fill=NAVY, anchor="middle", weight="700")
s.lines(
550,
430,
["completed / failed status object or pending + jobId on timeout (Zapier can Find Job Status next)"],
size=11,
fill=SLATE,
anchor="middle",
)
s.varrow(450, 330, 380)
s.varrow(840, 330, 380)
return s.save("05-flow-wait.svg")
def nats():
s = Svg(1100, 560, "NATS subject topology")
s.text(24, 28, "NATS + JETSTREAM", size=11, fill=TEAL, weight="700")
s.text(24, 50, "Private subjects, streams, and workers", size=20, fill=NAVY, weight="700")
streams = [
(40, "ZAPIER_JOBS", "verae.zapier.jobs.watch", "Work queue", "job-poller", "GET /api/status/{jobId}"),
(310, "ZAPIER_EVENTS", "verae.zapier.jobs.events", "Limits (time)", "event-router", "enqueue webhooks / waiters"),
(580, "ZAPIER_WEBHOOKS", "verae.zapier.webhooks.deliver", "Work queue", "webhook-deliver", "POST Zapier targetUrl"),
(850, "ZAPIER_USAGE", "verae.zapier.usage", "Optional", "usage-writer", "billing export"),
]
for x, stream, subj, ret, cons, call in streams:
s.rect(x, 88, 250, 250, fill=PAPER, stroke=NAVY, r=10)
s.badge(x + 14, 104, 222, 26, stream)
s.text(x + 125, 156, subj, size=11, fill=TEAL_DK, anchor="middle", weight="600")
s.lines(
x + 125,
182,
[ret, "", "consumer: " + cons, call],
size=12,
fill=SLATE,
anchor="middle",
leading=18,
)
s.harrow(290, 310, 213)
s.harrow(560, 580, 213)
s.harrow(830, 850, 213, color=LINE)
s.rect(40, 364, 1020, 168, fill=PRIVATE, stroke="#E0D2B4", r=10)
s.text(60, 392, "Ack semantics", size=13, fill=NAVY, weight="700")
s.lines(
60,
418,
[
"Job still pending Nak with delay ~ intervalMs or republish attempt+1",
"Job terminal publish jobs.events, then Ack the watch message",
"Webhook HTTP 2xx Ack · 5xx / network Nak until max_deliver",
"Poison message terminate after max_deliver; DEBUG_VERAE=webhooks DLQ log",
"Prefer tokenRef over embedding a Verae JWT in any NATS payload",
],
size=12,
fill=SLATE,
leading=22,
)
return s.save("06-nats-topology.svg")
def operations():
s = Svg(1100, 640, "Zapier operations mapped to middleware and Verae")
s.text(24, 28, "OPERATIONS MAP", size=11, fill=TEAL, weight="700")
s.text(24, 50, "Connector → /zapier/v1 → Verae wrap", size=20, fill=NAVY, weight="700")
headers = [(40, "Zapier"), (280, "Type"), (400, "Middleware"), (700, "Verae")]
s.rect(20, 78, 1060, 36, fill=NAVY, stroke=NAVY, r=0)
for x, h in headers:
s.text(x, 101, h, size=12, fill=PAPER, weight="700")
rows = [
("Create Timestamp", "create", "POST /timestamp", "POST /api/timestamp → 202 jobId"),
("Create Timestamp and Wait", "create", "POST /timestamp/wait", "create + poll / wait"),
("Create Batch Timestamps", "create", "POST /timestamp/batch", "POST /api/batch/timestamp"),
("Verify Certificate", "create", "POST /verify", "POST /api/verify"),
("Find Job Status", "search", "GET /status/{jobId}", "GET /api/status/{jobId}"),
("Find Job Verification", "search (TS)", "GET /status/{jobId}/verification", "GET /api/verify/{jobId}"),
("Timestamp Completed", "hook", "POST /webhooks/subscribe", "(no Verae hook — middleware stores targetUrl)"),
]
y = 114
for i, (a, b, c, d) in enumerate(rows):
fill = PAPER if i % 2 == 0 else "#EEF2F6"
s.rect(20, y, 1060, 52, fill=fill, stroke=LINE, r=0, sw=0.6)
s.text(40, y + 32, a, size=13, fill=NAVY, weight="600")
s.text(280, y + 32, b, size=12, fill=SLATE)
s.text(400, y + 32, c, size=12, fill=TEAL_DK, weight="600")
s.text(700, y + 32, d, size=12, fill=SLATE)
y += 52
s.text(
24,
500,
"Creates return one object. Searches and hook perform return arrays. 404 on status search → [].",
size=12,
fill=MUTED,
)
s.rect(20, 520, 340, 96, fill=ZAPIER, stroke="#F0D2B8")
s.text(36, 546, "Zapier cloud", size=12, fill=ACCENT, weight="700")
s.lines(36, 566, ["scratch/veraetime", "or verae-zapier"], size=12, fill=SLATE)
s.rect(390, 520, 340, 96, fill=PUBLIC, stroke="#C5D6E8")
s.text(406, 546, "Middleware :3100", size=12, fill=TEAL, weight="700")
s.lines(406, 566, ["api_base_url / MIDDLEWARE_BASE_URL", "never api.veraetime.net from Zapier"], size=12, fill=SLATE)
s.rect(760, 520, 320, 96, fill=VERAE, stroke="#B7DCC7")
s.text(776, 546, "Verae API", size=12, fill=OK, weight="700")
s.lines(776, 566, ["JWT server-side only", "OpenAPI in scratch/our-api/"], size=12, fill=SLATE)
s.harrow(360, 390, 568)
s.harrow(730, 760, 568)
return s.save("07-operations-map.svg")
def middleware():
s = Svg(1100, 580, "Middleware internals")
s.text(24, 28, "MIDDLEWARE INTERNALS", size=11, fill=TEAL, weight="700")
s.text(24, 50, "verae-zapier-api/verae-zapier-middleware", size=20, fill=NAVY, weight="700")
s.rect(20, 80, 160, 70, fill=PAPER, stroke=NAVY)
s.text(100, 110, "GET /health", size=12, fill=NAVY, anchor="middle", weight="700")
s.lines(100, 128, ["liveness"], size=11, fill=SLATE, anchor="middle")
s.rect(200, 80, 880, 70, fill=NAVY, stroke=NAVY)
s.text(640, 110, "Express /zapier → /v1", size=16, fill=PAPER, anchor="middle", weight="700")
s.lines(640, 132, ["authenticate · rateLimit on protected routes"], size=12, fill="#CBD5E1", anchor="middle")
# public
s.rect(20, 172, 340, 180, fill=PUBLIC, stroke="#C5D6E8")
s.text(36, 198, "Public", size=13, fill=TEAL, weight="700")
s.lines(
36,
222,
[
"POST /v1/auth/login",
"GET /v1/auth/me",
"POST /v1/signup",
"GET /v1/admin/* (X-Admin-Secret)",
],
size=13,
fill=SLATE,
leading=26,
)
s.rect(380, 172, 360, 180, fill=PAPER, stroke=NAVY, sw=1.8)
s.text(396, 198, "Protected /v1", size=13, fill=NAVY, weight="700")
s.lines(
396,
222,
[
"/timestamp /timestamp/wait /batch",
"/verify",
"/status/:jobId[/verification]",
"/webhooks/subscribe|unsubscribe",
],
size=13,
fill=SLATE,
leading=26,
)
s.rect(760, 172, 320, 180, fill=PRIVATE, stroke="#E0D2B4")
s.text(776, 198, "Services + store", size=13, fill=WARN, weight="700")
s.lines(
776,
222,
[
"auth / entitlement / tenant",
"timestamp / verify / webhook",
"store.json tenants usage hooks",
"veraeClient (+ mock)",
],
size=13,
fill=SLATE,
leading=26,
)
s.rect(20, 372, 520, 180, fill=PAPER, stroke=NAVY)
s.text(36, 400, "Flags", size=13, fill=NAVY, weight="700")
s.lines(
36,
426,
[
"NATS_ENABLED=false in-process job poller (Phase 6 product path)",
"NATS_ENABLED=true JetStream workers; no in-process poller",
"MOCK_VERAE=true deterministic jobs, no live Verae",
"DEBUG_VERAE=auth,nats,jobs,webhooks,http,billing",
],
size=13,
fill=SLATE,
leading=26,
)
s.rect(560, 372, 520, 180, fill=VERAE, stroke="#B7DCC7")
s.text(576, 400, "Outbound", size=13, fill=OK, weight="700")
s.lines(
576,
426,
[
"HTTPS VERAE_API_BASE_URL (prod: api.veraetime.net)",
"NATS NATS_URL nats://127.0.0.1:4222",
"HTTPS Zapier targetUrl (webhook worker)",
"Never bind NATS to a public interface",
],
size=13,
fill=SLATE,
leading=26,
)
return s.save("08-middleware-internals.svg")
def phases():
s = Svg(1100, 520, "Implementation phase roadmap")
s.text(24, 28, "ROADMAP", size=11, fill=TEAL, weight="700")
s.text(24, 50, "TODO.md phases and gates · do not skip", size=20, fill=NAVY, weight="700")
done = {
0,
1,
2,
3,
4,
5,
6,
7,
8,
10,
11,
}
items = [
(0, "Docs"),
(1, "Debug"),
(2, "HTTP"),
(3, "Store"),
(4, "Client"),
(5, "Auth"),
(6, "Sync API"),
(7, "NATS"),
(8, "Workers"),
(9, "Wait"),
(10, "Tenancy"),
(11, "Zapier"),
(12, "E2E"),
(13, "Prod"),
(14, "Push"),
(15, "Harden"),
]
# chain 0-6
x0, y0 = 40, 100
for i, (n, lab) in enumerate(items[:7]):
x = x0 + i * 150
fill = TEAL if n in done else PAPER
tc = PAPER if n in done else NAVY
st = TEAL if n in done else NAVY
s.rect(x, y0, 130, 56, fill=fill, stroke=st)
s.text(x + 65, y0 + 24, f"{n} {lab}", size=12, fill=tc, anchor="middle", weight="700")
s.text(x + 65, y0 + 42, "gate passed" if n in done else "open", size=10, fill="#99F6E4" if n in done else MUTED, anchor="middle")
if i < 6:
s.harrow(x + 130, x + 150, y0 + 28, color=TEAL)
# rail from phase 6 down to the three successor rows
spine_x = x0 + 6 * 150 + 65
rail_x = 24
s.raw(
f''
)
s.raw(
f''
)
s.raw(
f''
)
branches = [
(40, 210, [(7, "NATS"), (8, "Workers"), (9, "Wait-via-NATS")]),
(40, 310, [(10, "Tenancy")]),
(40, 400, [(11, "Zapier app"), (12, "E2E local"), (13, "Production"), (14, "Private push"), (15, "Harden")]),
]
for bx, by, seq in branches:
s.harrow(rail_x, bx, by + 28, color=TEAL)
for i, (n, lab) in enumerate(seq):
x = bx + i * 200
fill = TEAL if n in done else ("#FEF3C7" if n == 9 else PAPER)
tc = PAPER if n in done else NAVY
st = TEAL if n in done else (WARN if n == 9 else NAVY)
s.rect(x, by, 180, 56, fill=fill, stroke=st)
s.text(x + 90, by + 24, f"{n} {lab}", size=12, fill=tc, anchor="middle", weight="700")
note = "gate passed" if n in done else ("open blocker" if n == 9 else "next")
s.text(x + 90, by + 42, note, size=10, fill="#99F6E4" if n in done else MUTED, anchor="middle")
if i < len(seq) - 1:
s.harrow(x + 180, x + 200, by + 28, color=TEAL if n in done else LINE)
s.text(24, 500, "Phase 9 is the open blocker before multi-instance wait. Phase 12 needs 8 + 10 + 11.", size=12, fill=MUTED)
return s.save("09-phase-roadmap.svg")
def workspace():
s = Svg(1100, 560, "Research workspace and integration path")
s.text(24, 28, "THIS WORKSPACE", size=11, fill=TEAL, weight="700")
s.text(24, 50, "How Grok, CLIs, Mongo, and the stack fit together", size=20, fill=NAVY, weight="700")
s.rect(20, 80, 340, 220, fill=PAPER, stroke=NAVY)
s.text(36, 108, "Laptop · this repo", size=13, fill=NAVY, weight="700")
s.lines(
36,
132,
[
"getting-started.md / .pdf",
"scratch/veraetime",
"verae-zapier-api/",
"docs/diagrams/",
"/zapier-build skill",
"scripts/restart-grok.sh",
],
size=13,
fill=SLATE,
leading=24,
)
s.rect(380, 80, 340, 220, fill=PUBLIC, stroke="#C5D6E8")
s.text(396, 108, "Local runtime", size=13, fill=TEAL, weight="700")
s.lines(
396,
132,
[
"zapier-platform 19.1.0",
"zapier-sdk 0.77.1",
"middleware :3100",
"optional NATS :4222",
"build + validate (no login)",
"register / push need ~/.zapierrc",
],
size=13,
fill=SLATE,
leading=24,
)
s.rect(740, 80, 340, 220, fill=PRIVATE, stroke="#E0D2B4")
s.text(756, 108, "NS1 Mongo (tunneled)", size=13, fill=WARN, weight="700")
s.lines(
756,
132,
[
"ssh -L 27017:127.0.0.1:27017 ns1",
"db zapier",
"apps · templates · help",
"platform_reference",
"never 70.88.205.138:27017 public",
"creds in ~/.mcp-env only",
],
size=13,
fill=SLATE,
leading=24,
)
s.harrow(360, 380, 190)
s.harrow(720, 740, 190, label="tunnel")
s.rect(20, 324, 520, 208, fill=ZAPIER, stroke="#F0D2B8")
s.text(36, 352, "Publish path", size=13, fill=ACCENT, weight="700")
s.lines(
36,
378,
[
"1 validate locally against middleware",
"2 zapier-platform login (browser)",
"3 public HTTPS middleware (Phase 13)",
"4 register + push private version (Phase 14)",
"5 one human E2E Zap, then consider listing",
],
size=13,
fill=SLATE,
leading=26,
)
s.rect(560, 324, 520, 208, fill=PAPER, stroke=NAVY)
s.text(576, 352, "Consume path (optional, not Verae publish)", size=13, fill=NAVY, weight="700")
s.lines(
576,
378,
[
"zapier-sdk login → call existing apps",
"Hosted MCP mcp.zapier.com/api/v1/connect",
"discover → enable → inspect → execute",
"writes need explicit approval; 2 tasks each",
"do not mix with zapier-platform",
],
size=13,
fill=SLATE,
leading=26,
)
return s.save("10-workspace-integration.svg")
def billing():
s = Svg(1100, 620, "Zapier and Verae billing layers")
s.text(24, 28, "BILLING", size=11, fill=TEAL, weight="700")
s.text(24, 50, "Two meters · Zapier tasks + Verae timestamps", size=20, fill=NAVY, weight="700")
s.text(
24,
72,
"Publishing the Verae app is free. The customer’s Zapier plan is not Verae’s invoice.",
size=12,
fill=MUTED,
)
rows = [
(88, ZAPIER, ACCENT, "A Zapier customer plan", "Tasks / month (or Enterprise annual pool)", "Free 100 · Pro from 750 · Team from 2k · Ent custom"),
(178, PUBLIC, TEAL, "B Task multipliers", "1 = typical action (Create Timestamp)", "MCP execute = 2 AI Advanced = 3 Premium = 5"),
(268, PAPER, NAVY, "C Overflow", "Pay-per-task on paid plans", "Annual 1.25x · monthly 2.5x · cap 3x then pause"),
(358, VERAE, OK, "D Verae middleware plan", "Timestamps / verifications / batch / RPM", "free 50 · starter 500 · pro 5k · enterprise contract"),
(448, PRIVATE, WARN, "E Partner / embed", "Directory publish = $0 to Verae", "White Label: Zapier bills the product co. (usage)"),
(538, "#F1F5F9", SLATE, "F Add-ons (not tasks)", "Agents = activities Chatbots = seat/tier", "Do not mix into the Zapier task estimate"),
]
for y, fill, stroke, title, line1, line2 in rows:
s.rect(20, y, 1060, 80, fill=fill, stroke=stroke, r=10)
s.text(40, y + 28, title, size=14, fill=NAVY, weight="700")
s.text(40, y + 50, line1, size=12, fill=SLATE)
s.text(40, y + 68, line2, size=12, fill=SLATE)
return s.save("11-zapier-billing.svg")
def peergos_storage():
s = Svg(1100, 680, "Timestamped files on Peergos with tiered IPFS and cold retrieve")
s.text(24, 28, "CLIENT PATTERN", size=11, fill=TEAL, weight="700")
s.text(24, 50, "Timestamp · metadata · Peergos · pin cache · cold retrieve", size=18, fill=NAVY, weight="700")
s.text(
24,
70,
"Keep the index hot. Do not keep every CID live on a gateway.",
size=12,
fill=MUTED,
)
# write path
s.rect(20, 88, 1060, 200, fill=PUBLIC, stroke="#C5D6E8", r=12)
s.text(36, 112, "WRITE PATH (Zapier orchestrates; Verae timestamps the hash)", size=12, fill=TEAL, weight="700")
boxes = [
(40, "1 Source", ["Drive / mail /", "Peergos outbox"]),
(250, "2 Hash", ["SHA-256 or", "envelope JSON"]),
(460, "3 Verae", ["Create Timestamp", "jobId + hook"]),
(670, "4 Store", ["Peergos e2e file", "optional hot pin"]),
(880, "5 Index", ["cid · jobId · class", "always on"]),
]
for x, title, lines in boxes:
s.rect(x, 128, 190, 132, fill=PAPER, stroke=NAVY)
s.text(x + 95, 156, title, size=13, fill=NAVY, anchor="middle", weight="700")
s.lines(x + 95, 182, lines, size=12, fill=SLATE, anchor="middle", leading=20)
for x in (230, 440, 650, 860):
s.harrow(x, x + 20, 194)
# tiers
s.rect(20, 308, 340, 200, fill=ZAPIER, stroke="#F0D2B8")
s.text(36, 336, "HOT · cache pin", size=13, fill=ACCENT, weight="700")
s.lines(
36,
362,
["Kubo / Pinata / Filebase", "Pinning Services API", "TTL = pinnedUntil", "drop when cold copy exists"],
size=13,
fill=SLATE,
leading=24,
)
s.rect(380, 308, 340, 200, fill=VERAE, stroke="#B7DCC7")
s.text(396, 336, "WARM · Peergos", size=13, fill=OK, weight="700")
s.lines(
396,
362,
["e2e encrypted IPFS blocks", "user capabilities / sharing", "host cannot read plaintext", "not a public CDN"],
size=13,
fill=SLATE,
leading=24,
)
s.rect(740, 308, 340, 200, fill=PRIVATE, stroke="#E0D2B4")
s.text(756, 336, "COLD · retrieve on demand", size=13, fill=WARN, weight="700")
s.lines(
756,
362,
["S3 Glacier IR / Flexible / Deep", "or Filecoin / B2 / Storj", "Iceberg = catalog, not bytes", "restore API → short re-pin"],
size=13,
fill=SLATE,
leading=24,
)
s.rect(20, 528, 1060, 128, fill=PAPER, stroke=NAVY, sw=1.8)
s.text(36, 556, "READ PATH GET /ipfs/{cid} or gateway miss", size=13, fill=NAVY, weight="700")
s.lines(
36,
582,
[
"1 look up index by cid (always-on). 2 pin-hot hit → serve. 3 Peergos capability → fetch encrypted blocks.",
"4 storageClass glacier-* → StartRestore / vendor retrieve → 202 + Retry-After → rehydrate → optional short pin → serve.",
"Never walk every pin provider. Never put raw files or Verae JWTs in the Zap.",
],
size=13,
fill=SLATE,
leading=22,
)
return s.save("12-peergos-ipfs-tiered-storage.svg")
def main():
high_level()
security()
auth()
flow_async()
flow_wait()
nats()
operations()
middleware()
phases()
workspace()
billing()
peergos_storage()
readme = OUT / "README.md"
readme.write_text(
"""# Architecture diagrams
SVG sources for [getting-started.md](../../getting-started.md) and `getting-started.pdf`.
| File | What it shows |
|------|----------------|
| [01-high-level-architecture.svg](01-high-level-architecture.svg) | End-to-end stack: Zapier cloud, middleware, NATS, workers, Verae, REST Hooks |
| [02-security-boundaries.svg](02-security-boundaries.svg) | Public HTTPS vs private NATS / store / Verae JWT |
| [03-auth-two-hop.svg](03-auth-two-hop.svg) | User → middleware tokens; middleware → Verae JWT |
| [04-flow-async-timestamp.svg](04-flow-async-timestamp.svg) | Create Timestamp + hook sequence |
| [05-flow-wait.svg](05-flow-wait.svg) | Create and Wait (in-process vs Phase 9 NATS) |
| [06-nats-topology.svg](06-nats-topology.svg) | Streams, subjects, consumers, ack rules |
| [07-operations-map.svg](07-operations-map.svg) | Zapier nouns → `/zapier/v1` → Verae |
| [08-middleware-internals.svg](08-middleware-internals.svg) | Express routes, store, flags |
| [09-phase-roadmap.svg](09-phase-roadmap.svg) | Gates 0–15 |
| [10-workspace-integration.svg](10-workspace-integration.svg) | Repo, CLIs, Mongo tunnel, publish vs consume |
| [11-zapier-billing.svg](11-zapier-billing.svg) | Zapier task plans vs Verae timestamp quotas vs embed |
| [12-peergos-ipfs-tiered-storage.svg](12-peergos-ipfs-tiered-storage.svg) | Timestamp + Peergos + pin cache + Glacier retrieve-on-demand |
Regenerate:
```bash
python3 scripts/generate-diagrams.py
python3 scripts/generate-getting-started-pdf.py
```
""",
encoding="utf-8",
)
print(f"wrote {readme}")
if __name__ == "__main__":
main()