Skip to content

Multi-Store Architecture ​

A single Maho Storefront Worker deployment can serve multiple stores, each with its own theme, component configuration, and isolated catalog data.

Fashion StoreTech Store
Fashion themeTech theme

Store Resolution ​

Configuration ​

stores.json ​

Maps store codes to theme and page config files:

json
{
  "stores": {
    "en": {
      "theme": "maho",
      "pageConfig": "page.json"
    },
    "sv_2": {
      "theme": "tech",
      "pageConfig": "page-tech.json"
    },
    "store_view_3": {
      "theme": "brew-beyond",
      "pageConfig": "page-brew-beyond.json"
    }
  },
  "defaultTheme": "maho",
  "defaultPageConfig": "page.json"
}

Store Registry ​

The store registry maps hostnames to store codes. It can be stored in KV (key: _stores) or configured as a Wrangler environment variable (DEMO_STORES):

json
[
  {"code": "en", "name": "Fashion", "url": "https://demo.example.com"},
  {"code": "sv_2", "name": "TechZone", "url": "https://demo2.example.com"},
  {"code": "store_view_3", "name": "Cafe", "url": "https://cafe.example.com"}
]

Multi-Domain API URLs ​

By default all stores share a single Maho backend (MAHO_API_URL). For multi-domain setups where each store has its own backend, add an apiUrl per store:

json
[
  {"code": "en", "name": "Fashion", "url": "https://www.domain1.com", "apiUrl": "https://api.domain1.com"},
  {"code": "sv_2", "name": "TechZone", "url": "https://www.domain2.com", "apiUrl": "https://api.domain2.com"}
]

When apiUrl is omitted, the store falls back to the global MAHO_API_URL binding. This keeps single-backend setups simple - just omit apiUrl from all entries.

The per-store API URL is used for:

  • All API client requests (product, category, CMS, blog fetches)
  • Media proxying (/media/* paths)
  • Admin redirect (/admin → backend admin panel)
  • Client-side window.MAHO_API_URL (cart, checkout, search JS)

Data Isolation ​

Each store's data is isolated in KV via key prefixes:

StoreKey ExampleContent
enen:product:tori-tankEnglish store product
sv_2sv_2:product:tori-tankTech store product
enen:configEnglish store config
sv_2sv_2:configTech store config

This ensures:

  • Products can have different prices per store
  • Categories can be structured differently
  • CMS content is store-specific
  • Config (currency, locale) varies per store

Render Context ​

Before rendering any page, the store context is set:

typescript
// In route handler
const storeCode = getCurrentStoreCode(hostname, storeRegistry);
setRenderStore(storeCode);

// All components now use this store's config
const variant = getVariant('product', 'gallery'); // Uses store's page config

The render context is a module-level variable - set once per request, read by all components during synchronous SSR.

Theme Switching ​

Each store renders with its theme's CSS custom properties:

html
<!-- Store "sv_2" renders with tech theme -->
<html data-theme="tech">

The [data-theme="tech"] selector activates the tech theme's CSS custom properties, overriding the default :root values. All DaisyUI components and utility classes automatically use the store's colors, fonts, and spacing.

Available Themes ​

Custom Themes ​

Create a theme-{name}.json file with full control over colors, fonts, typography, spacing, radii, and component styles. See theme.json for the schema.

Pre-baked Color Themes ​

18 ready-to-use color themes are included in palette-themes.json. These provide DaisyUI semantic color mappings that work out of the box:

Light themes: Sunrise, Amethyst, Skyline, Caramel, Ocean, Terracotta, Botanical, Coral, Harvest, Sandstone, Coastal, Electric, Autumn Hunt, Tropical, Citrus

Dark themes: Neon Dream, Cobalt, Charcoal

To use a pre-baked theme, reference its name in stores.json:

json
{
  "stores": {
    "en": { "theme": "coastal", "pageConfig": "page.json" }
  }
}

Pre-baked themes only set colors. They inherit the default theme's fonts, typography, spacing, and component styles.

Adding a New Store ​

  1. Add the store code and configuration to stores.json
  2. Choose a theme - use a pre-baked theme name or create a theme-{name}.json
  3. Create a page config (page-{name}.json) if needed
  4. Add hostname → store code mapping to the store registry
  5. Sync data for the new store: POST /sync (will fetch data for all registered stores)
  6. Rebuild CSS: bun run build:css (generates new theme CSS)
  7. Deploy: ./maho storefront:build --deploy

Source: src/index.tsx, src/page-config.ts, stores.json, palette-themes.json