---
title: "Brand Setup"
subtitle: "Setting up a new brand environment"
description: "Complete guide to creating a brand instance: the brand.json manifest, the theme.css token overrides, logos, and user accounts."
author: "Studio"
section: "Docs"
layer: "app"
subsection: "Site Management"
order: 6
status: "published"
access: "admin"
brand: "internal"
---

This guide covers how to set up a new brand environment from scratch. When a brand user logs in, the system automatically loads their theme, swaps the logo, updates the home link, and filters the sidebar navigation.

**Scope.** This walkthrough is for brand instances hosted inside BrandOS — auth-driven theming on the docs site. A standalone consumer site (like the studio site) doesn't use brand instances: it loads one `theme.css` of its own after the engine CSS. See [Setup](setup.html) for that model.

A brand instance is two files plus assets:

| File | Holds | Owner |
|------|-------|-------|
| `cms/brands/<brand>/brand.json` | Admin settings: name, description, Google Fonts URL, footer text, optional favicon and OG image paths | Set at onboarding, rarely touched |
| `cms/brands/<brand>/assets/theme.css` | Designer settings: font slots and roles, palette, semantic colours, dark mode | Iterated during design |

The generator reads every manifest and builds `assets/js/theme-config.js` from them on each build. That file is generated output. Never edit it by hand.

By Default itself runs on the same model: instance 0's manifest and theme live at `cms/brands/bydefault/`, and the engine (`design-system.css`) ships neutral defaults.

---

## What Brands See

When a brand logs in, the following elements personalise automatically:

| Element | Default (team/admin) | Brand |
|---------|---------------------|--------|
| **Logo** | By Default | Brand logo from `brand-name/assets/` |
| **Home link** | `index.html` | `brand-name/index.html` |
| **Theme** | By Default (instance 0's `theme.css`) | Brand `theme.css` |
| **Fonts** | Zalando Sans + ES Face (instance 0's `theme.css`) | Per the brand's `theme.css` and `brand.json` fonts URL |
| **Sidebar** | Full navigation | Auto-filtered by page access |

All of this is driven by the user's `brandFolder` value in Netlify Identity.

---

## Quick Checklist

1. Copy `cms/brands/brand-template/` to `cms/brands/your-brand/`
2. Edit `cms/brands/your-brand/brand.json`: name, description, fonts URL, footer
3. Update `cms/brands/your-brand/_defaults.md` with `access: "brand:your-brand"`
4. Replace the demo logos in `cms/brands/your-brand/assets/` with the brand's
5. Customise `cms/brands/your-brand/assets/theme.css`
6. Grant tool access: add `your-brand` to `actionAccess` in relevant `cms/*.md` files
7. Create a Netlify Identity user with the brand role
8. Run `cd cms/generator && npm run docgen` and test in dev mode

There is no separate registration step. A folder with a `brand.json` and an `assets/theme.css` is a live brand: the generator adds it to the runtime registry, the switcher, and the build.

---

## Step 1: Create the Brand Folder

Copy the `cms/brands/brand-template/` folder and rename it to the brand name. Use lowercase, hyphenated names.

```
cp -r cms/brands/brand-template/ cms/brands/your-brand/
```

The template ships dressed as a demo brand (Brand Template), so every file you copy is a working example to replace, not an empty scaffold. The folder contains:

```
cms/brands/your-brand/
  brand.json      : Admin settings (the manifest)
  _defaults.md    : Access control defaults
  assets/
    theme.css     : Designer token overrides
    logo.svg      : Brand header logo (desktop)
    logo-small.svg : Compact logo variant (mobile)
    logos/        : Brand book logo set (light/dark variants)
  welcome.md      : Welcome page (optional)
  brand-book.md   : Brand book intro copy (optional)
```

On build, the generator copies `assets/` to the output, generates the brand's pages, and builds the brand book from the `logos/` folder. Nothing is hand-copied.

### Set up access defaults

Update `cms/brands/your-brand/_defaults.md`:

```yaml
---
access: "brand:your-brand"
---
```

This ensures all pages generated for this brand are scoped to their access level.

---

## Step 2: Fill in the Manifest

Edit `cms/brands/your-brand/brand.json`:

```json
{
  "name": "Your Brand",
  "description": "One line shown on the brand's home page.",
  "googleFontsUrl": "https://fonts.googleapis.com/css2?family=YourFont:wght@400..700&display=swap",
  "footerText": "© 2026 Your Brand"
}
```

| Key | Required | Description |
|-----|----------|-------------|
| `name` | Yes | Display name: the switcher label, page titles, and the brand home hero |
| `description` | No | Hero copy on the brand's index page |
| `googleFontsUrl` | No | Google Fonts URL, injected on brand pages and at runtime. Use `null` for self-hosted fonts |
| `typekitId` | No | Adobe Typekit kit ID (e.g. `wgr3lwl`). Emits the kit stylesheet link into the brand's page heads |
| `fontPreload` | No | Array of site-relative font file paths to preload (prevents FOUC on the primary face) |
| `footerText` | No | Brand footer. Falls back to the site footer when omitted |
| `faviconSvg` / `faviconIco` | No | Absolute site paths to a brand favicon. Fall back to the site set |
| `ogImage` | No | Absolute site path to the brand's share image. Falls back to the site default |

Font sources are per brand: a page head only carries the Typekit kit, preloads, and Google Fonts its own manifest declares. Self-hosted fonts pair a `fontPreload` entry with `@font-face` declarations in the brand's `theme.css` (see Step 4); the runtime preview switcher loads Google Fonts only, so Typekit-served faces show on the brand's own pages, not in a preview from another space.

The folder name is the brand key. It must match the `brandFolder` value in Netlify Identity.

**Note:** Tool access is NOT configured here. It is controlled via `actionAccess` in `cms/*.md` frontmatter. See the Access Control docs for details.

---

## Step 3: Add Brand Logos

Logo files live in the brand source folder with fixed names:

```
cms/brands/your-brand/assets/logo.svg        : Full header logo (desktop)
cms/brands/your-brand/assets/logo-small.svg  : Compact variant (mobile, future use)
cms/brands/your-brand/assets/logos/          : Brand book set, logo_brand-<type>-<light|dark>.svg
```

The generator copies these to `your-brand/assets/` on build and builds the brand book page from the `logos/` set.

### Logo requirements

- **Format:** SVG
- **Sizing:** The container is 50px tall (desktop) / 45px tall (mobile). Width scales automatically. A wide aspect ratio (roughly 2:1) works best.
- **Colour:** Use fixed colours, or `currentColor` if you want the logo to adapt to the theme's text colour and dark mode.
- **Cleanup:** Run logos through the SVG Cleaner before adding them:

```bash
node assets/js/svg-clean.js --strip-comments --size -o cms/brands/your-brand/assets/logo.svg <<'SVGEOF'
<svg>...paste logo SVG here...</svg>
SVGEOF
```

### How it works

When a brand logs in, `theme-loader.js` fetches the logo from `your-brand/assets/logo.svg` and injects it into the header. When an admin previews a theme, the logo swaps too. When the theme is unloaded, the By Default logo is restored.

---

## Step 4: Customise the Theme

Open `cms/brands/your-brand/assets/theme.css` and update the token values. The copied file is the Brand Template demo theme, a complete working example of the override surface.

### What to override

| Category | Tokens | Purpose |
|----------|--------|---------|
| **Typography** | `--font-primary` … `--font-quaternary`, plus the roles `--font-display`, `--font-playful`, `--font-script`, `--font-editorial` | Brand fonts. Add `@font-face` in the theme file if self-hosting |
| **Neutral scale** | `--neutral-50` through `--neutral-990` | The grey ramp behind surfaces and text |
| **Text** | `--text-primary`, `--text-secondary`, `--text-accent`, `--text-link`, `--text-inverted` | Core text colours |
| **Backgrounds** | `--background-primary`, `--background-secondary` | Surface colours |
| **Borders** | `--border-primary`, `--border-secondary`, `--input-border` | Border and input outline colours |
| **Accent** | `--status-info`, `--input-focus`, `--checkbox-selected`, `--selection-*` | Accent colour across UI |

Buttons are not a theme surface: they derive from `--text-primary` and `--text-inverted` at component scope and re-skin through those tokens.

### Tokens you usually don't need to override

- `--text-faded`, `--background-faded`, `--border-faded`: alpha-based, work across any theme
- `--status-*-bg`: background tints derived from the accent primitives
- Spacing, radius, type scale, breakpoints: engine rhythm, not brand expression

### Dark mode

Theme the `[data-theme="dark"]` block, then mirror it exactly inside the `@media (prefers-color-scheme: dark)` fallback for users without JavaScript. Drift between the two blocks is a known failure mode; keep them identical.

### How it works

Brand pages link the theme statically after the design system CSS; at runtime `theme-loader.js` injects the same file when a brand user logs in or an admin previews the brand. Later token values win the cascade, so the theme overrides the engine's neutral defaults and the whole site follows.

---

## Step 5: Create the Netlify Identity User

In the Netlify Identity dashboard, create a new user and add their brand name as a **role tag** (e.g. `your-brand`). The system automatically recognises non-hierarchy role tags as brand folder names.

You can also add `brand` as a second tag for clarity, but it's not required. A single tag like `your-brand` is sufficient.

| Tag | Required? | Purpose |
|-----|-----------|---------|
| `your-brand` | Yes | Maps to the brand folder name, manifest key, and logo path |
| `brand` | Optional | Explicitly sets the access level (auto-inferred if omitted) |

The brand name tag is the single identifier that ties everything together:
- Manifest: `cms/brands/your-brand/brand.json`
- Theme CSS: `your-brand/assets/theme.css`
- Logo: `your-brand/assets/logo.svg`
- Home link: `your-brand/index.html`
- Page access: restricted to `your-brand/` folder + shared tool pages

---

## Step 6: Test in Dev Mode

On localhost, the dev toolbar lets you switch roles without logging in.

### Test as brand

1. Open `assets/js/auth-config.js`
2. Set `devMode.brandFolder` to your brand name:
   ```js
   devMode: {
     defaultRole: 'brand',
     email: 'dev@bydefault.studio',
     brandFolder: 'your-brand'
   }
   ```
3. Open any page on localhost
4. Switch to the **brand** role in the dev toolbar
5. Verify: logo swaps, home link updates, theme loads, sidebar filters

### Test as admin with theme preview

1. Set `devMode.defaultRole` back to `'admin'`
2. Open any page on localhost
3. Click the user icon in the header, open the dropdown
4. Under **Theme Preview**, select your brand's theme
5. Verify: theme loads, logo swaps, persists across page navigation
6. Clear the preview to return to By Default

---

## Creating from Template: Full Walkthrough

Here is the complete workflow for adding a new brand called "acme":

```bash
# 1. Copy the brand source template
cp -r cms/brands/brand-template/ cms/brands/acme/

# 2. Clean and add logos
node assets/js/svg-clean.js --strip-comments --size \
  -o cms/brands/acme/assets/logo.svg <<'SVGEOF'
<svg>...paste full logo...</svg>
SVGEOF

node assets/js/svg-clean.js --strip-comments --size \
  -o cms/brands/acme/assets/logo-small.svg <<'SVGEOF'
<svg>...paste small logo...</svg>
SVGEOF
```

Then manually:

3. Edit `cms/brands/acme/brand.json`: name "Acme", description, fonts URL, footer
4. Edit `cms/brands/acme/assets/theme.css`: replace the demo values with Acme's fonts and colours
5. Replace the demo SVGs in `cms/brands/acme/assets/logos/` with Acme's light/dark set
6. Update `cms/brands/acme/_defaults.md`:
   ```yaml
   ---
   access: "brand:acme"
   ---
   ```
7. Edit `cms/brands/acme/welcome.md` and `brand-book.md` if needed
8. Grant tool access, edit the tool markdown files to include `acme`:
   - `cms/display-ad-preview-docs.md`: change `actionAccess: "brand:brand-template"` to `actionAccess: "brand:brand-template,acme"`
   - Update the matching `data-access` on `tools/display-ad-preview.html`
9. Run `cd cms/generator && npm run docgen`
10. In Netlify Identity, create a user with:
    ```json
    { "roles": ["brand"], "brandFolder": "acme" }
    ```
11. Test in dev mode

---

## Architecture Reference

| File | Purpose |
|------|---------|
| `cms/brands/<brand>/brand.json` | The manifest: admin settings. With `assets/theme.css` alongside it, the brand is live |
| `cms/brands/<brand>/assets/theme.css` | Designer token overrides |
| `assets/js/theme-config.js` | GENERATED runtime registry, built from the manifests on every docgen. Never hand-edit |
| `assets/js/theme-loader.js` | Loads/unloads theme CSS, fonts, logo, and home link dynamically |
| `assets/js/auth.js` | Calls `initThemeForUser()` after auth resolves |
| `assets/js/auth-config.js` | Role hierarchy, dev mode settings (including `brandFolder` for testing) |
| `scripts/theme-regression/check.py` | Instance-0 token regression check: run after editing the engine's tokens or By Default's theme |

Instance 0 is the exception that proves the model: `cms/brands/bydefault/` holds By Default's manifest and theme, the root site loads that theme as a static link on every page, and the brand does not appear in the switcher, because clearing a preview is the return to By Default.

### Data flow

```
Build
  → generator reads every cms/brands/*/brand.json
  → brands with assets/theme.css enter the runtime registry
  → theme-config.js is emitted in one write
  → brand pages are generated with the brand's chrome (title, footer, favicon, OG)

Login
  → auth.js resolves role + brandFolder from app_metadata
  → auth.js calls initThemeForUser(user)
  → theme-loader.js reads brandFolder
  → Sets home link to brandFolder/index.html
  → Swaps logo to brandFolder/assets/logo.svg
  → Loads brandFolder/assets/theme.css (if in the registry)
  → Injects the Google Fonts link (from the manifest, via the registry)
  → Auth filters sidebar navigation by access level
  → Page renders with full brand branding
```
