Drop the Avalanche-heritage /ext prefix; /v1 is the single canonical route surface (one way, no backward compat). The node's baseURL is the source of truth; clients, SDKs, CLI, indexer, maker, genesis, netrunner, and the k8s/compose/gateway/explorer configs are updated to match. Co-authored-by: Hanzo Dev <[email protected]>
35 KiB
White-Label Platform Architecture
Canonical architecture for the Lux ecosystem white-label platform. All services run on lux-k8s (do-sfo3-lux-k8s). LB: 134.199.138.27.
Last updated: 2026-03-24
1. Service Topology
Cloudflare DNS (DNS-only, no proxy)
|
134.199.138.27
|
hanzo-ingress (Traefik DaemonSet)
lux-system namespace
|
+---------------------------+---------------------------+
| | | | | |
lux-gateway lux-explorer lux-bridge lux-mpc lux-kms bootnode
| | | | | |
Gateway Lux Explorer Bridge UI MPC Dash KMS API Cloud UI
API GW (Go binary, + Bridge + MPC API (2 rep) + API
indexer + Server + 5 nodes
GraphQL +
SPA in one
process)
| | | | | |
+---------------------------+---------------------------+
|
lux-mainnet / lux-testnet / lux-devnet
5 luxd validators each
Namespace Layout (lux-k8s)
| Namespace | Services | Pod Count |
|---|---|---|
| lux-system | hanzo-ingress (Traefik), lux-operator | ~6 |
| lux-mainnet | 5 luxd validators (StatefulSet) | 5 |
| lux-testnet | 5 luxd validators (StatefulSet) | 5 |
| lux-devnet | 5 luxd validators (StatefulSet) | 5 |
| lux-gateway | Gateway API gateway | 2 |
| lux-explorer | Lux Explorer (single Go binary serves indexer+GraphQL+SPA for all 5 chains: C, Zoo, Hanzo, SPC, Pars), exchange (N brands), exchange-api, exchange-proxy, graph-node, indexers (13), faucets (N) | ~30 |
| lux-bridge | bridge-ui, bridge-server | 4 |
| lux-mpc | mpc-dashboard, mpc-api, 5 MPC nodes | 7 |
| lux-kms | KMS API (shared), PostgreSQL | 3 |
| bootnode | cloud-web (N brands), bootnode-api | ~6 |
2. Hostname-to-Service Mapping
Explorer (Lux Explorer — single Go binary)
| Hostname | Service | Chain | Brand |
|---|---|---|---|
| explore.lux.network | lux-frontend-mainnet | C-Chain | Lux |
| explore-zoo.lux.network | lux-frontend-zoo | Zoo | Lux |
| explore.zoo.network | lux-frontend-zoo | Zoo | Zoo |
| explore.zoo.ngo | lux-frontend-zoo | Zoo | Zoo |
| explore-hanzo.lux.network | lux-frontend-hanzo | Hanzo | Lux |
| explore.hanzo.network | lux-frontend-hanzo | Hanzo | Hanzo |
| explore.hanzo.ai | lux-frontend-hanzo | Hanzo | Hanzo |
| explore-spc.lux.network | lux-frontend-spc | SPC | Lux |
| explore-pars.lux.network | lux-frontend-pars | Pars | Lux |
| explore.pars.network | lux-frontend-pars | Pars | Pars |
| <dropped — tenant-managed> | (tenant frontend) | (tenant EVM) | (tenant) |
The Lux Explorer Go binary embeds indexer + GraphQL + SPA in one process
and indexes all 5 chains from a single chains.yaml ConfigMap. Hostname-
based branding is served at runtime via /envs.js so a single deployment
handles all brand hostnames without an image rebuild.
Explorer API (Lux Explorer Go binary, port 8090)
The same Lux Explorer pods serve both UI and API on :8090. Per-chain
data lives under /v1/indexer/{slug}/* and /v1/graph/{slug}/*.
| Hostname | Service | Chain | Slug |
|---|---|---|---|
| api-explore.lux.network | explorer:8090 | C-Chain | cchain |
| api-explore-zoo.lux.network | explorer:8090 | Zoo | zoo |
| api-explore.zoo.network | explorer:8090 | Zoo | zoo |
| api-explore-hanzo.lux.network | explorer:8090 | Hanzo | hanzo |
| api-explore.hanzo.network | explorer:8090 | Hanzo | hanzo |
| api-explore-spc.lux.network | explorer:8090 | SPC | spc |
| api-explore-pars.lux.network | explorer:8090 | Pars | pars |
| api-explore.pars.network | explorer:8090 | Pars | pars |
Exchange (DEX frontend)
| Hostname | Service | Primary Chain | Brand |
|---|---|---|---|
| lux.exchange | lux-exchange:3000 | C-Chain | Lux |
| exchange.lux.network | lux-exchange:3000 | C-Chain | Lux |
| markets.lux.network | lux-exchange:3000 | C-Chain | Lux |
| zoo.exchange | zoo-exchange:3000 | Zoo | Zoo |
| pars.market | pars-exchange:3000 | Pars | Pars |
| <dropped — tenant-managed> | (tenant exchange):3000 | (tenant EVM) | (tenant) |
Each exchange deployment is the same ghcr.io/luxfi/exchange:latest image with different
NEXT_PUBLIC_BRAND_* env vars injected via ConfigMap.
Exchange API + Proxy
| Hostname | Service | Purpose |
|---|---|---|
| api-exchange.lux.network | exchange-api:4000 | GraphQL proxy |
| gw.lux.exchange | exchange-proxy:8088 | Uniswap API proxy |
Shared. All exchange brands use the same API backend.
Bridge
| Hostname | Service | Purpose |
|---|---|---|
| bridge.lux.network | bridge-ui:3000 | Bridge frontend |
| bridge-api.lux.network | bridge-server:3000 | Bridge API |
Single deployment. Multi-chain by design (routes via MPC).
MPC
| Hostname | Service | Purpose |
|---|---|---|
| mpc.lux.network | mpc-dashboard-svc:3000 | MPC dashboard |
| mpc-api.lux.network | mpc-api-svc:8081 | MPC API |
| mpc.zoo.network | mpc-dashboard-svc:3000 | (alias) |
| mpc-api.zoo.network | mpc-api-svc:8081 | (alias) |
| mpc-api.pars.network | mpc-api-svc:8081 | (alias) |
Single MPC cluster (5 nodes, threshold 3). Serves all orgs.
KMS
| Hostname | Service | Isolation |
|---|---|---|
| kms.lux.network | lux-kms:8443 | Shared (Lux/Zoo/Hanzo/Pars/SPC) |
| <dropped — tenant-managed> | (tenant-kms):8443 | Dedicated (tenant-only) |
Cloud (Bootnode)
| Hostname | Service | Brand |
|---|---|---|
| cloud.lux.network | lux-cloud-web:3001 | Lux |
| api.cloud.lux.network | bootnode-api:80 | Lux |
| ws.cloud.lux.network | bootnode-api:80 | Lux |
| cloud.pars.network | pars-cloud-web:3001 | Pars |
| api.cloud.pars.network | bootnode-api:80 | Pars |
| web3.hanzo.ai | hanzo-cloud-web:3001 | Hanzo |
| cloud.zoo.network | zoo-cloud-web:3001 | Zoo |
RPC Gateway
| Hostname | Service | Purpose |
|---|---|---|
| api.lux.network | gateway:8080 | All RPC: /{network}/v1/bc/{chain}/rpc |
| api.zoo.network | gateway:8080 | Alias for Zoo RPC |
Faucet (NEW)
| Hostname | Service | Network | Chain |
|---|---|---|---|
| faucet.lux.network | faucet-lux:3000 | Testnet | C-Chain |
| faucet-zoo.lux.network | faucet-zoo:3000 | Testnet | Zoo |
| faucet.zoo.network | faucet-zoo:3000 | Testnet | Zoo |
| faucet-hanzo.lux.network | faucet-hanzo:3000 | Testnet | Hanzo |
| faucet-spc.lux.network | faucet-spc:3000 | Testnet | SPC |
| faucet-pars.lux.network | faucet-pars:3000 | Testnet | Pars |
| faucet.pars.network | faucet-pars:3000 | Testnet | Pars |
Wallet (NEW -- future)
| Hostname | Service | Brand |
|---|---|---|
| wallet.lux.network | lux-wallet:3000 | Lux |
3. Ingress Routing Rules
All IngressRoute CRDs use hanzo.ai/v1alpha1 (NOT traefik.io/v1alpha1).
All live in the namespace of their target service.
TLS via certResolver: letsencrypt with Cloudflare DNS-01 challenge.
Pattern: Multi-Hostname to Single Service
apiVersion: hanzo.ai/v1alpha1
kind: IngressRoute
metadata:
name: <service>-<brand>
namespace: <namespace>
labels:
app: <service>
brand: <brand>
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`primary.domain`) || Host(`alias.domain`)
services:
- name: <service-name>
port: <port>
tls:
certResolver: letsencrypt
domains:
- main: primary.domain
- main: alias.domain
Key Design Rule
One Deployment per (service, brand, chain) tuple. One IngressRoute per Deployment, matching all hostnames for that tuple. One ConfigMap per Deployment, holding all brand/chain-specific env vars.
Exception: The Lux Explorer pods index all chains in one process. Every
brand hostname for every chain routes to the same set of pods; per-brand
appearance is served at runtime via /envs.js and chains.yaml
brand: overrides — no image rebuild.
Existing IngressRoutes (Keep)
| Name | Namespace | Match |
|---|---|---|
| lux-explorer-zoo | lux-explorer | explore-zoo.lux.network, explore.zoo.network |
| lux-explorer-hanzo | lux-explorer | explore-hanzo.lux.network, explore.hanzo.network |
| lux-explorer-spc | lux-explorer | explore-spc.lux.network |
| lux-explorer-pars | lux-explorer | explore-pars.lux.network, explore.pars.network |
| lux-exchange | lux-explorer | exchange.lux.network, markets.lux.network, lux.exchange |
| exchange-api | lux-explorer | api-exchange.lux.network |
| exchange-proxy | lux-explorer | api-exchange.lux.network/proxy/*, gw.lux.exchange |
| lux-bridge | lux-bridge | bridge.lux.network, bridge-api.lux.network |
| lux-mpc | lux-mpc | mpc.lux.network, mpc-api.lux.network |
IngressRoutes to Add
| Name | Namespace | Match | Service |
|---|---|---|---|
| lux-explorer-hanzo-ai | lux-explorer | explore.hanzo.ai | lux-frontend-hanzo:3000 |
| lux-explorer-zoo-ngo | lux-explorer | explore.zoo.ngo | lux-frontend-zoo:3000 |
| zoo-exchange | lux-explorer | zoo.exchange | zoo-exchange:3000 |
| pars-exchange | lux-explorer | pars.market | pars-exchange:3000 |
| lux-explorer-tenant | lux-explorer | <dropped — tenant-managed> | lux-frontend-tenant:3000 |
| tenant-exchange | lux-explorer | tenant-exchange:3000 | |
| lux-kms | lux-kms | kms.lux.network | lux-kms:8443 |
| tenant-kms | lux-kms | <dropped — tenant-managed> | tenant-kms:8443 |
| faucet-lux | lux-explorer | faucet.lux.network | faucet-lux:3000 |
| faucet-zoo | lux-explorer | faucet-zoo.lux.network, faucet.zoo.network | faucet-zoo:3000 |
| faucet-hanzo | lux-explorer | faucet-hanzo.lux.network | faucet-hanzo:3000 |
| faucet-spc | lux-explorer | faucet-spc.lux.network | faucet-spc:3000 |
| faucet-pars | lux-explorer | faucet-pars.lux.network, faucet.pars.network | faucet-pars:3000 |
| mpc-zoo | lux-mpc | mpc.zoo.network, mpc-api.zoo.network | (same MPC services) |
| mpc-pars | lux-mpc | mpc-api.pars.network | (same MPC services) |
| hanzo-cloud-web | bootnode | web3.hanzo.ai | hanzo-cloud-web:3001 |
| zoo-cloud-web | bootnode | cloud.zoo.network | zoo-cloud-web:3001 |
4. ConfigMap Structure for Per-Brand Customization
Explorer Frontend ConfigMap
One ConfigMap per (chain, brand) tuple. Naming: frontend-config-{chain} (current)
or frontend-config-{chain}-{brand} if the same chain needs different branding for
different hostnames.
Current state: each chain has one ConfigMap (Zoo chain = Zoo brand). This is correct for now because each app chain EVM IS a brand. If a chain ever needs multiple brand presentations, add a second ConfigMap and a second Deployment.
Schema (all fields are NEXT_PUBLIC_* env vars):
# API routing
NEXT_PUBLIC_API_HOST # api-explore-{chain}.lux.network
NEXT_PUBLIC_API_PROTOCOL # https
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL # wss
NEXT_PUBLIC_APP_HOST # explore-{chain}.lux.network
# Network identity
NEXT_PUBLIC_NETWORK_NAME # Zoo, Hanzo, SPC, Pars
NEXT_PUBLIC_NETWORK_ID # EVM chain ID (200200, 36963, etc.)
NEXT_PUBLIC_NETWORK_CURRENCY_NAME # ZOO, AI, SPC, PARS
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL # same
NEXT_PUBLIC_NETWORK_RPC_URL # https://api.lux.network/mainnet/v1/bc/{blockchain_id}/rpc
NEXT_PUBLIC_IS_TESTNET # false
# Theme
NEXT_PUBLIC_COLOR_THEME_DEFAULT # dark
# Disable third-party branding / ad providers
NEXT_PUBLIC_AD_BANNER_PROVIDER # none
NEXT_PUBLIC_AD_TEXT_PROVIDER # none
Exchange ConfigMap
One ConfigMap per exchange brand. Naming: exchange-config-{brand}.
Schema (env vars from config/brand.ts):
NEXT_PUBLIC_BRAND_NAME # Lux Exchange, Zoo Exchange, Pars Market
NEXT_PUBLIC_BRAND_TITLE # same
NEXT_PUBLIC_BRAND_DESCRIPTION # tagline
NEXT_PUBLIC_BRAND_URL # https://lux.exchange
NEXT_PUBLIC_BRAND_SITE_NAME # same as name
NEXT_PUBLIC_BRAND_SHORT_NAME # Lux, Zoo, Pars
NEXT_PUBLIC_BRAND_AUTHOR # Lux Partners, Zoo Labs Foundation, etc.
NEXT_PUBLIC_NETWORK_NAME # Lux Network, Zoo Network, etc.
NEXT_PUBLIC_COIN_NAME # LUX, ZOO, PARS
NEXT_PUBLIC_LOGO_URL # /tokens/{brand}.svg
NEXT_PUBLIC_PRIMARY_COLOR # hex color
NEXT_PUBLIC_TWITTER_HANDLE # @luxnetwork, etc.
NEXT_PUBLIC_API_ENDPOINT # https://api.lux.network
Cloud (Bootnode) ConfigMap
Brand is set via NEXT_PUBLIC_BRAND env var on the Deployment directly.
IAM client ID varies per brand. API URL varies per brand domain.
NEXT_PUBLIC_BRAND # lux, zoo, pars, hanzo, <tenants...>
NEXT_PUBLIC_API_URL # https://api.cloud.{domain}
NEXT_PUBLIC_WS_URL # wss://ws.cloud.{domain}
NEXT_PUBLIC_IAM_URL # https://lux.id (shared)
NEXT_PUBLIC_IAM_CLIENT_ID # {brand}-cloud
NEXT_PUBLIC_IAM_DOMAIN # lux.id (shared) or tenant.id (dedicated)
Faucet ConfigMap
One per chain. Naming: faucet-config-{chain}.
FAUCET_CHAIN_NAME # Lux Testnet, Zoo Testnet, etc.
FAUCET_CHAIN_ID # testnet EVM chain ID
FAUCET_RPC_URL # https://api.lux.network/testnet/v1/bc/{chain}/rpc
FAUCET_AMOUNT # drip amount per request (e.g., "1000000000000000000" = 1 token)
FAUCET_CURRENCY_SYMBOL # LUX, ZOO, AI, SPC, PARS
FAUCET_RATE_LIMIT_SECONDS # 86400 (once per day)
FAUCET_BRAND_COLOR # hex
FAUCET_LOGO_URL # /logo.svg
# Secret: FAUCET_PRIVATE_KEY loaded from KMSSecret, never in ConfigMap
5. KMS Isolation Model
Shared KMS (kms.lux.network)
Serves: Lux, Zoo, Hanzo, Pars, SPC.
Organization isolation via KMS projects:
| KMS Project | Org | Secrets |
|---|---|---|
| lux | Lux Network | node staking keys, fee keys, deployer keys, validator BLS |
| zoo | Zoo Labs | faucet keys, deployer keys |
| hanzo | Hanzo AI | faucet keys, deployer keys |
| spc | SPC | faucet keys, deployer keys |
| pars | Pars | faucet keys, deployer keys |
Access control: IAM OIDC owner claim scopes KMS API calls to the org's project.
No org can read another org's secrets.
Dedicated KMS (<dropped — tenant-managed>)
Serves: a single tenant in dedicated mode.
Separate KMS deployment in lux-kms namespace with its own PostgreSQL database. A tenant is a paying client with regulatory requirements for key isolation. Their KMS has its own staking keys, MPC key shares, and deployer keys.
KMS Secret Flow
KMS API (kms.lux.network)
|
v
KMSSecret CRD (in target namespace)
|
v
lux-operator reconciles -> K8s Secret
|
v
Pod mounts Secret as env or volume
No secrets in ConfigMaps. No secrets in manifests. No secrets in env files committed to git.
6. Bootnode (Cloud Admin) Integration Points
Bootnode (~/work/bootnode/) is the admin control plane for the white-label platform.
What Bootnode Manages
- Org onboarding -- create org in IAM, create KMS project, provision DNS
- Chain deployment -- trigger app chain EVM creation via lux-operator CRDs
- Service provisioning -- generate K8s manifests for new WL tenant
- Monitoring -- health checks, chain sync status, validator status
- User management -- IAM users, roles, API keys per org
Architecture
cloud.{brand}.{tld} --> bootnode-web (Next.js 15, brand from env)
|
api.cloud.{brand}.{tld} --> bootnode-api (FastAPI, Python 3.12+)
|
+----------+----------+
| | |
lux-operator IAM API KMS API
(K8s CRDs) (hanzo.id) (kms.lux.network)
Bootnode API Endpoints (Relevant to WL)
| Endpoint | Purpose |
|---|---|
| POST /v1/orgs | Create new org (provisions IAM + KMS) |
| POST /v1/orgs/{id}/chains | Deploy new app chain EVM chain |
| POST /v1/orgs/{id}/services | Provision explorer/exchange/faucet for org |
| GET /v1/orgs/{id}/status | Health of all org services |
| POST /v1/orgs/{id}/dns | Configure DNS records via CF API |
| POST /v1/orgs/{id}/secrets | Store secret in org's KMS project |
Bootnode-Generated Manifests
When onboarding a new client, Bootnode API generates:
- ConfigMap for explorer frontend (chain identity, RPC URL, theme)
- ConfigMap for exchange (brand vars)
- ConfigMap for faucet (chain, drip amount, rate limit)
- Deployment + Service for each frontend
- IngressRoute for each hostname
- KMSSecret CRDs for deployer/faucet keys
These are applied via kubectl apply from the bootnode-api pod (ServiceAccount with
RBAC scoped to lux-explorer, lux-kms namespaces).
7. IAM/Auth Flow Per Org
Identity Domains
| Domain | Org | Codebase |
|---|---|---|
| hanzo.id | Hanzo | ~/work/hanzo/login/ |
| lux.id | Lux | ~/work/hanzo/login/ (same) |
| id.zoo.network | Zoo | ~/work/hanzo/login/ (same) |
| id.pars.network | Pars | ~/work/hanzo/login/ (same) |
| (tenant) | (tenant) | ~/work/hanzo/login/ (same) |
Single codebase, hostname detection determines org config (logo, colors, allowed OAuth providers).
Auth Flow
1. User visits cloud.pars.network
2. Frontend redirects to id.pars.network/oauth/authorize?client_id=pars-cloud&redirect_uri=...
3. User authenticates (email/pass, Google, GitHub, Web3 wallet)
4. id.pars.network issues JWT with:
- sub: user ID
- owner: "pars" <-- org scope
- aud: "pars-cloud" <-- client ID
- iss: "https://id.pars.network"
5. Frontend stores JWT, sends as Authorization: Bearer header
6. API validates JWT against IAM JWKS endpoint
7. API scopes all queries to owner="pars"
Tenant Isolation
Every API query MUST be scoped to the owner claim from the JWT.
No cross-org data leakage. Enforced at:
- KMS: project = owner
- Bootnode API: org = owner
- Explorer: public data (no auth needed)
- Exchange: public data (no auth needed, wallet-based)
- Bridge: wallet-based auth (no org scoping needed)
- MPC: API key per org, validated against IAM
IAM Client Registration Per Org
Each service per org gets an OIDC client:
| Client ID | Redirect URI |
|---|---|
| lux-cloud | https://cloud.lux.network/callback |
| pars-cloud | https://cloud.pars.network/callback |
| zoo-cloud | https://cloud.zoo.network/callback |
| lux-exchange | https://lux.exchange/callback |
| zoo-exchange | https://zoo.exchange/callback |
| pars-market | https://pars.market/callback |
8. Faucet Deployment Model
Image
ghcr.io/luxfi/faucet:latest -- single image, configured via env vars.
Architecture
User -> faucet.{domain} -> faucet-{chain}:3000
|
Rate limiter (Valkey)
|
Fund tx via private key
|
luxd RPC (testnet)
Deployment Pattern
One Deployment per chain (not per brand). Multiple brand hostnames route to the same faucet Deployment for a given chain.
apiVersion: apps/v1
kind: Deployment
metadata:
name: faucet-{chain}
namespace: lux-explorer
labels:
app: lux-faucet
chain: {chain}
spec:
replicas: 1
selector:
matchLabels:
app: lux-faucet
chain: {chain}
template:
spec:
containers:
- name: faucet
image: ghcr.io/luxfi/faucet:latest
envFrom:
- configMapRef:
name: faucet-config-{chain}
env:
- name: FAUCET_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: faucet-key-{chain}
key: private-key
ports:
- containerPort: 3000
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { cpu: 200m, memory: 256Mi }
Rate Limiting
Per-address, per-chain. One request per 24 hours.
Backed by Valkey (shared instance in lux-explorer namespace).
Key format: faucet:{chain_id}:{address} with TTL=86400.
Faucet Key Management
Each chain's faucet private key is stored in KMS under the org's project.
Synced to K8s via KMSSecret CRD -> faucet-key-{chain} Secret.
9. DNS Requirements Per Org
Record Type
All records are A records pointing to 134.199.138.27.
All DNS-only (NOT Cloudflare proxied) -- Traefik handles TLS.
Exception: apex domains on GitHub Pages use their own IPs.
Lux (lux.network zone + lux.exchange zone)
# lux.network (zone 287bdfd07cf016bd102f6394892e4759)
explore.lux.network A 134.199.138.27
api-explore.lux.network A 134.199.138.27
explore-{zoo,hanzo,spc,pars}.lux.network A 134.199.138.27
api-explore-{zoo,hanzo,spc,pars}.lux.network A 134.199.138.27
exchange.lux.network A 134.199.138.27
markets.lux.network A 134.199.138.27
bridge.lux.network A 134.199.138.27
bridge-api.lux.network A 134.199.138.27
mpc.lux.network A 134.199.138.27
mpc-api.lux.network A 134.199.138.27
kms.lux.network A 134.199.138.27
faucet.lux.network A 134.199.138.27
faucet-{zoo,hanzo,spc,pars}.lux.network A 134.199.138.27
wallet.lux.network A 134.199.138.27
cloud.lux.network A 134.199.138.27 (currently 134.199.136.205)
api.cloud.lux.network A 134.199.138.27 (currently 134.199.136.205)
ws.cloud.lux.network A 134.199.138.27 (currently 134.199.136.205)
api.lux.network A 134.199.138.27 (gateway)
api-exchange.lux.network A 134.199.138.27
api-indexer*.lux.network A 134.199.138.27
# lux.exchange (zone 99ba4822517691fef46757c763490ad5)
lux.exchange A 134.199.138.27
gw.lux.exchange A 134.199.138.27
www.lux.exchange A 134.199.138.27
Zoo (zoo.network zone)
# zoo.network (zone d9c81c5b54f0f713105d483ddbd61656)
explore.zoo.network A 134.199.138.27 (exists)
api-explore.zoo.network A 134.199.138.27 (exists)
api.zoo.network A 134.199.138.27 (exists)
mpc.zoo.network A 134.199.138.27 (exists)
mpc-api.zoo.network A 134.199.138.27 (exists)
# NEW:
exchange.zoo.network A 134.199.138.27
faucet.zoo.network A 134.199.138.27
cloud.zoo.network A 134.199.138.27
api.cloud.zoo.network A 134.199.138.27
Pars (pars.network zone)
# pars.network (zone 83fa188f7872fba1077981000498ea42)
explore.pars.network A 134.199.138.27 (exists)
api-explore.pars.network A 134.199.138.27 (exists)
bridge.pars.network A 134.199.138.27 (exists)
mpc-api.pars.network A 134.199.138.27 (exists)
# NEW:
faucet.pars.network A 134.199.138.27
cloud.pars.network A 134.199.138.27 (exists in bootnode ingress)
api.cloud.pars.network A 134.199.138.27 (exists in bootnode ingress)
Hanzo (hanzo.network + hanzo.ai zones)
# hanzo.network (zone c7b46289ad8a039c2cf4ccc24b11128a)
explore.hanzo.network A 134.199.138.27 (exists)
api-explore.hanzo.network A 134.199.138.27 (exists)
# hanzo.ai (zone bac51c3900e73fd28aa75a59898bcee0)
explore.hanzo.ai A 134.199.138.27 (NEW)
api-explore.hanzo.ai A 134.199.138.27 (NEW -- if needed)
web3.hanzo.ai A 134.199.138.27 (NEW -- bootnode cloud)
<dropped — tenant-managed> A 134.199.138.27
<dropped — tenant-managed> A 134.199.138.27
(tenant) CNAME hanzo-id.pages.dev (proxied)
Each tenant manages their own Cloudflare zone. We provide the target IP and IngressRoute specs. They create the records.
10. Onboarding Checklist for New L1/L2 Clients
Prerequisites (Client Provides)
- Organization name (slug, e.g.,
acme) - Domain name (e.g.,
acme.network) - Cloudflare zone ID (or delegate DNS to us)
- Logo files (SVG preferred: icon, wordmark, favicon)
- Brand colors (primary hex, optional secondary)
- Coin name and symbol (e.g., ACME)
- EVM chain ID (must be globally unique, verify against chainlist.org)
- Genesis config (validators, allocations, gas config)
- KMS isolation: shared or dedicated
Step 1: IAM Setup
- Create org in IAM:
POST /api/v1/orgs { name: "acme", slug: "acme" } - Create OIDC clients:
acme-cloud,acme-exchange - Configure identity domain:
id.acme.network-> hanzo-id.pages.dev (or login deployment) - Add org admin user
Step 2: KMS Setup
- Create KMS project:
acme - Generate deployer key -> store in KMS
- Generate faucet key -> store in KMS
- Generate fee key -> store in KMS (for P-chain operations)
- Create KMSSecret CRDs for each key
Step 3: Chain Deployment
- Add app chain EVM config to NETWORKS.yaml
- Deploy app chain EVM via lux-operator LuxChain CRD
- Verify chain is tracking on all 5 validators (mainnet/testnet)
- Deploy standard contracts (WLUX, bridged tokens, AMM, governance -- 20 contracts)
- Verify contract deployments
Step 4: Explorer
- Add the new chain to the existing Lux Explorer
chains.yamlConfigMap; reload (no new Deployment needed) - Create PostgreSQL database (or schema in shared instance)
- Create frontend ConfigMap (frontend-config-acme)
- Create frontend Deployment + Service (lux-frontend-acme)
- Create IngressRoute (explore-acme.lux.network, explore.acme.network)
- Verify indexing from block 0
Step 5: Exchange (if client wants WL DEX)
- Create exchange ConfigMap (exchange-config-acme)
- Create exchange Deployment + Service (acme-exchange)
- Create IngressRoute (acme.exchange or exchange.acme.network)
- Add chain to exchange-api supported chains
- Deploy AMM pools with initial liquidity
Step 6: Faucet
- Create faucet ConfigMap (faucet-config-acme)
- Create KMSSecret for faucet key
- Create faucet Deployment + Service (faucet-acme)
- Create IngressRoute (faucet.acme.network, faucet-acme.lux.network)
- Fund faucet address on testnet
Step 7: Cloud (Bootnode)
- Create cloud-web Deployment with
NEXT_PUBLIC_BRAND=acme - Create IngressRoute (cloud.acme.network, api.cloud.acme.network)
- Verify auth flow: cloud.acme.network -> id.acme.network -> callback
Step 8: DNS
- Add A records to client's Cloudflare zone (or our zone if delegated):
- explore.acme.network -> 134.199.138.27
- api-explore.acme.network -> 134.199.138.27
- faucet.acme.network -> 134.199.138.27
- cloud.acme.network -> 134.199.138.27
- api.cloud.acme.network -> 134.199.138.27
- (exchange, bridge, MPC as needed)
- Add ACME challenge TXT records for Traefik cert issuance
- Add subdomain records to lux.network zone:
- explore-acme.lux.network -> 134.199.138.27
- faucet-acme.lux.network -> 134.199.138.27
- Update
k8s/dns/lux-network.yamland client zone YAML - Run dns-sync to apply
Step 9: Verification
- Explorer loads and indexes blocks
- Exchange loads with correct branding
- Faucet drips tokens on testnet
- Cloud dashboard authenticates via IAM
- KMS secrets accessible only to org
- RPC gateway returns chain data
- All TLS certs issued and valid
Step 10: Handoff
- Document client's chain IDs in NETWORKS.yaml
- Add to monitoring (lux-operator health checks)
- Provide client with:
- IAM admin credentials (hashed, via KMS)
- API documentation
- RPC endpoints
- Explorer URLs
- Faucet URLs
11. Migration Plan: Current State to Target State
Phase 0: Prerequisites (Already Done)
- lux-k8s cluster operational with all 3 networks
- Lux Explorer (Go binary) indexing all 5 chains (C, Zoo, Hanzo, SPC, Pars)
- Exchange deployed (lux.exchange, exchange.lux.network, markets.lux.network)
- Bridge deployed (bridge.lux.network)
- MPC deployed (mpc.lux.network)
- Gateway deployed (api.lux.network)
- Bootnode (cloud) deployed for Lux and Pars brands
- DNS zone YAMLs for all zones
- Exchange brand.ts config-driven branding
Phase 1: Fix Ingress CRDs (Week 1)
Problem: Bootnode ingress uses networking.k8s.io/v1 Ingress with cert-manager,
not hanzo.ai/v1alpha1 IngressRoute with certResolver: letsencrypt.
Action:
- Convert
k8s/bootnode/ingress.yamlfrom K8s Ingress to hanzo.ai IngressRoute CRDs - Remove cert-manager annotations, use
certResolver: letsencrypt - Add explore.hanzo.ai and explore.zoo.ngo to existing explorer IngressRoutes
- Add MPC alias hostnames (mpc.zoo.network, mpc-api.pars.network)
Risk: Low. IngressRoute is additive. Old Ingress objects can be deleted after verification.
Phase 2: White-Label Exchange Deployments (Week 1-2)
Problem: Exchange is a single deployment. Zoo/Pars/tenants have no dedicated exchange instance.
Action:
- Create exchange-config-zoo ConfigMap from
.env.example.zoo - Create exchange-config-pars ConfigMap from
.env.example.pars - Create Deployment + Service for zoo-exchange and pars-exchange
- Create IngressRoutes for zoo.exchange, pars.market
- All use the same image (
ghcr.io/luxfi/exchange:latest), different ConfigMaps
Risk: Low. Same image, different env vars. The brand.ts already reads from env.
Phase 3: Faucet Deployment (Week 2)
Problem: No faucets exist.
Action:
- Build faucet image: single Next.js app with RPC integration and rate limiting
- One Deployment per chain (6 total: lux, zoo, hanzo, spc, pars + C-chain devnet)
- Store faucet private keys in KMS, sync via KMSSecret
- Deploy Valkey instance in lux-explorer for rate limiting (or use existing if present)
- Add DNS records and IngressRoutes
Risk: Medium. New service. Needs rate limiting to prevent draining. Testnet only.
Phase 4: KMS Namespace + Tenant Isolation (Week 2-3)
Problem: KMS runs on hanzo-k8s. Need lux-kms namespace on lux-k8s for:
- Shared KMS (Lux/Zoo/Hanzo/Pars/SPC)
- Dedicated KMS (per-tenant)
Action:
- Deploy KMS in lux-kms namespace on lux-k8s
- Create two KMS Deployments:
lux-kms(shared) andtenant-kms(dedicated) - Each has its own PostgreSQL database
- Migrate existing KMS projects from hanzo-k8s to lux-k8s KMS
- Update all KMSSecret CRDs to point to new KMS endpoint
Risk: High. KMS migration requires careful secret rotation. Do in maintenance window. Keep hanzo-k8s KMS running as fallback until migration is verified.
Phase 5: Bootnode Admin Automation (Week 3-4)
Problem: Onboarding is manual (write YAML, kubectl apply).
Action:
- Add
/v1/orgs/{id}/servicesendpoint to bootnode-api - Implement manifest generation: ConfigMap + Deployment + Service + IngressRoute
- Implement
kubectl applyvia ServiceAccount with scoped RBAC - Add IAM org creation endpoint (proxy to IAM API)
- Add KMS project creation endpoint (proxy to KMS API)
- Add DNS record creation endpoint (proxy to CF API)
Risk: Medium. Automation of cluster operations requires careful RBAC. ServiceAccount should only have access to lux-explorer and lux-kms namespaces.
Phase 6: Tenant Onboarding (Week 4)
Problem: the tenant is not yet deployed.
Action:
- Follow onboarding checklist (Section 10) for the new tenant
- Deploy tenant app chain EVM (chain IDs from NETWORKS.yaml)
- Dedicated KMS instance (Phase 4 prerequisite)
- Explorer, exchange, faucet, cloud
- Each tenant manages their own DNS zone -- we provide IP + IngressRoute specs
Risk: Medium. First external paying client. Template for all future clients.
Phase 7: Wallet Integration (Week 5+)
Problem: No wallet UI deployed.
Action: Deferred. wallet.lux.network reserved in DNS. Deploy when wallet codebase is production-ready.
Appendix A: Image Registry
All images on ghcr.io. Built via CI/CD (platform.hanzo.ai Tekton pipelines).
Never build images locally.
| Image | Purpose |
|---|---|
| ghcr.io/luxfi/node | luxd validator node |
| ghcr.io/luxfi/explorer | Lux Explorer (single Go binary: indexer+GraphQL+SPA) |
| ghcr.io/luxfi/exchange | Exchange frontend (Vite SPA) |
| ghcr.io/luxfi/exchange-api | Exchange GraphQL API |
| ghcr.io/luxfi/exchange-proxy | Uniswap API proxy |
| ghcr.io/luxfi/gateway | Gateway API gateway |
| ghcr.io/luxfi/operator | K8s operator (Rust) |
| ghcr.io/luxfi/indexer | Chain indexer |
| ghcr.io/luxfi/faucet | Faucet (NEW) |
| ghcr.io/hanzoai/bootnode | Cloud API + web |
| ghcr.io/hanzoai/ingress | Traefik-based ingress |
| ghcr.io/hanzoai/kms | KMS API |
Appendix B: Port Map (Cluster-Internal)
| Service | Port | Protocol |
|---|---|---|
| luxd HTTP | 9630 (mainnet), 9640 (testnet), 9650 (devnet) | HTTP |
| luxd staking | 9631/9641/9651 | TCP |
| Explorer backend | 4000 | HTTP |
| Explorer frontend | 3000 | HTTP |
| Exchange frontend | 3000 | HTTP |
| Exchange API | 4000 | HTTP |
| Exchange proxy | 8088 | HTTP |
| Bridge UI | 3000 | HTTP |
| Bridge API | 3000 | HTTP |
| MPC dashboard | 3000 | HTTP |
| MPC API | 8081 | HTTP |
| KMS API | 8443 | HTTPS |
| Gateway | 8080 | HTTP |
| Faucet | 3000 | HTTP |
| Cloud web | 3001 | HTTP |
| Cloud API | 80 | HTTP |
| PostgreSQL | 5432 | TCP |
| Valkey | 6379 | TCP |
Appendix C: Design Decisions
Decision: One Deployment per (service, brand) tuple, NOT hostname-based routing to a single pod
Rationale:
- NEXT_PUBLIC_* env vars are baked at build time (or container start). Cannot vary per request.
- Each brand needs its own container with its own env vars.
- Cost is ~64-256Mi per pod. Acceptable for 5-10 brands.
Trade-off: More pods than a single multi-tenant container. Acceptable because it eliminates runtime hostname detection complexity and keeps each brand's config immutable and auditable.
Decision: Single Lux Explorer pod set, runtime-branded frontend
Rationale:
- The Lux Explorer Go binary indexes a chain, not a brand. Chain data is the same regardless of branding.
- Branding (logo, colors, domain) is served at runtime from
chains.yamland/envs.js. The same image serves every brand on every hostname. - Per-chain SQLite under
/data/{slug}avoids the shared-PostgreSQL bottleneck of the legacy Elixir backend.
Trade-off: All brand hostnames for a given chain hit the same pod set. If one brand needs different API behaviour (e.g., different rate limits), add middleware in front of the explorer Service rather than a second backend.
Decision: Faucets in lux-explorer namespace, not a separate namespace
Rationale:
- Faucets are lightweight (50m CPU, 64Mi RAM).
- They share the same Valkey instance as explorer indexers.
- Fewer namespaces = simpler RBAC and service discovery.
Decision: Tenants can get dedicated KMS, all others share defaults
Rationale:
- (tenant) is a paying external client with regulatory requirements.
- Internal orgs (Zoo, Hanzo, Pars, SPC) are all under Lux Network governance.
- KMS project isolation (within shared KMS) is sufficient for internal orgs.
- Dedicated KMS means separate database, separate keys, separate audit log.
Trade-off: Operational overhead of running two KMS instances. Acceptable because this is the template for paying clients with isolation requirements.
Decision: hanzo.ai/v1alpha1 IngressRoute for everything, no K8s Ingress
Rationale:
- One way to do things. (PHILOSOPHY.md principle 1.)
- hanzo-ingress (Traefik) is the only ingress controller on lux-k8s.
- K8s Ingress objects work but bypass Traefik's certResolver.
- IngressRoute gives direct access to Traefik middlewares (strip prefix, rate limit, etc.).
Action: Convert remaining K8s Ingress objects in bootnode namespace to IngressRoute.