mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
feat: white-label multi-tenant bridge UI
- Add tenant config system (lib/tenant.ts) — hostname → branding + MPC mode - Tenants: bridge.lux.network, bridge.pars.network, bridge.hanzo.ai, bridge.zoo.ngo - Middleware passes x-tenant-hostname header for SSR + API routes - /api/tenant endpoint resolves tenant config by hostname - TenantProvider React context applies CSS vars (--brand-primary, --brand-accent) - Layout reads tenant config server-side, injects CSS vars + favicon + metadata - Header supports logoSrc prop for custom logo override per tenant - next.config.js allows remote logo images from all tenant domains - k8s ingress: added rules for bridge.pars.network, bridge.hanzo.ai, bridge.zoo.ngo - MPC backend pluggable: lux-mpc (3-node cluster) or t-chain (ThresholdVM) - Self-hosted deployments supported via BRIDGE_TENANT_CONFIG env var Anyone can point a domain at the bridge + add config → instant white-label bridge. All tenants share the same lux-mpc backend (no per-network SPOF).
This commit is contained in:
@@ -62,7 +62,16 @@ module.exports = (phase, { defaultConfig }) => {
|
||||
{
|
||||
protocol: "http",
|
||||
hostname: "localhost",
|
||||
}
|
||||
},
|
||||
// White-label tenant logo domains
|
||||
{ protocol: "https", hostname: "lux.network" },
|
||||
{ protocol: "https", hostname: "pars.network" },
|
||||
{ protocol: "https", hostname: "hanzo.ai" },
|
||||
{ protocol: "https", hostname: "zoo.ngo" },
|
||||
{ protocol: "https", hostname: "*.lux.network" },
|
||||
{ protocol: "https", hostname: "*.hanzo.ai" },
|
||||
{ protocol: "https", hostname: "*.pars.network" },
|
||||
{ protocol: "https", hostname: "*.zoo.ngo" },
|
||||
],
|
||||
},
|
||||
compiler: {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { resolveTenant } from '@/lib/tenant'
|
||||
|
||||
export const runtime = 'edge'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
// Hostname from query param (client-side) or x-tenant header (set by middleware)
|
||||
const hostname =
|
||||
req.nextUrl.searchParams.get('hostname') ||
|
||||
req.headers.get('x-tenant-hostname') ||
|
||||
req.headers.get('host') ||
|
||||
'bridge.lux.network'
|
||||
|
||||
const tenant = resolveTenant(hostname)
|
||||
|
||||
return NextResponse.json(tenant, {
|
||||
headers: { 'Cache-Control': 'public, max-age=60, stale-while-revalidate=300' },
|
||||
})
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import Main from '@/components/main'
|
||||
import Header from '@/components/header'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import getBridgeSettings from '@/util/getBridgeSettings'
|
||||
import { resolveTenant } from '@/lib/tenant'
|
||||
import { headers } from 'next/headers'
|
||||
|
||||
export const metadata = { ..._metadata }
|
||||
|
||||
@@ -29,10 +31,41 @@ const ConnectedWallets = dynamic(
|
||||
const RootLayout: React.FC<PropsWithChildren> = async ({ children }) => {
|
||||
const settings = await getBridgeSettings()
|
||||
|
||||
// Resolve tenant config server-side from incoming hostname
|
||||
const headersList = headers()
|
||||
const hostname =
|
||||
headersList.get('x-tenant-hostname') ||
|
||||
headersList.get('host') ||
|
||||
'bridge.lux.network'
|
||||
const tenant = resolveTenant(hostname)
|
||||
|
||||
// Build per-tenant siteDef override
|
||||
const tenantSiteDef = {
|
||||
...siteDef,
|
||||
currentAs: `https://${tenant.hostname}`,
|
||||
...(tenant.links?.home ? { home: tenant.links.home } : {}),
|
||||
}
|
||||
|
||||
// Per-tenant metadata
|
||||
const tenantMetadata = {
|
||||
..._metadata,
|
||||
title: tenant.name,
|
||||
description: `${tenant.name} — cross-chain bridge`,
|
||||
...(tenant.faviconUrl ? { icons: { icon: tenant.faviconUrl } } : {}),
|
||||
}
|
||||
|
||||
return (
|
||||
<html lang='en' suppressHydrationWarning className='dark' style={{backgroundColor: '#000'}}>
|
||||
<head>
|
||||
<base target='_blank' />
|
||||
{/* Inject CSS vars for white-label theming */}
|
||||
<style>{`
|
||||
:root {
|
||||
--brand-primary: ${tenant.primaryColor};
|
||||
--brand-accent: ${tenant.accentColor || tenant.primaryColor};
|
||||
}
|
||||
`}</style>
|
||||
{tenant.faviconUrl && <link rel='icon' href={tenant.faviconUrl} />}
|
||||
</head>
|
||||
<body
|
||||
suppressHydrationWarning
|
||||
@@ -40,7 +73,7 @@ const RootLayout: React.FC<PropsWithChildren> = async ({ children }) => {
|
||||
>
|
||||
{settings ? (
|
||||
<Contexts settings={settings}>
|
||||
<Header siteDef={siteDef} logoVariant='logo-only'>
|
||||
<Header siteDef={tenantSiteDef} logoVariant='logo-only' logoSrc={tenant.logoUrl}>
|
||||
<ConnectedWallets
|
||||
connectButtonVariant='outline'
|
||||
showWalletIcon={false}
|
||||
@@ -62,7 +95,7 @@ const RootLayout: React.FC<PropsWithChildren> = async ({ children }) => {
|
||||
</div>
|
||||
</Main>
|
||||
)}
|
||||
<Footer siteDef={siteDef} />
|
||||
<Footer siteDef={tenantSiteDef} />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ import { SendErrorMessage } from '@/lib/telegram'
|
||||
import { THEME_COLORS, type ThemeData } from '../Models/Theme'
|
||||
import { AuthProvider } from '@/context/authContext'
|
||||
import { SettingsProvider } from '@/context/settings'
|
||||
import { TenantProvider } from '@/context/tenantContext'
|
||||
import { FeeProvider } from '@/context/feeContext'
|
||||
import { JotaiProvider } from '@/context/jotaiContext'
|
||||
import { EthersProvider } from '@/context/ethersContext'
|
||||
@@ -63,6 +64,7 @@ const Contexts: React.FC<
|
||||
|
||||
return (
|
||||
<SWRConfig value={{ revalidateOnFocus: false }}>
|
||||
<TenantProvider>
|
||||
<IntercomProvider appId={INTERCOM_APP_ID} initializeDelay={2500}>
|
||||
{themeData && <ColorSchema themeData={themeData} />}
|
||||
<SettingsProvider settings={new BridgeAppSettings(_settings)}>
|
||||
@@ -90,6 +92,7 @@ const Contexts: React.FC<
|
||||
</AuthProvider>
|
||||
</SettingsProvider>
|
||||
</IntercomProvider>
|
||||
</TenantProvider>
|
||||
</SWRConfig>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { type PropsWithChildren } from 'react'
|
||||
import Image from 'next/image'
|
||||
|
||||
import type { LinkDef } from '@hanzo/ui/types'
|
||||
|
||||
@@ -12,6 +13,8 @@ const DesktopHeader: React.FC<
|
||||
className?: string
|
||||
noAuth?: boolean
|
||||
logoVariant?: LogoVariant
|
||||
/** White-label: override logo with custom image URL */
|
||||
logoSrc?: string
|
||||
} & PropsWithChildren
|
||||
> = ({
|
||||
links,
|
||||
@@ -19,11 +22,10 @@ const DesktopHeader: React.FC<
|
||||
noAuth = false,
|
||||
children,
|
||||
logoVariant = 'text-only',
|
||||
logoSrc,
|
||||
}) => {
|
||||
const [isMenuOpened, setIsMenuOpen] = React.useState(false)
|
||||
|
||||
// TODO move 13px into a size class and configure twMerge to recognize say, 'text-size-nav'
|
||||
// (vs be beat out by 'text-color-nav')
|
||||
return (
|
||||
<header
|
||||
id="DESKTOP_HEADER"
|
||||
@@ -40,14 +42,19 @@ const DesktopHeader: React.FC<
|
||||
'mx-3 md:mx-6 w-full max-w-screen'
|
||||
}
|
||||
>
|
||||
<Logo
|
||||
size={logoVariant === 'logo-only' ? 'sm' : 'sm'}
|
||||
href="/"
|
||||
outerClx="flex"
|
||||
key="two"
|
||||
variant={logoVariant}
|
||||
/>
|
||||
{/* md or larger */}
|
||||
{logoSrc ? (
|
||||
<a href="/" className="flex items-center">
|
||||
<Image src={logoSrc} alt="Bridge Logo" height={32} width={120} style={{ objectFit: 'contain' }} />
|
||||
</a>
|
||||
) : (
|
||||
<Logo
|
||||
size={logoVariant === 'logo-only' ? 'sm' : 'sm'}
|
||||
href="/"
|
||||
outerClx="flex"
|
||||
key="two"
|
||||
variant={logoVariant}
|
||||
/>
|
||||
)}
|
||||
<div className=""></div>
|
||||
<div className="flex items-center">{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -14,27 +14,27 @@ const Header: React.FC<{
|
||||
siteDef: SiteDef
|
||||
className?: string
|
||||
logoVariant?: LogoVariant
|
||||
/** White-label: custom logo image URL (overrides default Lux logo) */
|
||||
logoSrc?: string
|
||||
} & PropsWithChildren> = ({
|
||||
siteDef,
|
||||
className = '',
|
||||
children,
|
||||
logoVariant='text-only'
|
||||
logoVariant='text-only',
|
||||
logoSrc,
|
||||
}) => {
|
||||
|
||||
// TODO
|
||||
//const [open, setOpen] = React.useState<boolean>(false);
|
||||
|
||||
const { nav: { common, featured }, currentAs, noAuth } = siteDef
|
||||
const links = (featured) ? [...common, ...featured] : common
|
||||
|
||||
return (<>
|
||||
<DesktopHeader
|
||||
// className={cn(className, 'hidden sm:flex')}
|
||||
className='flex'
|
||||
links={links}
|
||||
currentAs={currentAs}
|
||||
noAuth={noAuth}
|
||||
logoVariant={logoVariant}
|
||||
logoSrc={logoSrc}
|
||||
>
|
||||
{children}
|
||||
</DesktopHeader>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
'use client'
|
||||
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react'
|
||||
import type { ResolvedTenant } from '@/lib/tenant'
|
||||
|
||||
interface TenantContextValue {
|
||||
tenant: ResolvedTenant | null
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const TenantContext = createContext<TenantContextValue>({
|
||||
tenant: null,
|
||||
loading: true,
|
||||
})
|
||||
|
||||
export function TenantProvider({ children }: { children: React.ReactNode }) {
|
||||
const [tenant, setTenant] = useState<ResolvedTenant | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const hostname = window.location.hostname
|
||||
fetch(`/api/tenant?hostname=${encodeURIComponent(hostname)}`)
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
setTenant(data)
|
||||
// Apply CSS custom properties for white-label theming
|
||||
if (data.primaryColor) {
|
||||
document.documentElement.style.setProperty('--brand-primary', data.primaryColor)
|
||||
}
|
||||
if (data.accentColor) {
|
||||
document.documentElement.style.setProperty('--brand-accent', data.accentColor)
|
||||
}
|
||||
})
|
||||
.catch(() => { /* use defaults */ })
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<TenantContext.Provider value={{ tenant, loading }}>
|
||||
{children}
|
||||
</TenantContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useTenant() {
|
||||
return useContext(TenantContext)
|
||||
}
|
||||
|
||||
/** Server-side tenant resolution helper for layout.tsx */
|
||||
export async function fetchTenantServer(hostname: string): Promise<ResolvedTenant | null> {
|
||||
try {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'
|
||||
const res = await fetch(
|
||||
`${baseUrl}/api/tenant?hostname=${encodeURIComponent(hostname)}`,
|
||||
{ next: { revalidate: 60 } }
|
||||
)
|
||||
if (!res.ok) return null
|
||||
return res.json()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* White-label multi-tenant bridge configuration.
|
||||
*
|
||||
* Tenant config is resolved by hostname at request time.
|
||||
* MPC backend is pluggable: "lux-mpc" (3-node cluster) or "t-chain" (ThresholdVM).
|
||||
* Anyone can self-host by pointing a domain at the bridge and providing config.
|
||||
*/
|
||||
|
||||
export type MpcMode = 'lux-mpc' | 't-chain' | 'custom'
|
||||
|
||||
export interface TenantConfig {
|
||||
/** Display name shown in UI */
|
||||
name: string
|
||||
/** Logo image URL */
|
||||
logoUrl: string
|
||||
/** Favicon URL */
|
||||
faviconUrl?: string
|
||||
/** Brand primary color (hex) */
|
||||
primaryColor: string
|
||||
/** Brand secondary/accent color (hex) */
|
||||
accentColor?: string
|
||||
/** Which MPC backend to use */
|
||||
mpcMode: MpcMode
|
||||
/** Override MPC endpoint (for "custom" mode or self-hosted) */
|
||||
mpcEndpoint?: string
|
||||
/** Network environment */
|
||||
network: 'mainnet' | 'testnet'
|
||||
/** Allowed source/destination chain IDs (undefined = all) */
|
||||
allowedChains?: string[]
|
||||
/** API server URL override */
|
||||
apiUrl?: string
|
||||
/** External links */
|
||||
links?: {
|
||||
home?: string
|
||||
explorer?: string
|
||||
docs?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface ResolvedTenant extends TenantConfig {
|
||||
hostname: string
|
||||
}
|
||||
|
||||
// Default tenant (lux bridge)
|
||||
export const DEFAULT_TENANT: TenantConfig = {
|
||||
name: 'Lux Bridge',
|
||||
logoUrl: 'https://lux.network/images/logo.png',
|
||||
primaryColor: '#0055ff',
|
||||
accentColor: '#00ccff',
|
||||
mpcMode: 'lux-mpc',
|
||||
network: 'mainnet',
|
||||
links: {
|
||||
home: 'https://lux.network',
|
||||
explorer: 'https://explore.lux.network',
|
||||
docs: 'https://docs.lux.network',
|
||||
},
|
||||
}
|
||||
|
||||
// Static tenant registry — add hostname → config.
|
||||
// For self-hosted deployments: set BRIDGE_TENANT_CONFIG env var with JSON.
|
||||
export const TENANT_REGISTRY: Record<string, TenantConfig> = {
|
||||
'bridge.lux.network': {
|
||||
name: 'Lux Bridge',
|
||||
logoUrl: 'https://lux.network/images/logo.png',
|
||||
faviconUrl: 'https://lux.network/favicon.ico',
|
||||
primaryColor: '#0055ff',
|
||||
accentColor: '#00ccff',
|
||||
mpcMode: 'lux-mpc',
|
||||
network: 'mainnet',
|
||||
links: {
|
||||
home: 'https://lux.network',
|
||||
explorer: 'https://explore.lux.network',
|
||||
docs: 'https://docs.lux.network',
|
||||
},
|
||||
},
|
||||
'bridge-test.lux.network': {
|
||||
name: 'Lux Bridge (Testnet)',
|
||||
logoUrl: 'https://lux.network/images/logo.png',
|
||||
primaryColor: '#0055ff',
|
||||
accentColor: '#00ccff',
|
||||
mpcMode: 'lux-mpc',
|
||||
network: 'testnet',
|
||||
links: {
|
||||
home: 'https://lux.network',
|
||||
explorer: 'https://explore.lux-test.network',
|
||||
},
|
||||
},
|
||||
'bridge.pars.network': {
|
||||
name: 'Pars Bridge',
|
||||
logoUrl: 'https://pars.network/logo.svg',
|
||||
faviconUrl: 'https://pars.network/favicon.ico',
|
||||
primaryColor: '#00cc88',
|
||||
accentColor: '#00ffaa',
|
||||
mpcMode: 'lux-mpc',
|
||||
network: 'mainnet',
|
||||
allowedChains: ['PARS', 'LUX', 'ETHEREUM', 'BSC'],
|
||||
links: {
|
||||
home: 'https://pars.network',
|
||||
explorer: 'https://explore.pars.network',
|
||||
},
|
||||
},
|
||||
'bridge.hanzo.ai': {
|
||||
name: 'Hanzo Bridge',
|
||||
logoUrl: 'https://hanzo.ai/logo.svg',
|
||||
faviconUrl: 'https://hanzo.ai/favicon.ico',
|
||||
primaryColor: '#fd4444',
|
||||
accentColor: '#ff6666',
|
||||
mpcMode: 'lux-mpc',
|
||||
network: 'mainnet',
|
||||
links: {
|
||||
home: 'https://hanzo.ai',
|
||||
explorer: 'https://explore.hanzo.ai',
|
||||
docs: 'https://docs.hanzo.ai',
|
||||
},
|
||||
},
|
||||
'bridge.zoo.ngo': {
|
||||
name: 'Zoo Bridge',
|
||||
logoUrl: 'https://zoo.ngo/logo.svg',
|
||||
faviconUrl: 'https://zoo.ngo/favicon.ico',
|
||||
primaryColor: '#22c55e',
|
||||
accentColor: '#4ade80',
|
||||
mpcMode: 'lux-mpc',
|
||||
network: 'mainnet',
|
||||
links: {
|
||||
home: 'https://zoo.ngo',
|
||||
explorer: 'https://explore.zoo.ngo',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve tenant config from hostname.
|
||||
* Falls back to env-configured tenants, then DEFAULT_TENANT.
|
||||
*/
|
||||
export function resolveTenant(hostname: string): ResolvedTenant {
|
||||
// Strip port if present (e.g. localhost:3000)
|
||||
const host = hostname.split(':')[0]
|
||||
|
||||
// 1. Check static registry
|
||||
const config = TENANT_REGISTRY[host]
|
||||
if (config) {
|
||||
return { ...config, hostname: host }
|
||||
}
|
||||
|
||||
// 2. Check env var override (for self-hosted deployments)
|
||||
const envConfig = process.env.BRIDGE_TENANT_CONFIG
|
||||
if (envConfig) {
|
||||
try {
|
||||
const parsed = JSON.parse(envConfig) as Record<string, TenantConfig>
|
||||
if (parsed[host]) {
|
||||
return { ...parsed[host], hostname: host }
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Default
|
||||
return { ...DEFAULT_TENANT, hostname: host }
|
||||
}
|
||||
|
||||
/**
|
||||
* MPC endpoint resolution.
|
||||
* lux-mpc → cluster endpoint (set via env or default)
|
||||
* t-chain → ThresholdVM RPC on T-chain
|
||||
* custom → tenant-specified endpoint
|
||||
*/
|
||||
export function getMpcEndpoint(tenant: TenantConfig): string {
|
||||
if (tenant.mpcEndpoint) return tenant.mpcEndpoint
|
||||
|
||||
switch (tenant.mpcMode) {
|
||||
case 't-chain':
|
||||
return (
|
||||
process.env.TCHAIN_MPC_ENDPOINT || 'https://api.lux.network/ext/bc/T'
|
||||
)
|
||||
case 'lux-mpc':
|
||||
default:
|
||||
return process.env.MPC_ENDPOINT || 'http://mpc-node-0:6000'
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,23 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
// Add cors
|
||||
export async function middleware(req: NextRequest) {
|
||||
const origin = req.headers.get("origin") || "";
|
||||
const hostname = req.headers.get("host") || "bridge.lux.network";
|
||||
|
||||
const response = NextResponse.next();
|
||||
|
||||
// CORS
|
||||
response.headers.set("Access-Control-Allow-Origin", origin);
|
||||
response.headers.set(
|
||||
"Access-Control-Allow-Methods",
|
||||
"GET,HEAD,POST,PUT,DELETE"
|
||||
);
|
||||
response.headers.set("Access-Control-Allow-Methods", "GET,HEAD,POST,PUT,DELETE");
|
||||
response.headers.set("Access-Control-Allow-Headers", "Content-Type");
|
||||
response.headers.set("Access-Control-Allow-Credentials", "true");
|
||||
|
||||
// Propagate tenant hostname for SSR + API routes
|
||||
response.headers.set("x-tenant-hostname", hostname);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Apply middleware to all API routes
|
||||
export const config = {
|
||||
matcher: "/api/:path*",
|
||||
matcher: ["/api/:path*", "/((?!_next/static|_next/image|favicon.ico).*)"],
|
||||
};
|
||||
|
||||
+48
-5
@@ -155,9 +155,9 @@ data:
|
||||
backup_dir: /data/mpc/backups
|
||||
peers.json: |
|
||||
{
|
||||
"node0": "0ce02715-0ead-48ef-9772-2583316cc860",
|
||||
"node1": "c95c340e-5a18-472d-b9b0-5ac68218213a",
|
||||
"node2": "ac37e85f-caca-4bee-8a3a-49a0fe35abff"
|
||||
"node0": "lux-mainnet-node0",
|
||||
"node1": "lux-mainnet-node1",
|
||||
"node2": "lux-mainnet-node2"
|
||||
}
|
||||
node0_identity.json: |
|
||||
{"node_name":"node0","node_id":"0ce02715-0ead-48ef-9772-2583316cc860","public_key":"96e47112bab4cb83e59a0a222964fad096f02a9dc52e449097f8991470a141ab"}
|
||||
@@ -522,8 +522,18 @@ spec:
|
||||
- hosts:
|
||||
- bridge.lux.network
|
||||
- bridge-api.lux.network
|
||||
secretName: bridge-tls
|
||||
secretName: bridge-lux-tls
|
||||
- hosts:
|
||||
- bridge.pars.network
|
||||
secretName: bridge-pars-tls
|
||||
- hosts:
|
||||
- bridge.hanzo.ai
|
||||
secretName: bridge-hanzo-tls
|
||||
- hosts:
|
||||
- bridge.zoo.ngo
|
||||
secretName: bridge-zoo-tls
|
||||
rules:
|
||||
# Lux Network (primary)
|
||||
- host: bridge.lux.network
|
||||
http:
|
||||
paths:
|
||||
@@ -543,4 +553,37 @@ spec:
|
||||
service:
|
||||
name: bridge-server
|
||||
port:
|
||||
number: 3000
|
||||
number: 3000
|
||||
# Pars Network — white-label, same bridge-ui backend
|
||||
- host: bridge.pars.network
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: bridge-ui
|
||||
port:
|
||||
number: 80
|
||||
# Hanzo — white-label
|
||||
- host: bridge.hanzo.ai
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: bridge-ui
|
||||
port:
|
||||
number: 80
|
||||
# Zoo — white-label
|
||||
- host: bridge.zoo.ngo
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: bridge-ui
|
||||
port:
|
||||
number: 80
|
||||
Reference in New Issue
Block a user