Multi-Tenant Theming
Overview
Section titled “Overview”All tenants deploy from the same GitHub repo (midego1/Magic-e-VERSE) via Coolify. Each tenant gets its own visual identity through a three-layer theming system:
- CSS Theme Overrides — tenant-specific CSS variables loaded at build time
- Brand Context — runtime domain detection that injects brand colors and metadata
- Theme Config — TypeScript helpers for store name, CSS class, and theme identification
Architecture
Section titled “Architecture”┌──────────────────────────────────────────────────┐│ Single Codebase ││ (Magic-e-VERSE repo) │├──────────────────────────────────────────────────┤│ ││ storefront/src/themes/ ││ ├── base/variables.css ← shared defaults ││ ├── brinxx/override.css ← pink #CF0D65 ││ ├── jodasign/override.css ← yellow #fcb900 ││ ├── mondial/override.css ← orange #ef8232 ││ ├── logohorloge/override.css← orange #FF9900 ││ ├── kiber/override.css ← kiber sub-brand ││ ├── bovisales/override.css ← cyan #00b2dd ││ ├── desluis/override.css ← green #008856 ││ ├── default/override.css ← indigo (fallback) ││ └── index.ts ← theme loader ││ ││ storefront/src/lib/brands/types.ts ││ └── DEFAULT_BRANDS = { brinxx, jodasign, ... } ││ ││ storefront/src/config/theme.ts ││ └── getThemeName(), getStoreName(), ThemeName ││ │└──────────────┬───────────────────────────────────┘ │ Coolify deploys with different env vars ▼┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ Brinxx │ │ JoDa Sign │ │ Mondial │ ...│ THEME=brinxx│ │THEME=jodasign│ │THEME=mondial││ pink #CF0D65│ │yellow #fcb900│ │orange #ef823│└─────────────┘ └─────────────┘ └─────────────┘Layer 1: CSS Theme Overrides
Section titled “Layer 1: CSS Theme Overrides”Base Variables
Section titled “Base Variables”storefront/src/themes/base/variables.css defines all shared CSS custom properties with sensible defaults. Every theme inherits these and only overrides what it needs.
Key variables:
| Variable | Purpose | Example |
|---|---|---|
--theme-accent | Primary brand color | #fcb900 |
--theme-accent-dark | Hover/active state | #e5a800 |
--theme-accent-light | Light accent | #ffd54f |
--theme-dark | Dark text/backgrounds | #0a0a0a |
--theme-topbar-bg | Top bar background | #fcb900 |
--theme-topbar-text | Top bar text color | #0a0a0a |
--theme-button-primary | Primary button bg | #fcb900 |
--theme-button-primary-hover | Button hover | #e5a800 |
--theme-link-color | Link color | #2ea3f2 |
--theme-footer-bg | Footer background | #0a0a0a |
--theme-footer-text | Footer text | #ffffff |
--theme-price-color | Product price | #ef8232 |
Tenant Override Files
Section titled “Tenant Override Files”Each tenant’s override.css sets :root CSS variables to override the base:
:root { --theme-accent: #fcb900; /* Yellow */ --theme-accent-dark: #e5a800; --joda-primary: #fcb900; --joda-blue: #2ea3f2; /* Blue accent */ --theme-topbar-bg: #fcb900; --theme-button-primary: #fcb900;}:root { --theme-accent: #ef8232; /* Orange */ --mondial-green: #679d00; /* Green accent */ --mondial-dark: #3c3c3c; --theme-footer-bg: #3c3c3c; --theme-button-primary: #ef8232;}:root { --theme-accent: #c2185b; /* Pink */ --theme-accent-dark: #880e4f; --brinxx-primary: #CF0D65;}:root { --theme-accent: #008856; /* Green */ --theme-accent-dark: #005536; --theme-dark: #242424;}Theme Loader
Section titled “Theme Loader”storefront/src/themes/index.ts reads NEXT_PUBLIC_THEME and requires the correct override:
import './base/variables.css';
const themeName = process.env.NEXT_PUBLIC_THEME || 'default';
if (themeName === 'brinxx') { require('./brinxx/override.css');} else if (themeName === 'jodasign') { require('./jodasign/override.css');} else if (themeName === 'mondial') { require('./mondial/override.css');} else if (themeName === 'logohorloge') { require('./logohorloge/override.css'); require('./kiber/override.css');} else if (themeName === 'kiber') { require('./kiber/override.css');} else if (themeName === 'bovisales') { require('./bovisales/override.css');} else if (themeName === 'desluis') { require('./desluis/override.css');} else { require('./default/override.css');}Layer 2: Brand Context (Runtime)
Section titled “Layer 2: Brand Context (Runtime)”The brand context system detects which brand to display based on the current domain at runtime.
Detection Order
Section titled “Detection Order”// Priority:// 1. Exact domain match (brand.domain === window.location.hostname)// 2. additionalDomains array match// 3. Domain slug prefix match (e.g., "brinxx.example.com")// 4. Fallback: "magiceverse" default brandRegistered Brands
Section titled “Registered Brands”Defined in storefront/src/lib/brands/types.ts:
| Brand | Primary Domain | Primary Color | Type |
|---|---|---|---|
| Brinxx | brinxx.magicomniverse.online | #CF0D65 pink | owner |
| JoDa Sign | jodasign.magicomniverse.online | #fcb900 yellow | owner |
| Mondial Gifts | mondial.magicomniverse.online | #ef8232 orange | brand |
| Logohorloge | logohorloge.magicomniverse.online | #FF9900 orange | owner |
| Bovisales | bovisales.magicomniverse.online | #00b2dd cyan | owner |
| De Sluis | desluis.magicomniverse.online | #008856 green | owner |
| Default | default.magicomniverse.online | #4caf50 green | owner |
| Magiceverse | demo.magicomniverse.online | #8338B6 purple | owner |
| MOXZ | moxz.magiceverse.online | #CF0D65 pink | brand |
| Promotionalz | promotionalz.magiceverse.online | #e91e63 pink | brand |
What Brand Context Provides
Section titled “What Brand Context Provides”When the brand is detected, the BrandProvider injects CSS variables at runtime:
// Injected into document.documentElement.style:--brand-primary // primaryColor--brand-secondary // secondaryColor--brand-accent // accentColorIt also provides React context via useBrand():
const { brand } = useBrand();// brand.name, brand.logo, brand.companyName, brand.email, etc.Layer 3: Theme Config (TypeScript)
Section titled “Layer 3: Theme Config (TypeScript)”storefront/src/config/theme.ts provides typed helpers:
type ThemeName = 'default' | 'brinxx' | 'jodasign' | 'mondial' | 'logohorloge' | 'kiber' | 'bovisales' | 'desluis' | 'demo';
getThemeName() // Returns current NEXT_PUBLIC_THEMEgetThemeClass() // Returns "theme-jodasign" for CSS scopinggetStoreName() // Returns "JoDa Sign" display namethemeConfig // { storeName, themeName, themeClass, showPoweredBy }How the Three Layers Work Together
Section titled “How the Three Layers Work Together”-
Build time: Coolify builds the storefront Docker image with
NEXT_PUBLIC_THEME=jodasign. Next.js tree-shakes and only includes the jodasign CSS override. -
Server render:
getThemeName()returns"jodasign",getStoreName()returns"JoDa Sign". The HTML getsclass="theme-jodasign"on the body. -
Client hydration:
BrandProviderreadswindow.location.hostname→ matchesjodasign.magicomniverse.online→ finds the jodasign brand config → injects--brand-primary: #fcb900into:root. -
Result: CSS variables from both the theme override (build-time) and brand context (runtime) are active. Components use
var(--theme-accent)orvar(--brand-primary)and get the correct tenant colors.
Adding a New Tenant Theme
Section titled “Adding a New Tenant Theme”-
Create the CSS override file:
Terminal window mkdir storefront/src/themes/<tenant>/# Create override.css with :root { --theme-accent: #...; ... } -
Register in the theme loader (
storefront/src/themes/index.ts):} else if (themeName === '<tenant>') {require('./<tenant>/override.css');} -
Add to ThemeName type (
storefront/src/config/theme.ts):export type ThemeName = '...' | '<tenant>';// Also update validThemes array and getStoreName() switch -
Add brand config (
storefront/src/lib/brands/types.ts):<tenant>: {name: 'Tenant Name',slug: '<tenant>',domain: '<tenant>.magicomniverse.online',primaryColor: '#...',// ... full BrandConfig} -
Set Coolify build variable:
NEXT_PUBLIC_THEME=<tenant> -
Push to GitHub → Coolify auto-deploys.
Coolify Environment Variables
Section titled “Coolify Environment Variables”Each tenant’s storefront deployment in Coolify needs these build variables:
| Variable | Purpose | Example |
|---|---|---|
NEXT_PUBLIC_THEME | Theme CSS selection | jodasign |
NEXT_PUBLIC_BASE_URL | Storefront URL | https://jodasign.magicomniverse.online |
NEXT_PUBLIC_MEDUSA_BACKEND_URL | Backend API | https://admin-jodasign.magicomniverse.online |
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY | Medusa API key | pk_... |
NEXT_PUBLIC_DEFAULT_REGION | Default region | nl |
Current Tenant Theme Mapping
Section titled “Current Tenant Theme Mapping”| Tenant | NEXT_PUBLIC_THEME | Primary Color | Accent |
|---|---|---|---|
| brinxx | brinxx | #CF0D65 pink | #e91e63 |
| jodasign | jodasign | #fcb900 yellow | #2ea3f2 blue |
| mondial | mondial | #ef8232 orange | #679d00 green |
| logohorloge | logohorloge | #FF9900 orange | #E68A00 |
| bovisales | bovisales | #00b2dd cyan | #0084c7 |
| desluis | desluis | #008856 green | #005536 |
| default | default | #4caf50 green | #43a047 |
| demo | demo | (uses default) | (uses default) |
Key Files Reference
Section titled “Key Files Reference”| File | Purpose |
|---|---|
storefront/src/themes/base/variables.css | Base CSS variables (all themes inherit) |
storefront/src/themes/<tenant>/override.css | Tenant CSS overrides |
storefront/src/themes/index.ts | Theme loader (reads NEXT_PUBLIC_THEME) |
storefront/src/config/theme.ts | TypeScript theme helpers |
storefront/src/lib/brands/types.ts | Brand configs + domain mapping |
storefront/src/lib/brands/brand-context.tsx | Runtime brand detection + CSS injection |
storefront/src/lib/brands/brand-css.ts | Dynamic CSS generation |
storefront/Dockerfile | Build args for NEXT_PUBLIC_* vars |
Troubleshooting
Section titled “Troubleshooting”Tenant shows “Welkom bij Magiceverse” instead of its own branding
Section titled “Tenant shows “Welkom bij Magiceverse” instead of its own branding”Cause: The brand-context falls back to magiceverse when it can’t match the domain.
Fix: Check that the tenant’s domain is registered in DEFAULT_BRANDS in types.ts, either as the domain field or in additionalDomains.
Theme colors don’t change after updating override.css
Section titled “Theme colors don’t change after updating override.css”Cause: NEXT_PUBLIC_THEME is baked at build time.
Fix: Trigger a full rebuild in Coolify (not just a restart). Verify NEXT_PUBLIC_THEME is set as a build variable.
Brand shows wrong logo/company info
Section titled “Brand shows wrong logo/company info”Cause: The DEFAULT_BRANDS entry has incorrect data.
Fix: Update the brand config in storefront/src/lib/brands/types.ts and push.
Logohorloge shows kiber styles leaking
Section titled “Logohorloge shows kiber styles leaking”Cause: Kiber override CSS is loaded alongside logohorloge.
Fix: Ensure kiber styles are properly scoped with body.brand-kiber selector.