Skip to content

Checking access...

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.

mermaid
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| icNodes

Key boundaries:

  • Public vs private. Every cross-machine backend call that can go through the vSwitch should. apis.*.helloworlddao.com terminates 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.

mermaid
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
MachineNamespace / surfaceServicePortPurposeAuthoritative source
VPShost Dockeroracle-bridge-staging8787Session store, canister signer, KYC adapter, governance + document import, email dispatch caller (deep dive)MEMORY VPS section
VPShost Dockeroracle-bridge-production8788Production twin (deep dive)MEMORY VPS section
AX42-Ukube-systemTraefik80/443TLS termination, path routing, middleware chainPLATFORM-006.2
AX42-Uplatformpayment-gateway3200Stripe + Stripe Connect + ICP/DOM provider factory, Neon-backed payments DBPLATFORM-007.1
AX42-Uplatformtoken-authz3000 (ClusterIP)Node ForwardAuth verifier for service-token-auth middlewarePLATFORM-006.4
AX42-Uplatformhealth80 (ClusterIP)nginx returning 200 ok for /healthPLATFORM-006.2
AX42-Uplatformfounderyos-api-xnsExternalName shim pointing at founderyos-api.founderyos.svc.cluster.local so Traefik Ingress can reach cross-namespacePLATFORM-006.3
AX42-Ufounderyosfounderyos-api8000Python/FastAPI: cross-domain auth consumer, FOS CRUD, governance-proposal document editor (deep dive)platform-003-1
AX42-Ufounderyosfounderyos-dashboard80 (static)Vite SPA served by nginx — static bundle, no runtime envPLATFORM-009.3
AX42-Ufounderyosnotification-service3100Resend-backed transactional email, 14 dual-brand templates, CAN-SPAM footer, digest+unsubscribePLATFORM-002.2
IC mainnetcanisterdom-token, governance, membership, treasury, user-service, identity-gateway, auth-service (shell), airdropDAO on-chain product layer (8 backend canisters)CLAUDE.md canister list
IC mainnetcanistermarketing-suite, dao-suite, dao-admin-suite, governance-suite, otter-camp-suite, think-tank-suiteDAO frontend suites (6 asset canisters)BL-111 Tier 2
IC mainnetcanisterblog, docsContent canistersBL-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.

mermaid
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:

MiddlewareApplied toPurpose
CORSall routesOrigin allowlist per environment
rate-limitall routesToken-bucket, per-IP
security-headersall routesHSTS, frame-options, referrer-policy, CSP
strip-prefix/fos/*, /oracle/*, /notify/*Remove path prefix before hitting backend
service-token-auth (ForwardAuth)/notify/* today; extensibleConstant-time compare of Authorization: Bearer <token> against k8s service-tokens Secret via token-authz sidecar
retryall routes3 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.

mermaid
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.

json
{
  "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.

mermaid
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:

  1. Canister IDs and service IDsCLAUDE.md and bmad-artifacts/implementation-artifacts/sprint-status.yaml.
  2. Machine topology + private network — CLAUDE.md §Private Network + §Platform API Gateway.
  3. Gateway routingops-infra/k8s/platform-gateway/ manifests + ops-infra/runbooks/api-gateway-add-service.md.
  4. Cross-domain auth contractbmad-artifacts/implementation-artifacts/platform-003-1-cross-domain-token-exchange.md (field list is load-bearing — never rename without a migration plan).
  5. Payment + notification service contractsdocs/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

ReferencePurpose
Infrastructure & DeploymentOperator-facing runbook: VPS Docker management, DNS sync, gateway bootstrap, kubectl access
oracle-bridge VPSoracle-bridge deployment topology, env-var contract, PEM/principal auth boundaries, deploy flow
Secret HygieneThree env-var patterns, drift-detection cron, rotation procedure
Email DeliverabilityResend DKIM/SPF/DMARC per brand, staged DMARC rollout, troubleshooting
docs/api/cross-domain-auth.mdFull 9-field contract, error shapes, CSRF + session auth details
docs/api/payment-gateway.mdStripe + Stripe Connect + ICP/DOM provider surfaces, webhook shapes, refund lifecycle
docs/api/notification-service.md14 templates, CAN-SPAM footer, digest preferences, error-body shape
bmad-artifacts/implementation-artifacts/platform-006-1-hetzner-vswitch.mdPrivate-network provisioning story
bmad-artifacts/implementation-artifacts/platform-003-1-cross-domain-token-exchange.mdCross-domain mint/exchange story (contract source of truth)
ops-infra/runbooks/api-gateway-add-service.mdTemplate-based walkthrough for adding a new gateway-routed service

Hello World Co-Op DAO