Skip to content

Theme Contract Reference

The @sovecom/theme-sdk package is the single source of truth for the SovEcom theme contract. The core runtime (apps/api) imports all validators and types from this package, so the published SDK can never drift from what the core enforces at install time.

A theme is a declarative asset: there is no activate, no worker, no runtime entrypoint, and no database tables. The SDK exports validators, typing helpers, and the two store-contract types the storefront reads — nothing executable.

Related reading: Architecture Overview, Module Guide.


Everything below is re-exported from @sovecom/theme-sdk. Import any symbol directly:

import {
defineTheme,
defineThemeSlots,
defineThemeSettings,
parseAndVerifyThemeManifest,
parseTemplate,
defineTemplate,
defineSection,
parseWidget,
WIDGET_TYPES,
MANIFEST_MAX_BYTES,
CORE_API_VERSION,
} from '@sovecom/theme-sdk';
ExportKindSource
THEME_SDK_VERSIONstring constindex.ts
defineThemefunctiontheme.ts
DefineThemeConfigtypetheme.ts
defineThemeSlotsfunctionslots.ts
defineThemeSettingsfunctionsettings.ts
ThemeSettingstypesettings.ts
DocumentedThemeSettingsinterfacesettings.ts
KnownThemeSettingstypesettings.ts
THEME_NAME_RERegExp constmanifest.ts
SLOT_SLUG_RERegExp constmanifest.ts
TEMPLATE_PATH_RERegExp constmanifest.ts
themeManifestSchemaZod schemamanifest.ts
themeTemplateDeclSchemaZod schemamanifest.ts
parseAndVerifyThemeManifestfunctionmanifest.ts
ThemeManifesttypemanifest.ts
ThemeTemplateDecltypemanifest.ts
PAGE_TYPESreadonly string[] consttemplate.ts
SECTION_TYPE_RERegExp consttemplate.ts
REGION_NAME_RERegExp consttemplate.ts
MAX_REGION_DEPTHnumber consttemplate.ts
pageTypeSchemaZod schematemplate.ts
regionNameSchemaZod schematemplate.ts
templateSectionSchemaZod schematemplate.ts
templateSchemaZod schematemplate.ts
parseTemplatefunctiontemplate.ts
defineTemplatefunctiontemplate.ts
defineSectionfunctiontemplate.ts
PageTypetypetemplate.ts
TemplateSectiontypetemplate.ts
ThemeTemplatetypetemplate.ts
SectionDeftypetemplate.ts
WIDGET_TYPESreadonly string[] constwidget.ts
WIDGET_MAX_BYTESnumber constwidget.ts
actionPathSchemaZod schemawidget.ts
actionSchemaZod schemawidget.ts
starRatingSummaryPropsSchemaZod schemawidget.ts
reviewListPropsSchemaZod schemawidget.ts
productCarouselPropsSchemaZod schemawidget.ts
toggleButtonPropsSchemaZod schemawidget.ts
submitFormPropsSchemaZod schemawidget.ts
widgetDescriptorSchemaZod schemawidget.ts
parseWidgetfunctionwidget.ts
WidgetTypetypewidget.ts
WidgetDescriptortypewidget.ts
StarRatingSummaryPropstypewidget.ts
ReviewListPropstypewidget.ts
ProductCarouselPropstypewidget.ts
ToggleButtonPropstypewidget.ts
SubmitFormPropstypewidget.ts
MANIFEST_MAX_BYTESnumber constre-exported from @sovecom/module-sdk
assertCoreCompatiblefunctionre-exported from @sovecom/module-sdk
CORE_API_VERSIONstring constre-exported from @sovecom/module-sdk
ActiveThemetypestore-contract.ts
SlotBindingtypestore-contract.ts
SlotMaptypestore-contract.ts

The manifest is validated by parseAndVerifyThemeManifest(raw: string): ThemeManifest. It enforces a 64 KiB byte cap (shared with @sovecom/module-sdk via MANIFEST_MAX_BYTES = 64 * 1024), rejects unknown top-level keys (.strict()), and runs a semver major gate against CORE_API_VERSION.

FieldTypeRequiredConstraint
namestringYesLowercase slug ^[a-z][a-z0-9-]*$; max 64 chars
displayNamestringYesNon-empty; max 128 chars
versionstringYesValid semver; max 64 chars
compatibleCorestringYesValid semver range; max 256 chars
slotsstring[]NoArray of lowercase slugs; max 64 entries; no duplicates
settingsSchemastringNoRelative path to a JSON-schema file; max 256 chars
templatesThemeTemplateDecl[]NoAt most one entry per page type; max entries = number of page types

Unknown top-level keys are rejected (.strict()).

TypeScript type (verbatim from manifest.ts)

Section titled “TypeScript type (verbatim from manifest.ts)”
export type ThemeManifest = z.infer<typeof themeManifestSchema>;

Resolved shape:

{
name: string;
displayName: string;
version: string;
compatibleCore: string;
slots?: string[];
settingsSchema?: string;
templates?: ThemeTemplateDecl[];
}

Each entry in templates[] is a ThemeTemplateDecl:

FieldTypeRequiredConstraint
pagePageTypeYesOne of PAGE_TYPES (see Page Types)
pathstringYesRelative .json slug path; no ..; no leading /; max 256 chars

The path regex is TEMPLATE_PATH_RE = /^[a-z0-9][a-z0-9_-]*(\/[a-z0-9][a-z0-9_-]*)*\.json$/. The ingest re-asserts containment against the extraction root as defence in depth; this regex is the first gate.

export type DefineThemeConfig = Omit<ThemeManifest, 'slots'> & {
readonly slots?: readonly string[];
};
export function defineTheme(config: DefineThemeConfig): ThemeManifest

Runs the same parseAndVerifyThemeManifest pipeline the core runs at install time. Throws a clear Error on any violation. Use this at build time to validate your manifest config object before serialising it to sovecom.theme.json.

{
"name": "aurora",
"displayName": "Aurora",
"version": "1.0.0",
"compatibleCore": "^1.0.0"
}

Slots are declarative metadata in the manifest’s slots array. The slot registry is derived at runtime from enabled modules’ manifests. There is no runtime registerSlot().

Each slot must match SLOT_SLUG_RE = /^[a-z][a-z0-9-]*$/. No duplicate slugs. Max 64 slots per theme. Max 128 chars per slug.

export function defineThemeSlots(slots: ReadonlyArray<string>): readonly string[]

Validates and returns a frozen array of slot slugs. Throws on the first invalid or duplicate slug. Its output is dropped verbatim into the manifest’s slots field.

import { defineThemeSlots } from '@sovecom/theme-sdk';
const slots = defineThemeSlots([
'product-reviews',
'related-products',
'wishlist-toggle',
]);

A page template is a declarative JSON document consumed by the storefront runtime. The runtime resolves each section type against its section registry and renders sections in order. Templates are pure data — no code execution.

export const PAGE_TYPES = ['home', 'product', 'category', 'products', 'search', 'cart'] as const;
export type PageType = (typeof PAGE_TYPES)[number];
ValueMeaning
homeStorefront homepage
productSingle product detail page
categoryCategory landing page
productsAll-products / collection listing
searchSearch results page
cartCart page

ThemeTemplate type (verbatim from template.ts)

Section titled “ThemeTemplate type (verbatim from template.ts)”
export type ThemeTemplate = z.infer<typeof templateSchema>;
// Resolved:
// { page: PageType; sections: TemplateSection[]; }

Top-level sections array: max 64 entries.

TemplateSection type (verbatim from template.ts)

Section titled “TemplateSection type (verbatim from template.ts)”
export interface TemplateSection {
type: string;
settings?: Record<string, unknown>;
regions?: Record<string, TemplateSection[]>;
}
FieldTypeRequiredConstraint
typestringYesLowercase slug ^[a-z][a-z0-9-]*$; max 128 chars
settingsRecord<string, unknown>NoOpaque bag; validated against the section’s own schema at render time
regionsRecord<string, TemplateSection[]>NoLayout nesting (see below)

Unknown keys are rejected (.strict()).

A section may declare named regions — each region is an ordered list of nested TemplateSections, enabling sidebar/results column splits and similar layouts.

ConstantValueMeaning
MAX_REGION_DEPTH2Maximum nesting depth of regions
MAX_REGIONS (internal)8Max named regions per section

Region names must match REGION_NAME_RE = /^[a-z][a-z0-9-]*$/.

  • Depth 0: a top-level section in sections[].
  • Depth 1: a section inside a named region of a depth-0 section.
  • Depth 2: a section inside a named region of a depth-1 section. This is the maximum.

parseTemplate enforces the depth cap with a post-parse walk after the Zod schema validates shape and structural bounds.

export function parseTemplate(raw: string): ThemeTemplate
  • Enforces the MANIFEST_MAX_BYTES (64 KiB) cap on the raw string.
  • Parses JSON; maps a parse failure to a clear Error.
  • Runs the Zod schema; aggregates issues into a descriptive message.
  • Walks the validated result asserting MAX_REGION_DEPTH.
  • Returns the typed ThemeTemplate or throws.
export function defineTemplate(config: ThemeTemplate): ThemeTemplate

Runs the same parseTemplate pipeline (round-trips through JSON). Use at build time to validate a template before writing it to disk.

export interface SectionDef<T> {
readonly type: string;
readonly settings?: T;
}
export function defineSection<T>(def: SectionDef<T>): SectionDef<T>

defineSection is a pure typing vehicle — a no-op at runtime that pins the settings type T for editor autocomplete. It does not validate.

{
"page": "home",
"sections": [
{ "type": "hero-banner", "settings": { "heading": "Welcome" } },
{
"type": "two-column",
"regions": {
"left": [{ "type": "filter-panel" }],
"right": [{ "type": "product-grid" }]
}
},
{ "type": "featured-products" }
]
}

Widget vocabulary (module slot-widget contract)

Section titled “Widget vocabulary (module slot-widget contract)”

Modules contribute storefront UI by returning a typed widget descriptor { type, props }. The storefront renders its own known MIT components from that data. No code, no HTML, no SVG crosses the boundary — only data.

The widget vocabulary is closed: a module author cannot register a new type. Adding a widget type is an MIT storefront contribution reviewed like adding a section.

export const WIDGET_TYPES = [
'star-rating-summary',
'review-list',
'product-carousel',
'toggle-button',
'submit-form',
] as const;
export type WidgetType = (typeof WIDGET_TYPES)[number];
export const WIDGET_MAX_BYTES = MANIFEST_MAX_BYTES; // 64 * 1024
export type WidgetDescriptor = z.infer<typeof widgetDescriptorSchema>;
// A discriminated union: { type: WidgetType; props: <per-widget props> }

Each member is .strict() — unknown keys at the descriptor level or within props are rejected.

export type StarRatingSummaryProps = z.infer<typeof starRatingSummaryPropsSchema>;
// { average: number; count: number }
PropTypeConstraint
averagenumber[0, 5]
countnumber (integer)≥ 0
export type ReviewListProps = z.infer<typeof reviewListPropsSchema>;
// { items: ReviewItem[] }

items array: max 50 entries.

Each item:

PropTypeConstraint
idstringMin 1, max 64 chars
ratingnumber (integer)[1, 5]
bodystringMax 2000 chars
authorstring (optional)Max 120 chars
createdAtstringISO-8601 datetime (e.g. 2026-06-25T10:00:00.000Z)
export type ProductCarouselProps = z.infer<typeof productCarouselPropsSchema>;
// { heading?: string; items: CarouselItem[] }

items array: max 24 entries.

PropTypeConstraint
headingstring (optional)Max 120 chars

Each item:

PropTypeConstraint
productIdstringMin 1, max 64 chars
slugstringMin 1, max 200 chars; no /, \, or ..
titlestringMin 1, max 200 chars
imageUrlstring (optional)Max 2048 chars

The slug traversal guard prevents a module from returning slug: '../../admin' and turning a carousel link into a within-origin redirect. C2 (the storefront render layer) also encodeURIComponents the slug at render as defence in depth.

export type ToggleButtonProps = z.infer<typeof toggleButtonPropsSchema>;
PropTypeConstraint
initialOnboolean
onAction{ path: string }Relative /store/v1/modules/… path
offAction{ path: string }Relative /store/v1/modules/… path
labels.onstringMin 1, max 60 chars
labels.offstringMin 1, max 60 chars
icon'heart' | 'bell' | 'star'Enum only
export type SubmitFormProps = z.infer<typeof submitFormPropsSchema>;
PropTypeConstraint
action{ path: string }Relative /store/v1/modules/… path
submitLabelstringMin 1, max 60 chars
successMessagestring (optional)Max 200 chars
fieldsFormField[]Max 8 entries

Each FormField:

PropTypeConstraint
namestringMin 1, max 40 chars
labelstringMin 1, max 120 chars
kind'text' | 'textarea' | 'rating' | 'email' | 'select'Enum only
requiredboolean
optionsstring[] (optional)Max 20 entries; each max 120 chars — required when kind is 'select'

Interactive widgets (toggle-button, submit-form) reference module endpoints via a relative action path. The actionPathSchema validates shape only (C1); the storefront (C2) binds it to the originating module’s mount.

Rules enforced:

  • Must start with /store/v1/modules/.
  • Max 512 chars.
  • Allowlist regex: only URL-safe path characters after the prefix (A–Z a–z 0–9 - _ . ~ / : @ ! $ & ' ( ) * + , ; =).
  • % is excluded — no encoding channel for .. or CRLF.
  • Must not contain a .. path segment (explicit .refine as defence in depth).
  • No CR/LF or ASCII control characters (not in the allowlist class).
export const actionPathSchema = z
.string()
.min(ACTION_PATH_PREFIX.length)
.max(512)
.regex(SAFE_PATH_BODY_RE, 'action path must be a clean relative /store/v1/modules/... path')
.refine((p) => !p.split('/').includes('..'), {
message: 'action path must not contain `..` traversal',
});
export function parseWidget(raw: unknown): WidgetDescriptor | null
  • Accepts a JSON string (byte-capped, then JSON.parsed) or an already-parsed value.
  • Returns the typed WidgetDescriptor on success.
  • Returns null on any failure — oversized, non-JSON, unknown type, discriminator mismatch, out-of-bounds prop, bad enum, bad path, unknown key. Never throws.
  • This is the fail-closed contract the storefront RSC relies on: “render nothing, never 500 the page”.

export type ThemeSettings = Record<string, unknown>;
export type KnownThemeSettings = DocumentedThemeSettings & ThemeSettings;

ThemeSettings is the shape that GET /store/v1/theme returns in ActiveTheme.settings — an open-ended record. The core treats settings as opaque: it never reads or executes the referenced settingsSchema JSON-schema file. Validation against the file is the theme’s own build-time concern.

Documented knobs (DocumentedThemeSettings)

Section titled “Documented knobs (DocumentedThemeSettings)”

These are the design-token settings keys the MIT storefront recognises and maps to CSS custom properties (3.9d). Documenting them gives editors autocomplete without closing the contract — KnownThemeSettings intersects with the open ThemeSettings so arbitrary author keys still type-check.

Typography values are CSS font-family stacks (system stacks only — no webfont files or Google Fonts, per the RGPD self-host rule).

KeyCSS variable / effectType
background--backgroundstring (CSS color)
foreground--foregroundstring (CSS color)
primary--primarystring (CSS color)
primaryHover--primary-hoverstring (CSS color)
primaryActive--primary-activestring (CSS color)
primaryForeground--primary-foregroundstring (CSS color)
accent--accentstring (CSS color)
accentForeground--accent-foregroundstring (CSS color)
ring--ringstring (CSS color)
radius--radiusstring (CSS length)
fontSans--font-sansstring (font-family stack)
fontHeading--font-headingstring (font-family stack)
logoUrlLayout logo <img> srcstring (absolute https:// or root-relative)
header.layoutChrome variant (not a CSS var)'simple' | 'mega'
cart.affordanceChrome variant (not a CSS var)'drawer' | 'page-link'

header.layout defaults to simple; cart.affordance defaults to drawer. Unknown values fall back to the default so the default theme is unaffected.

export function defineThemeSettings<T extends ThemeSettings>(defaults: T): T

A pure no-op at runtime (returns its argument unchanged). Gives editor autocomplete and a single inferred T to reuse. Does not read or validate against the settingsSchema file.

import { defineThemeSettings, type KnownThemeSettings } from '@sovecom/theme-sdk';
export const defaults = defineThemeSettings<KnownThemeSettings>({
background: '#ffffff',
foreground: '#111827',
primary: '#4f46e5',
fontSans: 'Inter, system-ui, sans-serif',
'header.layout': 'simple',
});

The storefront reads two endpoints from GET /store/v1/*. The types below are the compile-time contract: a CI type-conformance guard asserts the in-tree view types stay assignable to these.

ActiveTheme (verbatim from store-contract.ts)

Section titled “ActiveTheme (verbatim from store-contract.ts)”

Returned by GET /store/v1/theme.

export interface ActiveTheme {
readonly name: string;
readonly version: string;
readonly settings: ThemeSettings;
}
FieldTypeMeaning
namestringThe active theme’s slug name
versionstringSemver version string
settingsThemeSettingsOpaque settings record (the author’s JSON-schema file describes its shape)

SlotBinding (verbatim from store-contract.ts)

Section titled “SlotBinding (verbatim from store-contract.ts)”

One cleanly-resolved slot binding inside SlotMap.

export interface SlotBinding {
readonly module: string;
readonly component: string;
}
FieldTypeMeaning
modulestringThe module that fills this slot
componentstringThe widget type the storefront maps to that module’s UI

Returned by GET /store/v1/slots.

export type SlotMap = Record<string, SlotBinding>;

Keys are slot slugs. Only cleanly-resolved slots are present — conflicts and unresolved slots are omitted, never silently picked. A slot absent from the map means no module fills it; the theme should render nothing or a placeholder.


These are re-exported from @sovecom/module-sdk so both SDKs gate against a single source of truth.

ExportValue / SignatureMeaning
MANIFEST_MAX_BYTES64 * 1024 (65 536)Byte cap for manifests, templates, and widget descriptors
CORE_API_VERSIONstring (semver)Current core API version; used by assertCoreCompatible
assertCoreCompatible(manifest: { compatibleCore: string }) => voidThrows if the theme’s range does not include the current core’s major

ConstantValueDescription
THEME_SDK_VERSION'0.0.1'SDK package version
MANIFEST_MAX_BYTES65536Byte cap (64 KiB) shared across manifests, templates, widgets
WIDGET_MAX_BYTES65536Alias of MANIFEST_MAX_BYTES
MAX_REGION_DEPTH2Max regions nesting depth in templates
THEME_NAME_RE/^[a-z][a-z0-9-]*$/Theme name slug pattern
SLOT_SLUG_RE/^[a-z][a-z0-9-]*$/Slot slug pattern
SECTION_TYPE_RE/^[a-z][a-z0-9-]*$/Section type slug pattern
REGION_NAME_RE/^[a-z][a-z0-9-]*$/Region name slug pattern
TEMPLATE_PATH_RE/^[a-z0-9][a-z0-9_-]*(\/[a-z0-9][a-z0-9_-]*)*\.json$/Template file path pattern
PAGE_TYPES['home','product','category','products','search','cart']All valid page type values
WIDGET_TYPES['star-rating-summary','review-list','product-carousel','toggle-button','submit-form']Closed widget vocabulary

Last updated: 2026-06-25