> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useduro.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Appearance Builder

> A draft/publish design system that themes every customer surface from one token set — checkout, popup, portal, pay page, email.

A merchant should brand once and have it show up everywhere a customer sees them. The appearance builder is a Lemon-Squeezy-style visual editor backed by a **draft/publish** config that lives in the core schema's `StoreConfig`.

## One config, six surfaces

```mermaid theme={null}
flowchart TB
    CFG["StoreConfig<br/>(per tenant)"] --> DRAFT["draft"]
    CFG --> PUB["published"]
    PUB --> GEN["general — brand colour, font, logo, language"]
    PUB --> CHK["checkout — fields, rails, copy"]
    PUB --> OVL["overlay — the inline popup"]
    PUB --> PORTAL["portal — heading, link, permissions"]
    PUB --> STORE["store — listing"]
    PUB --> EMAIL["email — sender, header, footer"]
    GEN & CHK & OVL & PORTAL & STORE & EMAIL --> RENDER["rendered on:<br/>hosted checkout · inline SDK ·<br/>customer portal · pay page · emails"]
```

The six surfaces (`general`, `store`, `checkout`, `overlay`, `portal`, `email`) are the tabs in the builder. `general` holds the brand tokens that cascade everywhere; each other tab overrides locally.

## Draft, publish, discard

Edits are staged in `draft` and only go live on **Publish**, exactly like a CMS:

```mermaid theme={null}
stateDiagram-v2
    [*] --> draft: PATCH /appearance/{surface}
    draft --> published: POST /appearance/publish
    draft --> draft: more edits (unpublished changes)
    published --> draft: POST /appearance/discard (revert to published)
```

A `PATCH` to a surface deep-merges into the draft (so toggling one portal permission doesn't wipe the rest); `hasUnpublishedChanges` is computed by comparing draft to published, driving the "Unpublished changes" badge. The merge is bounded — surface level plus one nested level, which is all the config shape needs, so there's no unbounded recursion. Verified live: per-surface deep-merge including nested permissions, publish, and discard-reverts-to-published.

## The portal permissions live here

The customer portal asks this config what it's allowed to offer — `portal.permissions.cancelSubscription`, `pauseSubscription`, `updatePlan`, etc. A merchant who disables cancellation in the builder disables it in [the portal](/identity/customer-portal). Configuration and enforcement are the same source of truth.

## Powered by RBAC

Editing appearance needs the `appearance:manage` permission (owner, admin, and developer roles have it) — see the [security model](/security/security-model). The published config is read by the public surfaces without auth; only the *editing* is gated.

Next: the [security model](/security/security-model).
