White-label sports betting platform framework. N operator brands. One codebase. Zero conditional code per client.
Live Demo · Storybook · GrandBet · EliteBet
The architecture thesis: operator brands are differentiated exclusively via ClientConfig JSON and CSS Custom Properties. No if-statements per client. No forks. No custom builds.
{
"brand": { "name": "GrandBet", "primary": "#1A7A4A" },
"features": { "casino": true, "esports": false },
"layout": { "borderRadius": "md", "sidebarWidth": 220 }
}→ Adding a new operator = creating a JSON file. Zero new code.
/* Shell applies to :root */
--color-primary: #1a7a4a;
--layout-border-radius: 8px;
/* Remote inherits via CSS cascade — no communication needed */
background: var(--color-primary);→ The remote doesn't know it's inside a shell. The browser resolves it.
Shell (host) ──loads──▶ sportsbook/SportsbookPage (remote)
↑
webpack.container.cjs generates remoteEntry.js
@module-federation/enhanced 2.0
→ Remotes deploy independently. Shell consumes at runtime.
| ADR | Decision | Status |
|---|---|---|
| ADR-001 | Turborepo + pnpm workspaces | Accepted |
| ADR-002 | Zod as config validation layer | Accepted |
| ADR-003 | CSS Custom Properties as theme contract | Accepted |
| ADR-004 | Module Federation 2.0 | Accepted |
| ADR-005 | Standalone webpack container for sportsbook | Accepted |
| ADR-006 | Custom Events for shell↔remote communication | Accepted |
openbet-core/
├── packages/
│ ├── config-schema/ # Zod schema + TypeScript types
│ ├── theme-engine/ # CSS vars runtime injection
│ └── ui/ # React component library + Storybook
├── apps/
│ ├── shell/ # MF Host — Next.js 16, port 3000
│ └── sportsbook/ # MF Remote — Next.js 16, port 3001
├── clients/
│ ├── grandbet.config.json
│ └── elitebet.config.json
├── docs/
│ ├── architecture/ # ADR-001 to ADR-006
│ └── guides/ # Getting started, deploy, adding clients
└── .claude/agents/ # AI agent roles: architect, mf-engineer, ui-engineer, config-eng
| Feature | GrandBet | EliteBet |
|---|---|---|
| Primary color | #1A7A4A (green) |
#4F46E5 (indigo) |
| Border radius | md — 8px |
lg — 16px |
| Casino | ✅ | ❌ |
| Live betting | ✅ | ✅ |
| E-Sports | ❌ | ✅ |
| Locale | pt-BR | pt-BR |
| Currency | BRL | BRL |
→ Same codebase. Same components. Different experience.
# Clone
git clone https://github.com/LucasReisVillasBoas/openbet-core.git
cd openbet-core
# Install
pnpm install
# Build packages
pnpm build
# Run
pnpm dev
# Shell: http://localhost:3000
# Sportsbook: http://localhost:3001
# Storybook: http://localhost:6006| Script | Description |
|---|---|
pnpm dev |
Start all apps in watch mode |
pnpm build |
Build all packages and apps |
pnpm validate |
Validate all client configs with Zod |
pnpm test:theme |
Run ThemeEngine assertions |
pnpm lint |
ESLint across all packages |
pnpm format |
Prettier across all files |
pnpm typecheck |
TypeScript strict check |
# 1. Create config
cp clients/grandbet.config.json clients/newclient.config.json
# 2. Edit brand, theme, features
# 3. Validate
pnpm validate
# 4. Deploy with env var
NEXT_PUBLIC_CLIENT_ID=client-newclientThat's it. No code changes required.
| Layer | Technology |
|---|---|
| Monorepo | Turborepo + pnpm workspaces |
| Framework | Next.js 16 (webpack mode) |
| Language | TypeScript 5 (strict) |
| UI | React 19 |
| Component library | Custom (@openbet/ui) |
| Styling | CSS Custom Properties |
| Module Federation | @module-federation/enhanced 2.0 |
| Schema validation | Zod |
| Component explorer | Storybook 8 |
| CI/CD | GitHub Actions + Vercel |
push → install → build → format:check → typecheck → lint → validate configs
↓
grandbet + elitebet
both must pass Zod
Packages build in dependency order: config-schema → theme-engine → ui → apps
| App | URL | Env |
|---|---|---|
| Shell (demo) | openbet-core-shell.vercel.app | DEMO_MODE=true |
| Grandbet (demo) | openbet-core-shell-grandbet.vercel.app | Grandbet Client |
| Elitebet (demo) | openbet-core-shell-elitebet.vercel.app | Elitebet Client |
| Sportsbook remote | openbet-core-sportsbook.vercel.app | MF remote |
| Storybook | openbet-core-ui.vercel.app | Static |
| Document | Description |
|---|---|
| docs/architecture/overview.md | Architecture overview |
| docs/architecture/module-federation.md | Module Federation deep dive |
| docs/architecture/bet-slip-context.md | BetSlip context — toggle, exclusivity, accumulator |
| docs/architecture/sport-filter-context.md | Sport filter context — sidebar → matches |
| docs/architecture/theme-engine.md | ThemeEngine — CSS vars injection |
| docs/packages/config-schema.md | @openbet/config-schema package |
| docs/packages/theme-engine.md | @openbet/theme-engine package |
| docs/guides/getting-started.md | First steps |
| docs/guides/adding-a-client.md | Adding a new operator |
| docs/guides/adding-a-component.md | Adding a UI component |
| docs/guides/deploy.md | Deploy on Vercel |
| docs/decisions.md | Decision log |
MIT — see LICENSE