Skip to content

Multi-Tenant Theming

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:

  1. CSS Theme Overrides — tenant-specific CSS variables loaded at build time
  2. Brand Context — runtime domain detection that injects brand colors and metadata
  3. Theme Config — TypeScript helpers for store name, CSS class, and theme identification

┌──────────────────────────────────────────────────┐
│ 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│
└─────────────┘ └─────────────┘ └─────────────┘

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:

VariablePurposeExample
--theme-accentPrimary brand color#fcb900
--theme-accent-darkHover/active state#e5a800
--theme-accent-lightLight accent#ffd54f
--theme-darkDark text/backgrounds#0a0a0a
--theme-topbar-bgTop bar background#fcb900
--theme-topbar-textTop bar text color#0a0a0a
--theme-button-primaryPrimary button bg#fcb900
--theme-button-primary-hoverButton hover#e5a800
--theme-link-colorLink color#2ea3f2
--theme-footer-bgFooter background#0a0a0a
--theme-footer-textFooter text#ffffff
--theme-price-colorProduct price#ef8232

Each tenant’s override.css sets :root CSS variables to override the base:

storefront/src/themes/jodasign/override.css
: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;
}

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');
}

The brand context system detects which brand to display based on the current domain at runtime.

storefront/src/lib/brands/brand-context.tsx
// 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 brand

Defined in storefront/src/lib/brands/types.ts:

BrandPrimary DomainPrimary ColorType
Brinxxbrinxx.magicomniverse.online#CF0D65 pinkowner
JoDa Signjodasign.magicomniverse.online#fcb900 yellowowner
Mondial Giftsmondial.magicomniverse.online#ef8232 orangebrand
Logohorlogelogohorloge.magicomniverse.online#FF9900 orangeowner
Bovisalesbovisales.magicomniverse.online#00b2dd cyanowner
De Sluisdesluis.magicomniverse.online#008856 greenowner
Defaultdefault.magicomniverse.online#4caf50 greenowner
Magiceversedemo.magicomniverse.online#8338B6 purpleowner
MOXZmoxz.magiceverse.online#CF0D65 pinkbrand
Promotionalzpromotionalz.magiceverse.online#e91e63 pinkbrand

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 // accentColor

It also provides React context via useBrand():

const { brand } = useBrand();
// brand.name, brand.logo, brand.companyName, brand.email, etc.

storefront/src/config/theme.ts provides typed helpers:

type ThemeName = 'default' | 'brinxx' | 'jodasign' | 'mondial'
| 'logohorloge' | 'kiber' | 'bovisales' | 'desluis' | 'demo';
getThemeName() // Returns current NEXT_PUBLIC_THEME
getThemeClass() // Returns "theme-jodasign" for CSS scoping
getStoreName() // Returns "JoDa Sign" display name
themeConfig // { storeName, themeName, themeClass, showPoweredBy }

  1. Build time: Coolify builds the storefront Docker image with NEXT_PUBLIC_THEME=jodasign. Next.js tree-shakes and only includes the jodasign CSS override.

  2. Server render: getThemeName() returns "jodasign", getStoreName() returns "JoDa Sign". The HTML gets class="theme-jodasign" on the body.

  3. Client hydration: BrandProvider reads window.location.hostname → matches jodasign.magicomniverse.online → finds the jodasign brand config → injects --brand-primary: #fcb900 into :root.

  4. Result: CSS variables from both the theme override (build-time) and brand context (runtime) are active. Components use var(--theme-accent) or var(--brand-primary) and get the correct tenant colors.


  1. Create the CSS override file:

    Terminal window
    mkdir storefront/src/themes/<tenant>/
    # Create override.css with :root { --theme-accent: #...; ... }
  2. Register in the theme loader (storefront/src/themes/index.ts):

    } else if (themeName === '<tenant>') {
    require('./<tenant>/override.css');
    }
  3. Add to ThemeName type (storefront/src/config/theme.ts):

    export type ThemeName = '...' | '<tenant>';
    // Also update validThemes array and getStoreName() switch
  4. Add brand config (storefront/src/lib/brands/types.ts):

    <tenant>: {
    name: 'Tenant Name',
    slug: '<tenant>',
    domain: '<tenant>.magicomniverse.online',
    primaryColor: '#...',
    // ... full BrandConfig
    }
  5. Set Coolify build variable:

    NEXT_PUBLIC_THEME=<tenant>
  6. Push to GitHub → Coolify auto-deploys.


Each tenant’s storefront deployment in Coolify needs these build variables:

VariablePurposeExample
NEXT_PUBLIC_THEMETheme CSS selectionjodasign
NEXT_PUBLIC_BASE_URLStorefront URLhttps://jodasign.magicomniverse.online
NEXT_PUBLIC_MEDUSA_BACKEND_URLBackend APIhttps://admin-jodasign.magicomniverse.online
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEYMedusa API keypk_...
NEXT_PUBLIC_DEFAULT_REGIONDefault regionnl
TenantNEXT_PUBLIC_THEMEPrimary ColorAccent
brinxxbrinxx#CF0D65 pink#e91e63
jodasignjodasign#fcb900 yellow#2ea3f2 blue
mondialmondial#ef8232 orange#679d00 green
logohorlogelogohorloge#FF9900 orange#E68A00
bovisalesbovisales#00b2dd cyan#0084c7
desluisdesluis#008856 green#005536
defaultdefault#4caf50 green#43a047
demodemo(uses default)(uses default)

FilePurpose
storefront/src/themes/base/variables.cssBase CSS variables (all themes inherit)
storefront/src/themes/<tenant>/override.cssTenant CSS overrides
storefront/src/themes/index.tsTheme loader (reads NEXT_PUBLIC_THEME)
storefront/src/config/theme.tsTypeScript theme helpers
storefront/src/lib/brands/types.tsBrand configs + domain mapping
storefront/src/lib/brands/brand-context.tsxRuntime brand detection + CSS injection
storefront/src/lib/brands/brand-css.tsDynamic CSS generation
storefront/DockerfileBuild args for NEXT_PUBLIC_* vars

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.

Cause: The DEFAULT_BRANDS entry has incorrect data.

Fix: Update the brand config in storefront/src/lib/brands/types.ts and push.

Cause: Kiber override CSS is loaded alongside logohorloge.

Fix: Ensure kiber styles are properly scoped with body.brand-kiber selector.