12 KiB
Sankofa Phoenix — consolidated non-chain frontend and API hub
Status: Architecture proposal (resource conservation)
Last updated: 2026-04-13
LAN status (operator): Tier-1 API hub nginx on VMID 7800 listening http://192.168.11.50:8080 (sankofa-phoenix-api-hub.service). Apollo (Fastify) binds 127.0.0.1:4000 only (HOST=127.0.0.1 in /opt/sankofa-api/.env; apply: scripts/deployment/ensure-sankofa-phoenix-apollo-bind-loopback-7800.sh). NPM → :8080 + WebSocket upgrades is live for phoenix.sankofa.nexus (fleet 2026-04-13). Install hub: scripts/deployment/install-sankofa-api-hub-nginx-on-pve.sh with PROXMOX_OPS_APPLY=1 + PROXMOX_OPS_ALLOWED_VMIDS=7800. Readiness: scripts/verify/verify-sankofa-consolidated-hub-lan.sh, hub GraphQL scripts/verify/smoke-phoenix-api-hub-lan.sh, WebSocket upgrade scripts/verify/smoke-phoenix-graphql-wss-public.sh (pnpm run verify:phoenix-graphql-wss), graphql-ws handshake pnpm run verify:phoenix-graphql-ws-subscription, hub /graphql-ws headers scripts/deployment/ensure-sankofa-phoenix-api-hub-graphql-ws-proxy-headers-7800.sh.
r630-01 load goal: consolidating frontends and moving hub LXCs to quieter nodes is what reduces guest count and hypervisor pressure — see SANKOFA_R630_01_CONSOLIDATION_AND_HUB_PLACEMENT_GOAL.md.
Ecosystem shape (non-chain, hyperscaler-style): NON_CHAIN_ECOSYSTEM_HYPERSCALER_STYLE_MODEL.md (cell types, edge vs chain plane).
Scope: Non-blockchain Sankofa / Phoenix surfaces only. Out of scope: Chain 138 explorer, Besu/RPC, CCIP/relayers, token-aggregation compute — keep those on dedicated LXCs/VMs per existing runbooks.
1. Problem
Today, multiple LXCs/VMIDs often run one primary workload each (portal, corporate web, Phoenix API, DBIS API, gov dev shells, etc.). Each Node or Next process carries base RAM (V8 heap, file watchers in dev, separate copies of dependencies). Nginx-only static sites are cheap; many separate Node servers are not.
This document defines a consolidated runtime that:
- Puts all non-chain web frontends behind one LAN endpoint (one LXC or one Docker host — your choice), using static-first or one Node process where SSR is required.
- Puts all Phoenix-facing backend traffic behind one logical API (one public origin and port): GraphQL (current Phoenix), REST/BFF (
dbis_coreand future middleware), health, and webhooks.
Canonical surface taxonomy remains SANKOFA_PHOENIX_CANONICAL_BOUNDARIES_AND_TAXONOMY.md. Consolidation changes packaging, not the names of visitor vs client vs operator paths.
2. Single “web hub” LXC (frontends)
2.1 Option A — Static-first (lowest RAM)
When: Marketing pages, IRU/marketplace after static export, simple entity microsites, post-login SPAs that call the API hub only.
- Build:
next buildwithoutput: 'export'where compatible (no server-only APIs on those routes). - Serve: nginx with one
serverper FQDN (server_name) or one server +map $host $site_root→ differentrootdirectories under/var/www/.... - NPM: All affected FQDNs point to the same upstream
http://<WEB_HUB_IP>:80.
Tradeoff: NextAuth / OIDC callback flows and server components need either client-only OIDC (PKCE) against Keycloak or a small SSR slice (see option B).
2.2 Option B — One Node process for all SSR Next apps (moderate RAM)
When: Portal (portal.sankofa.nexus), admin, or any app that must keep getServerSideProps, NextAuth, or middleware.
- Monorepo (e.g. Turborepo/Nx): multiple Next “apps” merged into one deployable using:
- Next multi-zone (primary + mounted sub-apps), or
- Single Next 15 app with
middleware.tsrewriting byHost, or - Single custom server (less ideal) proxying to child apps — avoid unless necessary.
Outcome: One node process (or one standalone output + one PID supervisor) on one port (e.g. 3000). Nginx in front optional (TLS termination usually at NPM).
2.3 Option C — Hybrid (practical migration)
- nginx: static corporate apex, static entity sites, docs mirrors.
- One Node: portal + Phoenix “shell” that must stay dynamic.
Still fewer LXCs than “one LXC per microsite.”
2.4 What stays out of this box
- Blockscout / explorer stacks
info.defi-oracle.io, MEV GUI, relay health — separate nginx LXCs as today unless you explicitly merge static mirrors only- Keycloak — keep separate (identity is its own security domain)
3. Single consolidated API (Phoenix hub)
3.1 Responsibilities
| Path family | Today (typical) | Hub role |
|---|---|---|
/graphql, /graphql-ws |
Phoenix VMID 7800 :4000 | Reverse proxy to existing Apollo until merged in code |
/api/v1/*, /api-docs |
dbis_core (e.g. :3000) |
Reverse proxy mount |
/health |
Multiple | Aggregate (optional): hub returns 200 only if subgraphs pass |
| Future BFF | N/A | Implement in hub (session, composition, rate limits) |
Naming: Introduce an internal service name e.g. sankofa-phoenix-hub-api. Public FQDN can remain phoenix.sankofa.nexus or split to api.phoenix.sankofa.nexus for clarity; NPM decides.
3.2 Implementation tiers (phased)
Tier 1 — Thin hub (fastest, lowest risk)
One process: nginx or Caddy. Typical production pattern: hub on its own LXC or same CT as Apollo — proxy_pass Phoenix to 127.0.0.1:4000 when colocated, and dbis_core to IP_DBIS_API:3000 (LAN) as in install-sankofa-api-hub-nginx-on-pve.sh. Single public port (e.g. 443 behind NPM → 8080 on the hub). Before NPM sends public traffic to the hub, validate TRUST_PROXY and trusted proxy hops for dbis_core (see NON_CHAIN_ECOSYSTEM_PLAN_REVIEW_AND_GAPS.md §2.1).
Tier 2 — Application hub
Single Node (Fastify/Express) app: validates JWT once, applies rate limits, proxy to subgraphs, adds BFF routes (/bff/portal/...).
Tier 3 — Monolith (long-term)
Merge routers and schema into one codebase — only after boundaries and ownership are clear.
3.3 Middleware cross-cutting
Centralize in the hub:
- CORS allowlist (origins = web hub FQDNs only)
- Rate limiting (especially IRU public POST — align with
dbis_coreTRUST_PROXY=1and a trusted proxy list that includes NPM and this hub’s LAN IP, or rate limits see only the hub) - Request ID propagation
- mTLS or IP allowlist for operator-only routes (optional)
4. NPM and inventory
After cutover:
- Fewer distinct upstream IPs in NPM (many FQDNs can point at the same
IP:port); NPM may still use one proxy host record per FQDN for TLS—equivalent to one ALB with many listener rules, not literally one row total. Host-based routing then lives in web hub nginx (server_name/map) or in Nextmiddleware.ts. - Update ALL_VMIDS_ENDPOINTS.md and
get_host_for_vmidinscripts/lib/load-project-env.shwhen VMIDs are retired or replaced by hub VMIDs. config/ip-addresses.confdefines optional hub variables that default to the current discrete CT IPs (IP_SANKOFA_WEB_HUB→ portal IP,IP_SANKOFA_PHOENIX_API_HUB→ Phoenix API IP). Override in.envwhen hub LXCs exist.
5. Concrete file references in this repo
| Artifact | Purpose |
|---|---|
| config/nginx/sankofa-non-chain-frontends.example.conf | Example host → static root nginx for web hub |
| config/nginx/sankofa-phoenix-api-hub.example.conf | Example path → upstream for API hub (Tier 1); tune upstream to LAN or 127.0.0.1 when colocated |
| config/nginx/sankofa-hub-main.example.conf | Top-level nginx.conf for web hub CT (-c for systemd) |
| config/nginx/sankofa-api-hub-main.example.conf | Top-level nginx.conf for API hub CT |
| config/systemd/sankofa-non-chain-web-hub-nginx.service.example | systemd unit for web hub nginx |
| config/systemd/sankofa-phoenix-api-hub-nginx.service.example | systemd unit for API hub nginx |
| config/compose/sankofa-consolidated-runtime.example.yml | Optional Docker Compose sketch (API hub container only) |
| scripts/verify/check-sankofa-consolidated-nginx-examples.sh | nginx -t on example snippets (host nginx or Docker fallback) |
| scripts/deployment/plan-sankofa-consolidated-hub-cutover.sh | Read-only cutover reminder + resolved env from load-project-env.sh |
| scripts/deployment/install-sankofa-api-hub-nginx-on-pve.sh | Tier-1 hub install on CT (--dry-run / --apply + PROXMOX_OPS_*) |
| scripts/verify/verify-sankofa-consolidated-hub-lan.sh | Read-only LAN smoke (Phoenix, portal, dbis /health, Keycloak realm) |
6. Operator cutover checklist (complete in order)
- Run
bash scripts/verify/check-sankofa-consolidated-nginx-examples.sh(CI or laptop). - Provision one non-chain web hub LXC and/or one API hub LXC (or colocate nginx on an existing CT — document the choice).
- Copy and edit nginx snippets from
config/nginx/into/etc/sankofa-web-hub/and/etc/sankofa-phoenix-api-hub/per systemd examples; install systemd units fromconfig/systemd/*.example(drop.example, adjust paths). - Set
.envoverrides:IP_SANKOFA_WEB_HUB,SANKOFA_WEB_HUB_PORT,IP_SANKOFA_PHOENIX_API_HUB,SANKOFA_PHOENIX_API_HUB_PORT(seeplan-sankofa-consolidated-hub-cutover.shoutput aftersource scripts/lib/load-project-env.sh). - Dry-run NPM upstream changes; then apply during a maintenance window. Confirm WebSocket (GraphQL subscriptions) through NPM if clients use
graphql-ws. - Smoke:
curl -fsS http://<API_HUB>:<PORT>/health, GraphQL POST to/graphql,dbis_corehealth via hub asGET /api-docsorGET /healthon upstream:3000through/api/only if mounted there — simplest:curlhttp://<hub>:<port>/api-docs(proxied) per NON_CHAIN_ECOSYSTEM_PLAN_REVIEW_AND_GAPS.md §2.4. - Update inventory docs and VMID table; decommission retired CTs only after rollback window. Optionally bind Apollo to 127.0.0.1:4000 or firewall :4000 from LAN once NPM uses hub only (NON_CHAIN_ECOSYSTEM_PLAN_REVIEW_AND_GAPS.md §2.5).
7. Related docs
- SANKOFA_PHOENIX_CANONICAL_BOUNDARIES_AND_TAXONOMY.md
- SANKOFA_MARKETPLACE_SURFACES.md
- ENTITY_INSTITUTIONS_WEB_PORTAL_COMPLETION.md
- SERVICE_DESCRIPTIONS.md
- NON_CHAIN_ECOSYSTEM_PLAN_REVIEW_AND_GAPS.md (gaps, inconsistencies, P0/P1 backlog)
8. Decision log (fill when adopted)
| Decision | Choice | Date |
|---|---|---|
| Web hub pattern | TBD (interim: discrete CTs; target: A / B / C) | |
| API hub Tier | 1 (nginx on VMID 7800, LAN 2026-04-13) | 2026-04-13 |
| Public API hostname | phoenix.sankofa.nexus (NPM → 8080 hub; Apollo 127.0.0.1:4000) | 2026-04-13 |
| Retired VMIDs | none |