Skip to content

Theme Authoring Guide

A SovEcom theme is a declarative asset — JSON manifests, JSON page templates, and a settings schema. There is no runtime entrypoint, no worker, no capabilities list, and no database tables. defineTheme validates your configuration at build time using the same schema the core runs at install time, so a misconfigured theme fails fast rather than as an opaque install rejection.

Covers everything from create-sovecom-theme to a finished, installable theme. The exhaustive slot/widget/manifest contract lives at /guides/theme-contract/; this guide focuses on doing rather than specifying.



The create-sovecom-theme scaffolder emits a minimal, type-safe starter in one command. No interactive prompts, no network access.

Terminal window
pnpm create sovecom-theme aurora

To emit the starter into a specific directory:

Terminal window
pnpm create sovecom-theme aurora --dir themes/

The scaffolder validates the name against the theme-name slug rule (^[a-z][a-z0-9-]*$) and refuses to overwrite a non-empty destination. After it runs, install dependencies and type-check:

Terminal window
cd aurora
pnpm install
pnpm typecheck

typecheck runs tsc --noEmit over src/. A clean typecheck means every defineTheme / defineThemeSlots / defineThemeSettings call compiles against the SDK types and will be accepted by the core at install time.

aurora/
├── package.json
├── tsconfig.json
├── sovecom.theme.json ← the artifact the core ingests
├── settings.schema.json ← JSON-schema for admin UI / validation
└── src/
├── theme.ts ← typed, build-time-validated manifest
├── slots.ts ← slot-slug declarations
└── settings.ts ← default settings with autocomplete types

The file the core reads at install time. Keep it in sync with src/theme.ts — the TypeScript file is the validated authoring view, sovecom.theme.json is the shipped artifact.

{
"name": "aurora",
"displayName": "aurora",
"version": "0.1.0",
"compatibleCore": "^1.0.0",
"slots": ["product-page"],
"settingsSchema": "settings.schema.json"
}

Fields:

FieldRequiredRules
nameYesLowercase slug (^[a-z][a-z0-9-]*$), max 64 chars. Forms the install identity and URL segment.
displayNameYesHuman-readable label, max 128 chars.
versionYesValid semver, e.g. "0.1.0".
compatibleCoreYesValid semver range, e.g. "^1.0.0". Gated at install against CORE_API_VERSION.
slotsNoArray of lowercase slug strings, max 64 entries. Declare each slot once.
settingsSchemaNoRelative path to a JSON-schema file describing tunable settings, max 256 chars.
templatesNoArray of { page, path } declarations; see §5 Page templates.

defineTheme runs the same parseAndVerifyThemeManifest the core runs, so validation failures surface as TypeScript errors at build time rather than as API errors at install time:

import { defineTheme } from '@sovecom/theme-sdk';
import { slots } from './slots.js';
export const theme = defineTheme({
name: 'aurora',
displayName: 'aurora',
version: '0.1.0',
compatibleCore: '^1.0.0',
slots,
settingsSchema: 'settings.schema.json',
});
export default theme;

Slots are declarative metadata — they declare which slot slugs your theme renders in its storefront UI. The actual slot registry is derived at runtime from enabled modules’ manifests. defineThemeSlots only validates the slug shape and deduplication; it has no network effect.

import { defineThemeSlots } from '@sovecom/theme-sdk';
export const slots = defineThemeSlots(['product-page']);

Validation rules enforced by defineThemeSlots:

  • Each slot must match ^[a-z][a-z0-9-]*$.
  • Each slug must appear at most once in the array.
  • Throws a descriptive Error on the first violation — no silent truncation.

To expose more slots, add them to the array:

export const slots = defineThemeSlots([
'product-page',
'category-sidebar',
'cart-upsell',
]);

The returned array is frozen (Object.freeze), so it is safe to spread into the manifest config. Drop it directly into defineTheme({ ..., slots }) as shown in src/theme.ts.


4. Settings (src/settings.ts and settings.schema.json)

Section titled “4. Settings (src/settings.ts and settings.schema.json)”

Themes may expose tunable settings — design tokens, chrome flags, and custom author knobs — that admins override via the API or the admin UI.

defineThemeSettings is a compile-time identity helper: it types and returns your defaults object so you get autocomplete over the documented token keys, and a single inferred ThemeSettingsShape to reuse elsewhere. It has no runtime behaviour:

import { defineThemeSettings, type KnownThemeSettings } from '@sovecom/theme-sdk';
export const settings = defineThemeSettings<KnownThemeSettings>({
// ── design tokens → CSS custom properties ──────────────────────────
background: '#ffffff',
foreground: '#111111',
primary: '#2563eb',
primaryHover: '#1d4ed8',
primaryActive: '#1e40af',
primaryForeground: '#ffffff',
accent: '#7c3aed',
accentForeground: '#ffffff',
ring: '#2563eb',
radius: '0.5rem',
// System font stacks only — no webfonts / CDN (RGPD requirement).
fontSans: "Inter, 'Segoe UI', system-ui, sans-serif",
fontHeading: "Inter, 'Segoe UI', system-ui, sans-serif",
// ── chrome flags (not CSS vars — read by the layout directly) ──────
'header.layout': 'simple',
'cart.affordance': 'drawer',
});
export type ThemeSettingsShape = typeof settings;

The storefront maps these KnownThemeSettings keys onto CSS custom properties. All are optional; unrecognised keys are preserved in the settings bag and ignored by the CSS layer.

KeyCSS varNotes
background--backgroundPage background color
foreground--foregroundBody text color
primary--primaryBrand/primary color
primaryHover--primary-hoverHover state
primaryActive--primary-activeActive/pressed state
primaryForeground--primary-foregroundText on a primary surface
accent--accentAccent color
accentForeground--accent-foregroundText on an accent surface
ring--ringFocus ring color
radius--radiusCorner radius scale (CSS length)
fontSans--font-sansBase sans font-family stack
fontHeading--font-headingHeading font-family stack
logoUrl(layout prop)Absolute or root-relative logo URL

Chrome flags — not CSS vars; the layout reads them directly:

KeyValuesDefault
header.layout'simple' | 'mega''simple'
cart.affordance'drawer' | 'page-link''drawer'

An unknown value for either flag falls back to the default, so adding a new chrome variant is non-breaking.

The settingsSchema path in the manifest points at a JSON-schema file. The core stores the path opaque at install time — it never reads or executes the referenced file during manifest validation. The schema is consumed by the admin UI to render the settings form, and optionally by your own tooling for validation.

The scaffolder emits a starting schema for the two example keys:

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "aurora settings",
"type": "object",
"additionalProperties": false,
"properties": {
"primaryColor": {
"type": "string",
"description": "Primary brand colour, as a CSS hex string.",
"default": "#111111"
},
"showBreadcrumbs": {
"type": "boolean",
"description": "Whether product pages render breadcrumbs.",
"default": true
}
}
}

Extend it to cover every key you expose in src/settings.ts.


Page templates are declarative JSON documents — a page type plus an ordered list of sections, each a { type, settings? } pair. The storefront runtime resolves each type against a section registry and renders the sections in order. There is no template code: templates are pure data, validated by the same parseTemplate pipeline as defineTemplate.

home | product | category | products | search | cart
{
"page": "home",
"sections": [
{ "type": "hero", "settings": { "fullBleed": true } },
{ "type": "featured-products", "settings": { "limit": 6 } },
{ "type": "category-list" }
]
}

Layout sections with named regions (product page)

Section titled “Layout sections with named regions (product page)”

The columns section is a layout primitive — it accepts a regions map of named sub-section arrays. Nesting depth is capped at 2 levels (a region inside a region), and each section may declare at most 8 regions.

{
"page": "product",
"sections": [
{ "type": "breadcrumbs" },
{
"type": "columns",
"settings": {
"containerClass": "grid grid-cols-1 md:grid-cols-2 gap-8",
"rightClass": "space-y-6"
},
"regions": {
"left": [{ "type": "product-gallery" }],
"right": [
{ "type": "product-info" },
{ "type": "variant-selector" }
]
}
}
]
}

Add a templates array to sovecom.theme.json (and src/theme.ts) to wire each template file. Each entry is { page, path } where path is a safe relative slug path ending in .json (no .., no leading /):

{
"name": "aurora",
"displayName": "aurora",
"version": "0.1.0",
"compatibleCore": "^1.0.0",
"slots": ["product-page"],
"settingsSchema": "settings.schema.json",
"templates": [
{ "page": "home", "path": "templates/home.json" },
{ "page": "product", "path": "templates/product.json" },
{ "page": "category","path": "templates/category.json" },
{ "page": "products","path": "templates/products.json" },
{ "page": "search", "path": "templates/search.json" },
{ "page": "cart", "path": "templates/cart.json" }
]
}

Each page type may be declared at most once. Absent page types fall back to the default template set at render time.

Use defineTemplate to validate a template in TypeScript with the same pipeline the runtime uses:

import { defineTemplate } from '@sovecom/theme-sdk';
export const homeTemplate = defineTemplate({
page: 'home',
sections: [
{ type: 'hero', settings: { fullBleed: true } },
{ type: 'featured-products', settings: { limit: 6 } },
{ type: 'category-list' },
],
});

defineTemplate round-trips the config through parseTemplate (serialise → byte-cap check → JSON parse → Zod schema → depth walk), so it throws the same errors the runtime would at install time.

Use defineSection to pin a typed settings shape for editor autocomplete. This is a pure no-op at runtime — it returns its argument unchanged:

import { defineSection, type SectionDef } from '@sovecom/theme-sdk';
interface HeroSettings {
fullBleed?: boolean;
heading?: string;
}
export const heroSection: SectionDef<HeroSettings> = defineSection<HeroSettings>({
type: 'hero',
settings: { fullBleed: false, heading: 'Welcome' },
});

Themes do not run server code. The storefront (Next.js) fetches data from the SovEcom Store API; your theme’s page templates declare which sections appear and the section components read the data the storefront already fetched.

The two endpoints a theme surfaces are:

EndpointResponse typeWhat it returns
GET /store/v1/themeActiveTheme{ name, version, settings } — the active theme name, version, and the merged settings bag.
GET /store/v1/slotsSlotMapRecord<slot, { module, component }> — cleanly-resolved slot bindings only; conflicts are omitted.

The TypeScript types for both are exported from @sovecom/theme-sdk:

import type { ActiveTheme, SlotMap, SlotBinding } from '@sovecom/theme-sdk';
// Example: reading the active theme in a Next.js RSC
async function getActiveTheme(): Promise<ActiveTheme> {
const res = await fetch(`${process.env.API_URL}/store/v1/theme`, {
next: { revalidate: 60 },
});
if (!res.ok) throw new Error('Failed to fetch active theme');
return res.json() as Promise<ActiveTheme>;
}
// Example: reading the slot map
async function getSlotMap(): Promise<SlotMap> {
const res = await fetch(`${process.env.API_URL}/store/v1/slots`, {
next: { revalidate: 60 },
});
if (!res.ok) throw new Error('Failed to fetch slot map');
return res.json() as Promise<SlotMap>;
}

settings in ActiveTheme is the merged bag — live API settings overlaid on your theme’s bundled defaults. Your storefront components read it to apply design tokens and chrome flags.

For the full Store API reference, see /api-reference/. For the slot/widget contract (how modules contribute UI into slots), see /guides/theme-contract/.


The active theme name is resolved server-side in this order of precedence:

  1. STOREFRONT_THEME environment variable (server-only, not exposed to the browser).
  2. The live API theme’s name field from GET /store/v1/theme.
  3. default.

To preview your theme without changing the API configuration, start the storefront with:

Terminal window
STOREFRONT_THEME=aurora pnpm dev

This forces the storefront to resolve your theme’s template set and settings on every request without a rebuild. Changing back to the default is a restart with the variable unset.

Bundled themes as reference implementations

Section titled “Bundled themes as reference implementations”

The monorepo ships two bundled themes in apps/storefront-next/src/themes/. Both use the same section library but compose it differently:

  • default — flat nav, drawer cart, neutral palette, standard PDP layout.
  • boutique — mega-menu nav, page-link cart, warm editorial palette (ivory/terracotta/gold), system serif headings, full-bleed home hero, all-image grid PDP.

Boutique’s settings are defined in apps/storefront-next/src/themes/boutique/settings.ts and illustrate every documented KnownThemeSettings key as well as the two chrome flags.


A theme package is a directory containing at minimum:

  • sovecom.theme.json — the manifest.
  • Any files declared in templates[].path.
  • settings.schema.json (if settingsSchema is set).

Install via the API:

Terminal window
# POST /admin/v1/themes/install (multipart/form-data, field: file)
curl -X POST https://your-store.example.com/admin/v1/themes/install \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-F "file=@aurora.tar.gz"

The API enforces:

  • The byte cap (MANIFEST_MAX_BYTES from @sovecom/module-sdk).
  • All manifest schema rules (name slug, semver, slots deduplication, template path safety).
  • A semver major gate: compatibleCore must accept CORE_API_VERSION.

A theme that passes pnpm typecheck in your repo will pass these checks, because defineTheme runs the same validator.


Complete file set for a minimal but fully-declared theme:

sovecom.theme.json

{
"name": "aurora",
"displayName": "Aurora",
"version": "0.1.0",
"compatibleCore": "^1.0.0",
"slots": ["product-page"],
"settingsSchema": "settings.schema.json",
"templates": [
{ "page": "home", "path": "templates/home.json" },
{ "page": "product", "path": "templates/product.json" }
]
}

src/theme.ts

import { defineTheme } from '@sovecom/theme-sdk';
import { slots } from './slots.js';
export const theme = defineTheme({
name: 'aurora',
displayName: 'Aurora',
version: '0.1.0',
compatibleCore: '^1.0.0',
slots,
settingsSchema: 'settings.schema.json',
templates: [
{ page: 'home', path: 'templates/home.json' },
{ page: 'product', path: 'templates/product.json' },
],
});
export default theme;

src/slots.ts

import { defineThemeSlots } from '@sovecom/theme-sdk';
export const slots = defineThemeSlots(['product-page']);

src/settings.ts

import { defineThemeSettings, type KnownThemeSettings } from '@sovecom/theme-sdk';
export const settings = defineThemeSettings<KnownThemeSettings>({
background: '#f0f4ff',
foreground: '#111827',
primary: '#6366f1',
primaryHover: '#4f46e5',
primaryActive: '#4338ca',
primaryForeground: '#ffffff',
accent: '#ec4899',
accentForeground: '#ffffff',
ring: '#6366f1',
radius: '0.75rem',
fontSans: "'Segoe UI', system-ui, sans-serif",
fontHeading: "'Segoe UI', system-ui, sans-serif",
'header.layout': 'simple',
'cart.affordance': 'drawer',
});
export type ThemeSettingsShape = typeof settings;

templates/home.json

{
"page": "home",
"sections": [
{ "type": "hero", "settings": { "fullBleed": true } },
{ "type": "featured-products", "settings": { "limit": 8 } },
{ "type": "category-list" }
]
}

templates/product.json

{
"page": "product",
"sections": [
{ "type": "breadcrumbs" },
{
"type": "columns",
"settings": {
"containerClass": "grid grid-cols-1 md:grid-cols-2 gap-8",
"rightClass": "space-y-6"
},
"regions": {
"left": [{ "type": "product-gallery" }],
"right": [
{ "type": "product-info" },
{ "type": "variant-selector" }
]
}
}
]
}

All exports come from @sovecom/theme-sdk.

ExportKindUse
defineTheme(config)FunctionValidate + return a typed ThemeManifest. Throws on any violation.
defineThemeSlots(slugs)FunctionValidate slug shapes + deduplication; return a frozen array for the manifest.
defineThemeSettings<T>(defaults)FunctionIdentity helper — types defaults object, no runtime effect.
defineTemplate(config)FunctionValidate + return a typed ThemeTemplate. Throws on any violation.
defineSection<T>(def)FunctionIdentity helper — types a section definition for editor autocomplete.
parseAndVerifyThemeManifest(raw)FunctionParse + validate a raw manifest JSON string (same pipeline as the core).
parseTemplate(raw)FunctionParse + validate a raw template JSON string.
ThemeManifestTypeThe validated manifest shape.
ThemeTemplateTypeThe validated template shape ({ page, sections }).
TemplateSectionTypeA single section ({ type, settings?, regions? }).
PageTypeType'home' | 'product' | 'category' | 'products' | 'search' | 'cart'
ThemeSettingsTypeRecord<string, unknown> — the opaque settings bag.
KnownThemeSettingsTypeDocumentedThemeSettings & ThemeSettings — documented keys + open record.
ActiveThemeType{ name, version, settings }GET /store/v1/theme response.
SlotMapTypeRecord<slot, SlotBinding>GET /store/v1/slots response.
SlotBindingType{ module, component } — a single resolved slot.
THEME_SDK_VERSIONConstCurrent SDK package version string.
CORE_API_VERSIONConstThe core API version this SDK gates against (re-exported from @sovecom/module-sdk).
assertCoreCompatible(range)FunctionAssert a compatibleCore range accepts CORE_API_VERSION; throws if incompatible.

For the slot-filling and widget descriptor contract that modules use to contribute UI into your theme’s declared slots, see /guides/theme-contract/.

Last updated: 2026-06-25