LPsLux Proposals
Governance & Impact
LP-10

Brand Sovereignty and Federation Discovery

Final

Per-org brand data isolation, white-label by fork (not env-preset multi-tenant), `/.well-known/<appId>.json` federation discovery per IETF RFC 8615.

Type
Meta
Created
2026-05-29

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:

  1. 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.
  2. 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.
  3. 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

OrgRepo namespaceBrand data home
Luxluxfi/*only Lux brand (e.g. luxfi/exchange/config/brands/lux.json)
Hanzohanzoai/*only Hanzo brand (e.g. hanzoai/market/public/brands/hanzo.json)
Zoozooai/*only Zoo brand
Parsparsdao/*only Pars brand
Partnerpartner/*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:

AppCanonicalForks
Exchangeluxfi/exchangehanzoai/exchange, zooai/exchange, parsdao/exchange, partner/exchange
Markethanzoai/marketzooai/market, parsdao/market, luxfi/market (future)
Exploreluxfi/explorehanzoai/explore (L2), zooai/explore (L2), etc.
Bridgeluxfi/bridgezooai/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 — OPTIONAL sha256-<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 the brand link).

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:

EndpointSchemaOwned byChange rateRecommended cache
/.well-known/brand.jsonWellKnownBrand (identity only)Brand teamRarely (rebrand, logo, domain move)≥24 hours
/.well-known/<appId>.jsonWellKnownApp (federation graph)Deployment operatorWeekly (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:

  1. GET P/.well-known/<appId>.json → parse WellKnownApp payload.
  2. If payload.brand is a string URL: resolve against the response URL, then GET it → parse WellKnownBrand. If payload.brandHash is present, verify sha256(brandBytes) == payload.brandHash.
  3. If payload.brand is 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

RepoCommitAction
luxfi/exchange0c14ebeRemoved config/brands/{hanzo,zoo,pars,partner,generic}.json and config/federation/
luxfi/bridgedcb5be0Removed 4 tenant manifests + added .well-known/bridge.json
luxfi/explore4dc352b04Removed Dockerfile.partner + multi-brand .env.example.*
luxfi/build7aa17411fCollapsed multi-brand Docker matrix to canonical Lux-only
luxfi/wallet6df559bdRemoved Partner, etc. from chain comment
luxfi/node534917ec9b, acabdccea3Removed Partner from code comments
hanzoai/marketda7008eRemoved non-Hanzo presets; only Hanzo + generic remain
hanzoai/exchange56ae5c8Swapped broken @hanzoai/brand (404) → @hanzo/[email protected]; ripped Zoo crossover (had been forked from Zoo)
hanzoai/iam9a1fd61b13+ Partner refs removed; added .well-known/iam.json
hanzoai/base-studioc38131aAdded .well-known/base.json
zoo-labs/zooe6bb40619Moved DAO images ghcr.io/luxfi/*ghcr.io/zooai/*
partner/exchangechore/brand-audit-2026-05-29Hero.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-typesfeat/well-known-split§4.1 — adds WellKnownBrand + WellKnownApp types (v0.2.0)
hanzoai/marketfeat/well-known-brand-app-split§4.1 — split generator + federation aggregator follows brand link with sha256 verification
luxfi/exchangefeat/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>.json should be served via TLS only. Stale peer entries are a minor liveness issue (stale links to dead exchanges); not a security issue.
  • brandHash semantics (§4.1): A brandHash mismatch 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: brandHash is computed over the exact bytes returned by GET /.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 that brandHash verification 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-refs style).

See Also

  • LP-0011 — Onchain Federation Registry (chain-native successor)
  • HIP-0303 — Hanzo adoption pointer (records Hanzo-specific @hanzo/brand package + commit table)
  • ZIP-0031 — Zoo adoption pointer (records zooai/* GHCR isolation + commit table)
  • IETF RFC 8615 — Well-Known URIs

Copyright and related rights waived via CC0.