Brand Sovereignty and Federation Discovery
Per-org brand data isolation, white-label by fork (not env-preset multi-tenant), `/.well-known/<appId>.json` federation discovery per IETF RFC 8615.
Abstract
Every app shared across the Lux + Hanzo + Zoo + Pars + Partner orgs (exchange, market, explore, bridge, IAM, etc.) consumes a single BrandConfig schema, but each org's source repos carry only that org's brand data. White-label deployments are realized by forking the canonical app into the target org's GitHub namespace; cross-org brand presets, k8s manifests, federation peer URLs, and similar brand data are not committed to upstream repos. Apps expose /.well-known/<appId>.json per IETF RFC 8615 so peers can discover each other's brand identity, chain bindings, and capabilities at runtime; the peer list itself is supplied via runtime ConfigMap, never source.
Motivation
Earlier drafts of the white-label stack carried per-org brand presets (hanzo.json, zoo.json, pars.json, partner.json) inside luxfi/exchange's source tree. This was wrong on three axes:
- Regulatory isolation. The Partner is a US securities ATS / BD / TA. Source-level mention of Lux's open-source ecosystem inside the Partner (or of the Partner inside any of Lux / Hanzo / Zoo / Pars) blurs the legal boundary.
- Ownership. Each brand's preset is the responsibility of that brand's engineering team. Co-located presets make upstream PRs accidentally land cross-org brand updates.
- Forking ergonomics. A fork that inherits five other orgs' brand data must hand-strip them before going live. The per-org fork model inverts the default: the fork inherits zero cross-brand pollution.
The fix is to push the fork point higher: shared SDK code + canonical app are upstream; brand data lives in each downstream fork.
Specification
1. Brand schema
A BrandConfig schema (the existing pkgs/config/src/brand.ts shape in luxfi/exchange and the equivalent in ~/work/hanzo/market) defines:
interface BrandConfig {
name: string
title: string
shortName: string
description: string
legalEntity?: string
walletName?: string
protocolName?: string
copyrightHolder?: string
appDomain: string
docsDomain?: string
infoDomain?: string
gatewayDomain?: string
wsDomain?: string
helpUrl?: string
termsUrl?: string
privacyUrl?: string
complianceEmail?: string
supportEmail?: string
twitter?: string
github?: string
discord?: string
primaryColor?: string
defaultChainId?: number
supportedChainIds?: number[]
theme?: { light?: BrandTheme; dark?: BrandTheme }
}
Schemas may be shared via a neutral types-only package (no brand data). Brand DATA never crosses org boundaries even via shared packages.
2. Per-org brand sovereignty
| Org | Repo namespace | Brand data home |
|---|---|---|
| Lux | luxfi/* | only Lux brand (e.g. luxfi/exchange/config/brands/lux.json) |
| Hanzo | hanzoai/* | only Hanzo brand (e.g. hanzoai/market/public/brands/hanzo.json) |
| Zoo | zooai/* | only Zoo brand |
| Pars | parsdao/* | only Pars brand |
| Partner | partner/* | only Partner brand; zero Lux / Hanzo / Zoo / Pars refs |
No cross-org brand presets, k8s manifests, deploy configs, or peer URLs in source. Code comments referencing other orgs' brand identity (e.g. // also used by Zoo) are similarly disallowed.
3. White-label by fork
Shared apps (exchange, market, explore, bridge, etc.) live as canonical OSS repos under one org and are forked into each consumer org's namespace:
| App | Canonical | Forks |
|---|---|---|
| Exchange | luxfi/exchange | hanzoai/exchange, zooai/exchange, parsdao/exchange, partner/exchange |
| Market | hanzoai/market | zooai/market, parsdao/market, luxfi/market (future) |
| Explore | luxfi/explore | hanzoai/explore (L2), zooai/explore (L2), etc. |
| Bridge | luxfi/bridge | zooai/bridge-shim, etc. |
Each fork pulls upstream for code changes and ships with its own brand JSON. No environment-preset multi-tenancy in upstream (no BRAND_ID=zoo selecting between bundled presets — Zoo's fork ships Zoo's preset; nothing else exists).
4. Federation discovery — /.well-known/<appId>.json
Per IETF RFC 8615, each app exposes a discovery endpoint at /.well-known/<appId>.json. Per §4.1, the descriptor is split into two endpoints with separate ownership and change rates:
/.well-known/brand.json— brand IDENTITY only, owned by the brand team./.well-known/<appId>.json— federation graph + API metadata, owned by the deployment operator, references the brand by URL.
Example app descriptor:
{
"appId": "exchange",
"title": "Lux Exchange",
"domain": "lux.exchange",
"url": "https://lux.exchange",
"github": "https://github.com/luxfi/exchange",
"brand": "/.well-known/brand.json",
"brandHash": "sha256-MipJxMZbfZkl5BeHrGTFQPPWU01sKZej/FMZVH2YzCc=",
"chain": {
"id": 96369,
"name": "Lux Mainnet",
"rpcUrl": "https://api.lux.network/mainnet/v1/bc/C/rpc",
"explorerUrl": "https://explore.lux.network"
},
"peers": [],
"capabilities": ["swap", "lp", "limit-order"],
"apiVersion": "1",
"apiBase": "https://lux.exchange/api/v1",
"brandId": "lux"
}
Fields:
appId— the app identifier (e.g.exchange,market,explore,bridge,iam,base).title,domain,url,github— display + canonical URLs (denormalized for legacy consumers).brand— URL to the brand descriptor (relative resolved against this endpoint, or absolute).brandHash— OPTIONALsha256-<base64>subresource-integrity tag over the brand payload bytes. Consumers MAY verify; a mismatch is a warning (peer may have rebranded), not a hard error.chain— primary chain binding for this deployment (optional).peers— federated peer list. Empty by default in source. Operators add peers at deploy time via Kubernetes ConfigMap that overrides the bundled file.capabilities— declared feature flags.apiVersion,apiBase— API root metadata.brandId— back-compat: the org identifier (kept at top level for legacy consumers that don't follow thebrandlink).
The endpoint MUST be served with Content-Type: application/json and SHOULD be cacheable for ≥1 hour.
4.1. Brand/App descriptor split
The federation descriptor is split across two endpoints so that data with different ownership and change cadence does not share a write path:
| Endpoint | Schema | Owned by | Change rate | Recommended cache |
|---|---|---|---|---|
/.well-known/brand.json | WellKnownBrand (identity only) | Brand team | Rarely (rebrand, logo, domain move) | ≥24 hours |
/.well-known/<appId>.json | WellKnownApp (federation graph) | Deployment operator | Weekly (peers, capabilities, chain bindings) | ≥1 hour |
Brand descriptor (/.well-known/brand.json):
{
"brandId": "lux",
"name": "Lux Exchange",
"title": "Lux Exchange | Trade",
"shortName": "Lux Exchange",
"description": "The decentralized exchange for the Lux ecosystem",
"appDomain": "lux.exchange",
"legalEntity": "Lux Industries Inc.",
"copyrightHolder": "Lux Industries Inc.",
"supportEmail": "[email protected]",
"twitter": "https://x.com/luxfi",
"github": "https://github.com/luxfi",
"discord": "https://discord.gg/lux",
"primaryColor": "#FFFFFF",
"theme": { "light": {}, "dark": {} }
}
The brand descriptor contains ONLY identity fields. It MUST NOT contain chain, peers, capabilities, apiBase, or any other field that is per-app or per-deployment. Those belong on the app descriptor.
Rationale. Earlier drafts mashed brand IDENTITY (logo, legal entity, theme) with federation GRAPH (peers, capabilities, chain binding) in a single endpoint. The two have fundamentally different ownership: the brand team owns and updates identity (rebrands happen once a year), while the deployment operator owns and updates the federation graph (peer additions happen weekly via ConfigMap). Mashing them meant brand updates required redeploying every app, and federation peer adds touched the same file that the brand team owns. The split decouples ownership and cache strategy without changing the underlying schema vocabulary.
Brand integrity (brandHash). When the app descriptor includes a brandHash field of the form sha256-<base64>, federated consumers MAY verify it before trusting brand-driven UI (logo, theme, copyright line). The hash is computed over the exact bytes returned by GET /.well-known/brand.json. A mismatch SHOULD render a [unverified brand] badge but MUST NOT block rendering — the peer may have rebranded without updating their advertised hash.
Resolution algorithm. A federated consumer fetching peer P's well-known performs:
GET P/.well-known/<appId>.json→ parseWellKnownApppayload.- If
payload.brandis a string URL: resolve against the response URL, thenGETit → parseWellKnownBrand. Ifpayload.brandHashis present, verifysha256(brandBytes) == payload.brandHash. - If
payload.brandis an object: treat as legacy mashed shape, use inline (no second fetch). This preserves backwards compatibility with pre-split publishers.
Backwards compatibility. Pre-split publishers continue to work: their per-app file embeds a brand OBJECT; new consumers detect the object type and use it inline. Pre-split consumers continue to work against new publishers: the per-app file emitted by the new generator retains denormalized top-level fields (title, domain, url, github, brandId) and MAY include a brandObject mirror of the linked brand payload.
5. Build-time generation
Each app ships a scripts/generate-well-known.js (or equivalent) that reads its single brand JSON at build time and emits BOTH public/.well-known/brand.json AND public/.well-known/<appId>.json into the static asset bundle. The script computes sha256(brand.json bytes) and embeds the result in the per-app file as brandHash. CI hooks this after sitemap generation, before image upload.
6. Runtime federation overlay
Operators populate the peer list by mounting a custom <app>.json ConfigMap over the default in the running pod:
# k8s/peers-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: well-known-exchange
data:
exchange.json: |
{
"brandId": "lux",
"appId": "exchange",
...
"peers": [
{ "id": "zoo", "url": "https://zoo.exchange", "appId": "exchange" },
{ "id": "pars", "url": "https://pars.market", "appId": "exchange" }
],
...
}
Mounted at /app/public/.well-known/exchange.json. The bundled file is the lower bound; ConfigMap is the upper.
7. Partner isolation rule
The partner brand preset and all partner/* repos MUST contain zero substring matches for lux, luxfi, hanzo, zoo, pars (case-insensitive) in:
- Brand strings (name, title, copyright, etc.)
- Domain references
- Federation peer URLs
- Code identifiers (excluding genuine OSS dependency imports like
import "github.com/luxfi/hsm")
Inverse rule: luxfi/*, hanzoai/*, zooai/*, and parsdao/* repos MUST contain zero references to the Partner's brand surfaces. OSS image refs (e.g. ghcr.io/luxfi/node pulled by the Partner for upstream code) are exempt.
This is a hard rule, not a guideline. The Partner is a regulated US securities ATS — source-level cross-pollination is a compliance issue, not a hygiene one.
Rationale
Why no multi-tenant runtime? A BRAND_ID=<id> env switch is operationally tempting but creates a single attack surface (compromise of upstream ships compromised brand to all tenants), a single CI bottleneck, and a single dependency-update cadence that doesn't match the per-org engineering teams' rhythms. Fork-and-pull is slower per change but distributes blast radius.
Why /.well-known/? IETF RFC 8615 is the standard URI path prefix for service discovery. Re-using it gives us free interop with existing HTTP tooling, CDN caching, and operator mental models.
Why empty peers by default? Source-level peer URLs encode trust decisions that change faster than source. ConfigMap-mounted peers let operators (and future on-chain registries) update the federation graph without code review on every change.
Backwards Compatibility
None required for the per-org sovereignty rules (§1–§3). The §4.1 split is additive and back-compat by design: legacy publishers that embed a brand OBJECT in their per-app file continue to work, and legacy consumers that read denormalized top-level fields (title, domain, url, github, brandId) continue to work against new publishers.
Previous co-located multi-brand presets in luxfi/exchange were removed in luxfi/exchange@df60f3d (2026-05-29); see commit message.
Reference Implementation
| Repo | Commit | Action |
|---|---|---|
luxfi/exchange | 0c14ebe | Removed config/brands/{hanzo,zoo,pars,partner,generic}.json and config/federation/ |
luxfi/bridge | dcb5be0 | Removed 4 tenant manifests + added .well-known/bridge.json |
luxfi/explore | 4dc352b04 | Removed Dockerfile.partner + multi-brand .env.example.* |
luxfi/build | 7aa17411f | Collapsed multi-brand Docker matrix to canonical Lux-only |
luxfi/wallet | 6df559bd | Removed Partner, etc. from chain comment |
luxfi/node | 534917ec9b, acabdccea3 | Removed Partner from code comments |
hanzoai/market | da7008e | Removed non-Hanzo presets; only Hanzo + generic remain |
hanzoai/exchange | 56ae5c8 | Swapped broken @hanzoai/brand (404) → @hanzo/[email protected]; ripped Zoo crossover (had been forked from Zoo) |
hanzoai/iam | 9a1fd61b | 13+ Partner refs removed; added .well-known/iam.json |
hanzoai/base-studio | c38131a | Added .well-known/base.json |
zoo-labs/zoo | e6bb40619 | Moved DAO images ghcr.io/luxfi/* → ghcr.io/zooai/* |
partner/exchange | chore/brand-audit-2026-05-29 | Hero.tsx fallback fix + 2 dead Lux*Card.tsx files removed |
partner/bd, partner/ta | (branches) | IAM_ENDPOINT default https://hanzo.id → in-cluster Partner IAM |
luxfi/brand-types | feat/well-known-split | §4.1 — adds WellKnownBrand + WellKnownApp types (v0.2.0) |
hanzoai/market | feat/well-known-brand-app-split | §4.1 — split generator + federation aggregator follows brand link with sha256 verification |
luxfi/exchange | feat/well-known-brand-app-split | §4.1 — split generator + brand.json + brandHash |
Security Considerations
- Brand spoofing: ConfigMap-mounted peer lists must be authenticated. Operators SHOULD restrict ConfigMap write access via RBAC.
- Discovery cache poisoning:
/.well-known/<appId>.jsonshould be served via TLS only. Stale peer entries are a minor liveness issue (stale links to dead exchanges); not a security issue. brandHashsemantics (§4.1): AbrandHashmismatch is NOT proof of compromise — the peer may have rebranded without re-publishing their advertised hash. Consumers MUST treat mismatch as a warning (e.g. an[unverified brand]badge) and MUST NOT refuse to render. Operators who want hard rejection on mismatch should layer that policy on top of the consumer SDK, not assume the protocol enforces it.- Hash sensitivity to serialization:
brandHashis computed over the exact bytes returned byGET /.well-known/brand.json. Intermediaries that re-serialize JSON (whitespace stripping, key reordering) will break verification. Operators SHOULD configure their CDN / ingress to serve the brand payload byte-for-byte, or accept thatbrandHashverification will be unavailable. - Regulatory: The Partner isolation rule is enforced socially today. A future LP MAY codify it as a CI check (
gh-actions/no-cross-brand-refsstyle).
See Also
- LP-0011 — Onchain Federation Registry (chain-native successor)
- HIP-0303 — Hanzo adoption pointer (records Hanzo-specific
@hanzo/brandpackage + commit table) - ZIP-0031 — Zoo adoption pointer (records
zooai/*GHCR isolation + commit table) - IETF RFC 8615 — Well-Known URIs
Copyright
Copyright and related rights waived via CC0.