Brand System

Versions & Snapshots

Date-based immutable brand-package version snapshots, pnpm brand:snapshot, and the Portal runtime API endpoints.

Versions & Snapshots

Brand packages are versioned and immutable so the Portal can pin a version, diff two versions, and explain what changed. Versions are date-based — clearer for brand governance than semver, and matching how brand locks are already referenced (e.g. "v2 lock 2026-05-06") (ADR 0002 §7).

Snapshot layout

Each version is an immutable directory under the tenant's versions/:

registries/{ns}/
  brand-system.css            # working copy (next version in progress)
  versions/
    2026.06.05/               # immutable once published
      brand-system.css
      brand-profile.json      # governed profile instance (git-reviewed)
      requirements.schema.json
      asset-manifest.json     # asset IDs, versions, signed-URL policy, alt text, usage rules
      guidelines.json         # normalized docs summary

The first Vandoko snapshot is registries/vandoko/versions/2026.06.05/.

  • brand-profile.json — the tenant's contract-instance values ({ namespace, contractVersion, tokens }), validated by BrandTokenProfileSchema.
  • requirements.schema.json — generated from the canonical token contract.
  • guidelines.json — a normalized, versioned summary of the brand guideline docs (sections, headings, asset refs) that the Portal's Guideline Viewer renders with its own components.
  • asset-manifest.json — the per-tenant asset index plus the signed-URL policy.

Publishing — pnpm brand:snapshot

pnpm brand:snapshot

brand:snapshot (scripts/brand-snapshot.mjs) freezes the working copy into a new versions/{date}/ directory.

Immutability is enforced at publish time. brand:snapshot refuses to overwrite an existing version directory, and git review backs it up — handlers only ever read committed snapshot files, never the working copy. A published version never changes.

Each brand-system version gets a changelog entry so Portal snapshots can reference it to explain what changed.

Portal runtime API

The Portal consumes brand packages at runtime via an authenticated API, distinct from the shadcn-CLI install path. The endpoints (lib/brand-system-api.ts, app/api/brand-system/**):

GET  /api/brand-system/{brandId}/latest                 -> newest version payload
GET  /api/brand-system/{brandId}/{version}              -> immutable snapshot
GET  /api/brand-system/{brandId}/{version}/{file}       -> one snapshot file
POST /api/brand-system/validate                         -> validate a brand-profile against the contract

The {file} whitelist is brand-system.css, brand-profile.json, requirements.schema.json, asset-manifest.json, guidelines.json. brandId is the serving-namespace slug; versions are date-based (2026.06.05).

Gating

This route is key-gated like /r/*, not Clerk-session protectedproxy.ts matches it before the generic /api/* auth.protect() fallthrough. Every branch is force-dynamic + no-store.

RequestCredentialStatus
GET /api/brand-system/{brandId}/…platform key200 (any brand)
GET /api/brand-system/{brandId}/…namespace key bound to {brandId}200
GET /api/brand-system/{brandId}/…namespace key bound elsewhere403 JSON, no-store
POST /api/brand-system/validateany valid credential200 (tenant-neutral schema check)
any /api/brand-system/*none / invalid401 JSON, no-store
unknown brand / version / fileauthorized404 JSON, no-store

The full matrix lives in the Registry Auth & Cache Contract (docs/registry-auth-contract.md).

Portal docs consumption = the summary API. The Portal renders guidelines.json with its own components rather than reading the Registry's MDX directly — this keeps the client-facing Portal decoupled from the Registry's Fumadocs internals and lets the Portal pin a version.

On this page