How org themes overlay the base theme
All theme files live in Azure blob storage under two prefixes (ThemeV2FileStore.cs):
themes-v2/{themeId}/theme/{filePath} ← the shared base theme
themes-v2/{themeId}/overrides/{organisationId}/{filePath} ← per-org edits
Resolution is override-first, per file:
// Try override first
var overridePath = $"themes-v2/{themeId}/overrides/{organisationId}/{filePath}";
if (await blobService.CheckBlobExistAsync(Container, overridePath, ct)) { ... return override; }
// Fall back to base theme
var basePath = $"themes-v2/{themeId}/theme/{filePath}";
An org can also create files that don't exist in the base (they simply live in the overrideprefix). Editing goes through AdminThemeV2Service, which validates before writing: 512 KB sizecap, extension-per-directory allowlist (layouts|partials|snippets|sections|templates →.fluid, schema → .json, assets → .css/.js/.json), the {% schema %} JSON mustdeserialise, and the Fluid must parse — a broken template is rejected at save time instead of500-ing the storefront. Deleting an override reverts the file to base. Every write bumpsCacheVersion and logs to the edit timeline. Snapshots ("restore points") zip all overridesplus the six settings/content JSON columns; restore unpacks them and bumps the version.
Startup seeding — disk → blob by content hash
ThemeV2SeedHostedService runs on every app start and syncswwwroot/themes/origin/ to themes-v2/{themeId}/theme/:
- Picks the base theme deterministically (lowest Id among active
ThemesV2rows). - Reads
schema/theme.jsonand syncs itsname/versiononto the DB row — the version is what makes "update available" appear for orgs (ProvisionedVersionvs baseVersion, compared numerically). Bumpversionintheme.jsonwhen you ship theme changes. - Lists existing blobs once, then for each disk file compares MD5 of the bytes against the blob's stored
ContentHashand only uploads on mismatch. - Prunes blobs with no matching disk file (this is what retired the legacy
.hbsfiles). - Only allowlisted extensions ship:
.fluid .hbs .css .js .json .html .svg .png .jpg .jpeg .gif .webp; editor temp files are skipped.
Org overrides are untouched by seeding — they keep layering over whatever the new base is.ApplyBaseThemeUpdateAsync is the per-org "take the update" action: it snapshots first, thenre-stamps ProvisionedVersion and bumps the cache version so the org re-renders against thenew base files.
Note: the seeder is currently hard-wired to one theme folder (ThemeName = "origin") and oneDB row. Building a separate theme today means either forking the origin folder in place (itbecomes the base theme) or authoring your theme entirely as org-level override files throughthe admin file API. Multi-base-theme support would need the seeder and thelowest-Id-wins picks generalised.