Theme Data Structure — Schema Reference

Updated: 2026-08-29 · Status: reconciled against the shipped Mobile Content API (AB#2609)

TL;DR — Tabular reference for the theme data structure Vlad Oprica's Mobile Content API actually shipped: SiteDto/PageDto theme assignment (activeThemeId/activeThemeKey), the ThemeDto field schema, how Container/Item content overrides individual tokens, a worked example for UAL, and the token catalog (95 tokens across 15 categories per the Admin Portal's token registry).
Reconciled 2026-08-29 (AB#2609) — the cascading model below was not built. Earlier revisions of this doc were the tabular reference for a proposed ThemeNode schema with scopeType/scopeKey/ parentThemeId cascading inheritance (Site → Page → LEC, sparse overrides layered root-to-leaf) and a mainBanner asset override. None of that shipped. The real, live theme model is simpler: a theme is an independent row (GUID id + semantic key + flat token dictionary), assigned to a Site via activeThemeId/activeThemeKey, with an optional full-swap override at the Page level — there is no cascade below Page, no per-node ThemeNode/scopeType row, and no mainBanner field. This page has been rewritten to describe the real schema. See Theme Token Inheritance Model for the full account of what was proposed vs. what shipped.

Contents

ThemeDto field schema

A theme is a single, independent row — not a node in an inheritance chain. The Dynamic Feed guide and the Admin Portal guide document slightly different field sets for the same underlying row (read-side vs. write/manage-side); both are shown below. See Theme Token Inheritance Model for why the cascading ThemeNode schema previously documented here was never built.

FieldTypeWhere documentedDescription
idint (Dynamic Feed guide) / guid (Admin Portal guide)BothDatabase PK — the FK used as SiteDto.activeThemeId / PageDto.activeThemeId. The type disagreement between Vlad Oprica's two docs is unresolved upstream; treat as opaque client-side.
keystringBothUnique, lowercase semantic identifier, e.g. ual-default. Matches activeThemeKey on Site/Page.
titlestring?AdminHuman-readable display name. Used as the slug source when key is omitted on create.
descriptionstring?BothAdditional notes / label.
tokensdictionary<string, string>BothFlat token-path → value map. Every theme carries a full set — there is no sparse-override/inheritance concept between themes.
typographydictionary<string, TypographyStyle>AdminNamed typography bundles (font family/size/weight/style/line-height/transform/letter-spacing/color together), managed separately from tokens via the Typography endpoints. Empty when none are set.
workflowStateint (0=Draft, 1=Published, 2=Unpublished, 3=Archived)AdminSame 4-state workflow as Sites and Pages — not a per-node Live/Draft flag.
activeSnapshotIdguid?AdminGUID of the last-published snapshot. Null if never published.
hasDraftChangesboolAdminTrue when live token rows changed since the last publish.
isLocked / lockedByUserbool / string?AdminEdit-lock state, same mechanism as Page locking.
createdAt / updatedAtDateTimeAdminUTC record timestamps.
No per-node exhaustiveness invariant to enforce. Because there is no cascade, there is no "root must define every token" rule — every theme row is independently a full set. The token-count denominator for any "N of M tokens set" UI is simply the size of the canonical registry (95 — see the catalog note below), the same for every theme.

Theme assignment: Site and Page

LevelFieldsBehavior
SiteactiveThemeId: guid?, activeThemeKey: string?The default theme for every Page under this Site, unless a Page overrides it.
PageactiveThemeId: guid?, activeThemeKey: string?When non-null, replaces the Site's theme entirely for that Page — a full swap to a different theme row, not a merge/cascade of individual tokens on top of the Site's theme.

There is no tier below Page — an LEC is itself a Page (same as before), but it has no special theming behavior beyond the ordinary Page-level activeThemeId override described above. There is no scopeType/scopeKey concept; Site and Page are identified by their own native keys (siteKey, pageKey).

Container / Item token references

Container and Item content records do not carry a merged override dictionary. Instead, each carries its own set of individual *Token fields that reference a token path by name — the value is resolved from whichever theme is active for that Page at render time:

Field (on Item; Container has a subset)TypeDescription
backgroundToken, cornerRadiusToken, paddingTokenstring?Also present on ContainerDto. Token-path reference for background/corner-radius/padding.
eyebrowToken, titleToken, descriptionToken, headerTextToken, identityToken, contractLinkToken, ctaTextTokenstring?Item-only. Token-path reference applied to the named field's text color/style.
rosterTitleToken, rosterNameToken, rosterEmailToken, rosterTelToken, viewAllLinkToken, iconSizeToken, iconColorTokenstring?Item-only, Admin-write-side fields not yet echoed by the read-side ItemDto docs — see admin-portal.html — Item Endpoints.
tokenOverridesstring? (free-form JSON)Item-only, Admin-write-side. Ad hoc JSON object of token overrides scoped to this item (e.g. {"Surface/Brand":"#ff0000"}), stored as-is and applied by the mobile client on top of the active theme.

This is the real analog of what earlier revisions of this doc modeled as a single structured themeTokenOverrides dictionary merged at resolution time. The actual shape is the reverse of that description: most overrides are individual, per-field, direct token-path references (not a merged bag), and only Items get one additional free-form JSON override field (tokenOverrides) for ad hoc cases — Containers don't have an equivalent free-form field.

Worked example: UAL hierarchy

Two independent theme rows, a Site assignment, and a Page-level full-swap override:

ThemeidkeyworkflowStateupdatedAtdescription
ALPA base7c9e6679-…alpa-defaultPublished2026-06-26T00:00:00ZDefault ALPA palette
UAL default3fa85f64-…ual-defaultPublished2026-06-26T00:00:00ZUnited Airlines default theme
UAL HomePage promo91a2c3d4-…ual-homepage-promoDraft2026-08-14T15:42:00ZSeasonal promo palette, still being tuned — not published
EntitysiteKey / pageKeyactiveThemeIdResolved theme
Siteual3fa85f64-… (UAL default)UAL default — applies to every Page in this Site by default
Pageual-homenullFalls back to the Site's theme (UAL default) — no override set
Page (once published)ual-home, after an Admin publishes the promo theme91a2c3d4-… (UAL HomePage promo)UAL HomePage promo — replaces the Site's theme entirely for this one Page; every other Page in the ual Site is unaffected

There is no walk/merge step: resolving a Page's effective theme is "does this Page have a non-null activeThemeId? If yes, fetch that theme. If no, fetch the Site's activeThemeId instead." — a single conditional, not a chain.

A Container and Item under this Page layer their own token references on top of whichever theme resolved above (see Container / Item token references):

EntityRelevant fieldValueEffect
Container hero-container-3backgroundTokennullNo override — background follows the container's own rendering default.
Item promo-tile-1titleToken"Surface/Brand"The item's title color resolves against Surface/Brand in whichever theme is active for ual-home at render time.
Item promo-tile-1tokenOverrides (free-form){ "Surface/Brand": "#0057a3" }Ad hoc override applied on top of the active theme for this one item only — the mobile client applies this after resolving the theme, not the backend.

Canonical token catalog

Tokens — the registry, not the values

Tokens is a separate, simpler structure from everything above — it doesn't belong to any one theme or scope. It's the flat list of which token keys are allowed to exist at all, and how they're grouped:

FieldTypeDescription
tokenNamestringThe canonical key, e.g. "Surface/Brand". Matches exactly what appears as a key in any ThemeDto.tokens dictionary, or as the value of any Container/Item *Token field or tokenOverrides entry.
categorystringGrouping label, e.g. "Surface" — matches the namespace prefix before the /. Used for UI grouping (the per-category tables below), not parsed out of tokenName at runtime. The real registry has 15 such categories — see the warning below.

Tokens (the registry) answers "what tokens exist"; ThemeDto.tokens (the field, on any individual theme) answers "what value does this theme give every one of them." Because there is no inheritance between themes, every ThemeDto must provide a value for every Tokens registry entry — there's no partial or sparse theme. Tokens itself carries no values — it's schema, not data — so it changes far less often than any individual theme does, and is enforced server-side via the Admin API's POST /theme/validate-tokens endpoint (admin-portal.html).

Relationship to design-tokens.html#theme-tokens. A richer, already-shipped per-token reference exists at design-tokens.html — Theme Endpoint Token Namespace Reference — descriptions, paired On-* tokens, CSS variable names, and per-namespace grouping tables for every token below. Tokens as defined here is the minimal subset of that same information (name + category only) needed to drive resolution and validation; it is not a competing source of truth. The token registry extensibility point (type/lockable/deprecated per token) is best read as formalizing metadata design-tokens.html already documents in prose today, not as a new idea invented from nothing.
Token count is unsettled across this doc set — 2026-08-29. This section previously counted 45 tokens (a pre-shipped test-data baseline), then a later revision cited a corrected count of 107 based on one live FDX payload, describing new namespaces (FontSize/*, FontWeight/*, TextTransform/*, LetterSpacing/*) and additional keys (Surface/Secondary, Text/Eyebrow). The authoritative source is the Admin Portal guide's token registry (admin-portal.html — Token Endpoints, updated 2026-08-26, the most recently updated of Vlad Oprica's docs): 95 tokens across 15 categories — Surface, Text, Border, Icon, Action, Navigation, Status, Typography, Font Size, Font Weight, Text Transform, Letter Spacing, Spacing, Border Radius, and Icon Size. (The Ingestion API guide separately says "91 registry tokens" — a minor inconsistency in Vlad's own docs this doc does not attempt to resolve.) None of Vlad's docs enumerate the full 95-token list by name — the tables below cover only the subset already known from earlier examples in this doc set (the original ~45 tokens across the 10 categories below); the five categories the real registry adds beyond these ten (the finer-grained Typography/Font Size/Font Weight/Text Transform/Letter Spacing split, plus Icon Size) are not enumerated here — this is the same open item tracked as OT-8 in the Theme Endpoint contract.

The token subset already known from earlier examples in this doc set, grouped by category, comparing the ALPA base value against the UAL theme's value for each. This is illustrative, not the full 95-token registry (see the warning above).

Surface

TokenDefaultUAL themeDiffers from ALPA base?
Surface/Brand#05273e#002243Yes
Surface/Default#ffffff#ffffffNo
Surface/Primary#efefef#f7f7f7Yes
Surface/Subtle#dfedf9#dfedf9No

Text

TokenDefaultUAL themeDiffers from ALPA base?
Text/Primary#05273e#05273eNo
Text/Secondary#5d6471#5d6471No
Text/On-Brand#ffffff#bed6fbYes
Text/On-Subtle#05273e#05273eNo
Text/Link#007bc2#94ebfeYes
Text/Disabled#d2d4d6#d2d4d6No

Border

TokenDefaultUAL themeDiffers from ALPA base?
Border/Brand#05273e#0008ceYes
Border/Default#d2d4d6#d2d4d6No
Border/Subtle#e3e3e3#e3e3e3No

Icon

TokenDefaultUAL themeDiffers from ALPA base?
Icon/Default#05273e#05273eNo
Icon/On-Brand#ffffff#ffffffNo
Icon/Active#007bc2#94ebfeYes

Action

TokenDefaultUAL themeDiffers from ALPA base?
Action/Primary#007bc2#0008ceYes
Action/PrimaryText#ffffff#ffffffNo
Action/Accent#007bc2#94ebfeYes
TokenDefaultUAL themeDiffers from ALPA base?
Nav/Background#05273e#002243Yes
Nav/Active#ffffff#ffffffNo
Nav/Inactive#9ba1a8#9ba1a8No
Nav/Hoverrgba(255,255,255,0.12)rgba(255,255,255,0.12)No

Status

TokenDefaultUAL themeDiffers from ALPA base?
Status/Error#c02126#c02126No
Status/Success#1a7340#1a7340No
Status/Warning#b7770d#b7770dNo
Status/Info#007bc2#007bc2No
Status/OnTime#00ea75#00ea75No

Font

TokenDefaultUAL themeDiffers from ALPA base?
Font/HeadingFuturaPTLeagueSpartanYes
Font/BodyFuturaPTLeagueSpartanYes

Spacing

TokenDefaultUAL themeDiffers from ALPA base?
Spacing/xtiny4px4pxNo
Spacing/tiny8px8pxNo
Spacing/xxsmall12px12pxNo
Spacing/xsmall16px16pxNo
Spacing/small20px20pxNo
Spacing/medium24px24pxNo
Spacing/large28px28pxNo
Spacing/xxlarge40px40pxNo
Spacing/page-margins16px16pxNo

BorderRadius

TokenDefaultUAL themeDiffers from ALPA base?
BorderRadius/None0px0pxNo
BorderRadius/S4px4pxNo
BorderRadius/M8px8pxNo
BorderRadius/L12px12pxNo
BorderRadius/XL16px16pxNo
BorderRadius/Full50%50%No

See also