System Topology
Complementary views: Infrastructure & Deployment covers the operator-facing runbook for the same machines; this page is the architect-facing map of how the services fit together. Secret Hygiene documents how configuration lands on each service; Email Deliverability covers the notification path specifically.
Why this exists
In April 2026 the platform grew from "two canisters and a VPS" into a three-machine topology with a unified API gateway, two new off-chain microservices, a private network bridging the machines, and a cross-domain auth bridge between the DAO and FounderyOS brands. None of that was captured in a single place — the developer-ops view lived in developer/infrastructure.md, the service contracts lived in docs/api/*, and the physical topology lived in CLAUDE.md. Onboarding engineers had to stitch three views together to answer "what runs where." This page is the stitched view. It does not replace any of the above; it orients new readers before they drill into them.
Every concrete value on this page (IP, port, namespace, canister ID) traces back to an authoritative source — CLAUDE.md, sprint-status, or an implementation-artifact story. When those sources change, update this page from them rather than inventing new values.
Three machines, one platform
The platform spans three machines and IC mainnet. Public traffic enters through Cloudflare DNS; most DAO traffic terminates on IC boundary nodes, while FounderyOS and platform-service traffic terminates at Traefik on AX42-U. The oracle-bridge VPS and AX42-U exchange backend traffic over a Hetzner vSwitch rather than the public internet.
graph TB
subgraph public["Public internet"]
user["Browser / CLI client"]
end
subgraph cf["Cloudflare (DNS + TLS edge)"]
dns["helloworlddao.com<br/>founderyos.dev<br/>All records DNS-only"]
end
subgraph ic["IC mainnet (canister product layer)"]
icNodes["Boundary nodes<br/>icp0.io / ic0.app<br/>15 prod canisters"]
end
subgraph vps["Oracle-bridge VPS (Hetzner Cloud — Helsinki)"]
vpsPub["Public: 65.21.149.226"]
vpsPriv["Private: 10.0.0.2/32<br/>(vSwitch iface enp7s0, MTU 1450)"]
obStg["oracle-bridge-staging :8787<br/>Docker"]
obProd["oracle-bridge-production :8788<br/>Docker"]
end
subgraph ax["AX42-U (Hetzner Robot — Helsinki)"]
axPub["Public: 157.180.13.84"]
axPriv["Private: 10.0.1.3/24<br/>(VLAN 4010 on enp7s0.4010, MTU 1400)"]
traefik["Traefik ingress<br/>kube-system namespace"]
pgw["payment-gateway :3200<br/>ns: platform"]
authz["token-authz<br/>(ForwardAuth) ns: platform"]
ns["notification-service :3100<br/>ns: founderyos"]
fosapi["founderyos-api :8000<br/>ns: founderyos"]
fosdash["founderyos-dashboard<br/>static nginx ns: founderyos"]
end
subgraph vswitch["Hetzner vSwitch 80388 — VLAN 4010 — 10.0.0.0/16"]
vsw["Private backbone<br/>(ICMP dropped at gateway;<br/>TCP/UDP forwarded)"]
end
subgraph ext["External managed services"]
neon["Neon Postgres<br/>(oracle-bridge DB + payment-gateway DB,<br/>separate databases)"]
resend["Resend (transactional email)"]
stripe["Stripe / Stripe Connect"]
didit["Didit KYC"]
end
user -->|HTTPS| dns
dns -->|DAO suites| icNodes
dns -->|apis.*.helloworlddao.com| axPub
dns -->|staging-oracle.*<br/>oracle.*| vpsPub
axPub --> traefik
traefik --> pgw
traefik --> ns
traefik --> fosapi
traefik --> authz
traefik -.->|/oracle via vSwitch| vsw
vpsPriv --- vsw
axPriv --- vsw
vsw --> obStg
vsw --> obProd
pgw --> neon
obStg --> neon
pgw --> stripe
ns --> resend
obStg -.->|KYC adapter| didit
obStg -.->|signed HTTP outcalls| icNodesKey boundaries:
- Public vs private. Every cross-machine backend call that can go through the vSwitch should.
apis.*.helloworlddao.comterminates at Traefik and proxies/oracle/*requests over the vSwitch — no public-internet round-trip for gateway-to-oracle-bridge traffic once oracle-bridge finishes binding to its private-net IP. - DNS-only. Every DNS record for both brands is DNS-only (grey cloud). Cloudflare proxying is deliberately off — IC boundary nodes reject proxied records and DKIM verification fails under proxy.
- Two Neon databases. oracle-bridge and payment-gateway own separate Neon Postgres databases. They are not sharing a schema. Migrations live in each service's repo.
Service inventory per machine
The table is the canonical inventory; the grouping diagram below visualizes the same services by machine → namespace. When these diverge, the table wins.
graph TB
subgraph vpsGroup["VPS — host Docker"]
obS["oracle-bridge-staging :8787"]
obP["oracle-bridge-production :8788"]
end
subgraph axGroup["AX42-U — k3s"]
subgraph ksys["ns: kube-system"]
tf["Traefik :80/:443"]
end
subgraph plat["ns: platform"]
pg["payment-gateway :3200"]
tokz["token-authz (ForwardAuth)"]
hl["health (nginx 200 ok)"]
xns["founderyos-api-xns<br/>(ExternalName shim)"]
end
subgraph fos["ns: founderyos"]
fa["founderyos-api :8000"]
fd["founderyos-dashboard (nginx)"]
nsv["notification-service :3100"]
end
end
subgraph icGroup["IC mainnet"]
daoC["DAO backend (8): dom-token, governance,<br/>membership, treasury, user-service,<br/>identity-gateway, auth-service (shell), airdrop"]
suiteC["DAO suites (6): marketing, dao, dao-admin,<br/>governance, otter-camp, think-tank"]
contentC["Content (2): blog, docs"]
end| Machine | Namespace / surface | Service | Port | Purpose | Authoritative source |
|---|---|---|---|---|---|
| VPS | host Docker | oracle-bridge-staging | 8787 | Session store, canister signer, KYC adapter, governance + document import, email dispatch caller (deep dive) | MEMORY VPS section |
| VPS | host Docker | oracle-bridge-production | 8788 | Production twin (deep dive) | MEMORY VPS section |
| AX42-U | kube-system | Traefik | 80/443 | TLS termination, path routing, middleware chain | PLATFORM-006.2 |
| AX42-U | platform | payment-gateway | 3200 | Stripe + Stripe Connect + ICP/DOM provider factory, Neon-backed payments DB | PLATFORM-007.1 |
| AX42-U | platform | token-authz | 3000 (ClusterIP) | Node ForwardAuth verifier for service-token-auth middleware | PLATFORM-006.4 |
| AX42-U | platform | health | 80 (ClusterIP) | nginx returning 200 ok for /health | PLATFORM-006.2 |
| AX42-U | platform | founderyos-api-xns | — | ExternalName shim pointing at founderyos-api.founderyos.svc.cluster.local so Traefik Ingress can reach cross-namespace | PLATFORM-006.3 |
| AX42-U | founderyos | founderyos-api | 8000 | Python/FastAPI: cross-domain auth consumer, FOS CRUD, governance-proposal document editor (deep dive) | platform-003-1 |
| AX42-U | founderyos | founderyos-dashboard | 80 (static) | Vite SPA served by nginx — static bundle, no runtime env | PLATFORM-009.3 |
| AX42-U | founderyos | notification-service | 3100 | Resend-backed transactional email, 14 dual-brand templates, CAN-SPAM footer, digest+unsubscribe | PLATFORM-002.2 |
| IC mainnet | canister | dom-token, governance, membership, treasury, user-service, identity-gateway, auth-service (shell), airdrop | — | DAO on-chain product layer (8 backend canisters) | CLAUDE.md canister list |
| IC mainnet | canister | marketing-suite, dao-suite, dao-admin-suite, governance-suite, otter-camp-suite, think-tank-suite | — | DAO frontend suites (6 asset canisters) | BL-111 Tier 2 |
| IC mainnet | canister | blog, docs | — | Content canisters | BL-111 Tier 2 |
auth-service is DECOMMISSIONED (WASM uninstalled 2026-04-11) but the canister ID is preserved for future use; authentication runs out of PostgreSQL sessions on oracle-bridge.
API gateway routing
Traefik on AX42-U unifies the backend surface under two hostnames: apis.helloworlddao.com (production) and staging-apis.helloworlddao.com (staging). Each path has a dedicated Ingress, and each applies a subset of the middleware chain. The chain is shared across routes; path-specific middleware (like service-token-auth ForwardAuth) is added on top.
graph LR
client["Client"] --> edge["Traefik :443<br/>Let's Encrypt HTTP-01"]
edge --> mw["Middleware chain<br/>CORS · rate-limit · security-headers<br/>strip-* · retry"]
mw -->|/health| hl["health (platform)<br/>nginx 200 ok"]
mw -->|/fos/*<br/>strip /fos| xns["founderyos-api-xns<br/>(ExternalName shim)"]
xns --> fosapi["founderyos-api :8000"]
mw -->|/oracle/*<br/>strip /oracle,<br/>via vSwitch 10.0.0.2| ob["oracle-bridge :8787 / :8788"]
mw -->|/auth/*| auth_ph["placeholder 503<br/>(filled by PLATFORM-003)"]
mw -->|/notify/* + token| fa["service-token-auth<br/>ForwardAuth → token-authz"]
fa -->|401 no token| client
fa -->|authorized| ns_svc["notification-service :3100"]Middleware responsibility map:
| Middleware | Applied to | Purpose |
|---|---|---|
| CORS | all routes | Origin allowlist per environment |
| rate-limit | all routes | Token-bucket, per-IP |
| security-headers | all routes | HSTS, frame-options, referrer-policy, CSP |
| strip-prefix | /fos/*, /oracle/*, /notify/* | Remove path prefix before hitting backend |
| service-token-auth (ForwardAuth) | /notify/* today; extensible | Constant-time compare of Authorization: Bearer <token> against k8s service-tokens Secret via token-authz sidecar |
| retry | all routes | 3 retries on 502/503 with backoff |
The /auth/* path is a placeholder 503 today; PLATFORM-003's cross-domain endpoints land there once the routing story promotes them. /oracle/* currently depends on oracle-bridge rebinding its listener to the vSwitch IP — the gateway Ingress is fully configured but the backend returns 502 until oracle-bridge listens on 10.0.0.2.
Service tokens live in k8s Secret service-tokens in the platform namespace with keys TOKEN_NOTIFICATION_SERVICE, TOKEN_PAYMENT_GATEWAY, TOKEN_FOUNDERYOS_API, TOKEN_ORACLE_BRIDGE. They are rotated via gh secret set followed by kubectl apply + rollout restart and are never committed to git.
Cross-domain auth bridge
Users authenticated on one brand (portal.helloworlddao.com) sometimes need to land on the other brand (staging.founderyos.dev) without re-logging in. PLATFORM-003 wired a one-time-token bridge: the origin brand mints a token via oracle-bridge, the destination brand consumes it, and the destination brand issues its own session cookie. The token is single-use, 30-second TTL, and audience-bound to a target domain allowlist.
sequenceDiagram
autonumber
participant U as User (browser)
participant D as dao-suite (portal.helloworlddao.com)
participant OB as oracle-bridge
participant FD as founderyos-dashboard (staging.founderyos.dev)
participant FA as founderyos-api
U->>D: Click "Switch to FOS"
D->>OB: POST /api/auth/cross-domain-token<br/>(session cookie + CSRF)
Note over OB: Generate 32-byte token,<br/>SHA-256 hash in DB,<br/>30s TTL, target_domain=founderyos.dev
OB-->>D: { token, expires_at, target_domain, entry_product }
D->>U: 302 Location:<br/>https://staging.founderyos.dev/auth/cross-domain-login?token=...
U->>FD: GET /auth/cross-domain-login
FD->>FA: POST /api/v1/auth/cross-domain-login { token }
FA->>OB: POST /api/auth/exchange-token<br/>Bearer TOKEN_FOUNDERYOS_API
Note over OB: Atomic UPDATE ... WHERE used_at IS NULL<br/>RETURNING → single-use enforced
OB-->>FA: Full 9-field profile contract (see below)
Note over FA: Find-or-create User by email,<br/>mint session cookie for .founderyos.dev
FA-->>FD: Set-Cookie: session=...; Domain=.founderyos.dev
FD-->>U: 302 Location: /dashboard (logged in)The exchange-token response body is the locked consumer interface — all downstream PLATFORM-003 work mocks against these exact field names. Changing them is a breaking change across oracle-bridge, founderyos-api, and dao-suite.
{
"user_id": "<uuid>",
"email": "<email>",
"display_name": "<str|null>",
"ic_principal": "<str|null>",
"roles": ["<str>", ...],
"entry_product": "dao" | "fos" | "lighthouse",
"target_domain": "<str>",
"issued_at": "<ms-epoch int>",
"expires_at": "<ms-epoch int>"
}Failure modes on the consumer collapse to a single 401 so that oracle-bridge's distinct rejections (404 not-found, 410 expired, 409 already-used, 403 audience-mismatch) are not distinguishable by a browser attacker. This is deliberate; do not plumb the oracle-bridge error code through to the user-visible response. The full field-level rationale lives in docs/api/cross-domain-auth.md and the auth@0.14.0 helper navigateCrossDomain() is the sanctioned client-side caller.
Payment and notification data flow
payment-gateway is a provider factory — /api/v1/checkout/create accepts a provider-agnostic request and routes to Stripe, Stripe Connect, or the ICP/DOM provider based on the provider field. On successful capture, it calls notification-service to send a receipt. notification-service routes by brand: DAO brand sends from notifications.helloworlddao.com, FOS brand sends from notifications.founderyos.dev. Both go through the same Resend account; subdomain-level DKIM records separate them. The same pattern handles welcome emails, membership confirmations, renewal receipts, and password resets.
graph LR
caller["oracle-bridge<br/>or founderyos-api"]
caller -->|Bearer TOKEN_PAYMENT_GATEWAY| pgw["payment-gateway :3200"]
pgw --> factory["Provider factory<br/>(select by payment type)"]
factory -->|fiat| stripe["StripeProvider<br/>→ stripe.com /checkout/sessions"]
factory -->|marketplace| connect["StripeConnectProvider<br/>→ stripe.com /accounts + /transfers"]
factory -->|crypto| icp["IcpProvider<br/>→ dom-token canister<br/>(quote 60s, +/-2% slippage)"]
pgw --> db[("Neon Postgres<br/>payments / refunds / fee_splits<br/>webhook_events")]
pgw -->|on success| ns["notification-service :3100"]
ns --> branding["Brand router<br/>domain → From address"]
branding -->|helloworlddao.com| resend_dao["Resend<br/>notifications.helloworlddao.com"]
branding -->|founderyos.dev| resend_fos["Resend<br/>notifications.founderyos.dev"]
resend_dao --> mxdao["SES bounce MX<br/>send.notifications.helloworlddao.com"]
resend_fos --> mxfos["SES bounce MX<br/>send.notifications.founderyos.dev"]Service-token auth applies to both hops: caller → payment-gateway uses TOKEN_PAYMENT_GATEWAY; payment-gateway → notification-service uses TOKEN_NOTIFICATION_SERVICE. Both tokens live in the same k8s service-tokens Secret and rotate under the secret-hygiene rotation procedure. The IcpProvider's 60-second quote expiry and ±2% slippage gate prevent the usual crypto-checkout griefing where the DOM/USD price moves between quote and capture — a stale quote fails closed rather than settling at a bad price.
Notification templates that trigger email include a CAN-SPAM footer and an unsubscribe link on marketing mail (digests); transactional mail (receipts, password resets) deliberately omits unsubscribe. Digest preferences live in oracle-bridge's digest_preferences table and are surfaced via /api/v1/digest/preferences for user management.
How to update these diagrams
When any of the underlying topology facts change — new service, new namespace, new gateway path, new cross-machine edge — update this page and its authoritative source in the same PR. The authoritative sources, in order of canonicality:
- Canister IDs and service IDs —
CLAUDE.mdandbmad-artifacts/implementation-artifacts/sprint-status.yaml. - Machine topology + private network — CLAUDE.md §Private Network + §Platform API Gateway.
- Gateway routing —
ops-infra/k8s/platform-gateway/manifests +ops-infra/runbooks/api-gateway-add-service.md. - Cross-domain auth contract —
bmad-artifacts/implementation-artifacts/platform-003-1-cross-domain-token-exchange.md(field list is load-bearing — never rename without a migration plan). - Payment + notification service contracts —
docs/api/payment-gateway.md+docs/api/notification-service.md.
This page is a view over those sources, not a primary source. Diagrams drift; your PR reviewer is not going to cross-reference every port number. When in doubt, cite the authoritative source inline next to the changed value.
References
| Reference | Purpose |
|---|---|
| Infrastructure & Deployment | Operator-facing runbook: VPS Docker management, DNS sync, gateway bootstrap, kubectl access |
| oracle-bridge VPS | oracle-bridge deployment topology, env-var contract, PEM/principal auth boundaries, deploy flow |
| Secret Hygiene | Three env-var patterns, drift-detection cron, rotation procedure |
| Email Deliverability | Resend DKIM/SPF/DMARC per brand, staged DMARC rollout, troubleshooting |
docs/api/cross-domain-auth.md | Full 9-field contract, error shapes, CSRF + session auth details |
docs/api/payment-gateway.md | Stripe + Stripe Connect + ICP/DOM provider surfaces, webhook shapes, refund lifecycle |
docs/api/notification-service.md | 14 templates, CAN-SPAM footer, digest preferences, error-body shape |
bmad-artifacts/implementation-artifacts/platform-006-1-hetzner-vswitch.md | Private-network provisioning story |
bmad-artifacts/implementation-artifacts/platform-003-1-cross-domain-token-exchange.md | Cross-domain mint/exchange story (contract source of truth) |
ops-infra/runbooks/api-gateway-add-service.md | Template-based walkthrough for adding a new gateway-routed service |