Schema Extensions Quick Reference
Reusable, versioned JSON schemas for data types
Schema Extensions Quick Reference
Audience: Developers extending the platform. For comprehensive implementation details, see the internal developer docs.
Schema extensions provide reusable, versioned JSON schemas for data types.
Quick Start
Using Built-in Extensions
import type { Type } from "@smallstack/shared";
const emailType: Type = {
schemaExtensions: [
{ name: "core", version: 1 },
{ name: "email", version: 1 }
],
// ... other fields
};
Creating Custom Extensions
import { registerSchemaExtension } from "@smallstack/shared";
registerSchemaExtension({
id: "custom",
version: "v1",
schema: {
type: "object",
properties: {
customField: { type: "string" }
}
}
});
Built-in Extensions
| Extension | Description |
|---|---|
core:v1 |
Base entity fields (id, tenantId, timestamps, access) |
contacts:v1 |
Contact information (name, email, phone, address) |
email:v1 |
Email schema with Microsoft Graph support |
todos:v1 |
Task/todo schema (title, status, priority, due date) |
real-estate:v1 |
Real estate property information |
products:v1 |
Product catalog entries with variants, pricing, files and supplier info |
warehouse-location:v1 |
Physical storage locations with name, zone and description |
warehouse:v1 |
Warehouse stock items with product link, storage location and quantity |
visitor:v1 |
Visitor records with contact, check-in status, and GDPR retention fields |
visitor-events:v1 |
Visitor sign-in/sign-out event log linked to visitor records |
Products (products:v1)
Provides a data type for product catalog entries. Included in the Warehouse and Product modules.
Schema properties:
| Property | Type | Description |
|---|---|---|
title |
inline translation | Product title (required) |
shortTitle |
inline translation | Short display title |
description |
inline translation | Full product description |
shortDescription |
inline translation | Brief product description |
tags |
string[] |
Searchable tags |
prices |
ProductPriceSchema[] |
Price entries with value and price zone ID |
options |
ProductOptionSchema[] |
Configurable product options |
variants |
ProductVariantSchema[] |
Product variants (e.g. size, colour) |
files |
ProductFileSchema[] |
Attached files and media |
status |
"active" | "inactive" |
Availability status (default: "active") |
supplier |
ProductSupplierSchema |
Supplier information |
Navigation entry: /products — icon fa-box-open
Storage Locations (warehouse-location:v1)
Provides a data type for physical storage locations within a warehouse. Included in the Warehouse module.
Schema properties:
| Property | Type | Description |
|---|---|---|
name |
inline translation | Location name, e.g. "Hauptlager München" (required) |
zone |
string |
Area or zone, e.g. "Eingang", "Versand", "Kühllager" |
description |
string (textarea) |
Free-text description of the location |
Navigation entry: /warehouse-locations — icon fa-map-marker-alt
Warehouse Stock Items (warehouse:v1)
Provides a data type for warehouse stock entries. Each item links a product to a storage location and records a quantity. Included in the Warehouse module.
Dependency note: The warehouse stock type is only created when both the
productstype and thewarehouse-locationtype have been installed. If either was skipped during setup, the warehouse stock type will not be created.
Schema properties:
| Property | Type | Description |
|---|---|---|
name |
inline translation | Name of the stock item |
productId |
foreign ID (string) | Link to a product entry |
locationId |
foreign ID (string) | Link to a storage location |
quantity |
number |
Current stock quantity |
Navigation entry: /warehouse — icon fa-warehouse
Visitors (visitor:v1)
Provides a data type for visitor records. Included in the Check-in template. Stores personal contact details, the latest check-in status, and GDPR data-retention preferences.
Schema properties:
| Property | Type | Description |
|---|---|---|
firstName |
string |
Visitor's first name |
lastName |
string |
Visitor's last name |
email |
string |
Visitor's email address |
company |
string |
Visitor's company or organisation |
contactPerson |
foreign ID (string) | Link to an internal contact person (from the Contacts type) |
latestEventType |
"in" | "out" |
Whether the visitor is currently checked in or out |
latestEventAt |
number (timestamp ms) |
Unix timestamp of the most recent check-in or check-out event |
deleteOn |
number (timestamp ms) |
Unix timestamp after which the record will be deleted by the GDPR cleanup action |
deleteOnPreference |
number (duration ms) |
The visitor's chosen retention period in milliseconds |
Note: The
contactPersonforeign-ID field is only populated when the Contacts feature type has been installed alongside the Check-in template.
Navigation entry: /visitors — icon fa-address-book
Visitor Events (visitor-events:v1)
Provides a data type for logging individual visitor sign-in and sign-out events. Included in the Check-in template. Each event record links back to a visitor and records when and in which direction the transition occurred.
Schema properties:
| Property | Type | Description |
|---|---|---|
visitorId |
foreign ID (string) | Link to the corresponding visitor record |
type |
"in" | "out" |
Whether this event is a sign-in or a sign-out |
createdAt |
number (timestamp ms) |
Unix timestamp of when the event was recorded |
Dependency note: The
visitorIdforeign-ID field is only wired up when the Visitor type has been installed first. The Check-in template installs the Visitor type before the Visitor Events type to ensure this link is always present.
Navigation entry: /visitor-events — icon fa-clock-rotate-left
Version Compatibility Rules
When creating new versions of schema extensions, certain changes are allowed while others will break existing code.
✅ Allowed Changes
| Change | Example | Why It's Safe |
|---|---|---|
| Add new properties | Add phone field |
Old code ignores unknown properties |
| Add enum values | Add "urgent" to priority |
Existing code handles known values |
| Add new required fields | Make field required | Validation enforces requirement |
| Change constraints | minLength: 1 → minLength: 5 |
Widgets read from schema |
| Change description/title | Update docs | Metadata only |
❌ Prohibited Changes
| Change | Example | Why It Breaks |
|---|---|---|
| Change property types | email: string → number |
TypeScript types break, widgets render wrong |
| Remove enum values | Remove "pending" status |
Switch statements fail, UI breaks |
Note: Constraint changes may make existing data invalid but won't break code. Handle with data migrations if needed.
Example: Creating Version 2
// v1 - Initial schema
registerSchemaExtension({
id: "product",
version: "v1",
schema: {
type: "object",
properties: {
name: { type: "string" },
price: { type: "number" }
},
required: ["name", "price"]
}
});
// v2 - Add new optional field (OK)
registerSchemaExtension({
id: "product",
version: "v2",
schema: {
type: "object",
properties: {
name: { type: "string" },
price: { type: "number" },
description: { type: "string" } // New field
},
required: ["name", "price"]
}
});
Common Patterns
Multiple Extensions
{
schemaExtensions: [
{ name: "core", version: 1 },
{ name: "contacts", version: 1 },
{ name: "todos", version: 1 }
],
schemaOverrides: {
// Type-specific customizations
}
}
Override Extension Fields
{
schemaExtensions: [{ name: "contacts", version: 1 }],
schemaOverrides: {
properties: {
email: {
format: "email",
"x-type-schema": {
inputWidgetConfiguration: {
string: { type: "email" }
}
}
}
}
}
}
Feature Extension Dependencies
The requires Field
FeatureExtension objects can declare inter-extension dependencies via the optional requires field. When an extension lists other extensions in requires, those extensions must be installed before the declaring extension.
import type { FeatureExtension } from "@smallstack/shared";
const myExtension: FeatureExtension = {
name: "my-reports",
label: { en: "My Reports" },
// This extension can only be installed after "contacts-type" and "todos-type"
requires: ["contacts-type", "todos-type"],
check: (options) => { /* ... */ },
execute: (options) => { /* ... */ }
};
The requires field is purely declarative — it signals intent and is used by the resolveExtensionDependencies helper to automatically expand a user's selection to include all transitive prerequisites.
resolveExtensionDependencies Helper
When building extension selection UIs (such as the Store's install modal or the new project wizard), use resolveExtensionDependencies to expand a set of user-chosen extensions into the full set needed for installation. The helper traverses the requires graph transitively.
import {
resolveExtensionDependencies,
getFeatureExtension,
type FeatureExtensionName
} from "@smallstack/shared";
const userSelected: FeatureExtensionName[] = ["my-reports"];
const result = resolveExtensionDependencies(userSelected, getFeatureExtension);
// result.resolved — full set including auto-added dependencies
// result.autoAdded — extensions added automatically (not chosen by the user)
// result.missing — named dependencies whose FeatureExtension could not be found
Result shape
| Property | Type | Description |
|---|---|---|
resolved |
FeatureExtensionName[] |
Complete list of extensions to install (user selection + dependencies) |
autoAdded |
Map<FeatureExtensionName, FeatureExtensionName> |
Maps each auto-added extension to the extension that required it |
missing |
FeatureExtensionName[] |
Required extension names that are not registered (useful for warnings) |
Rendering auto-added extensions
In a selection UI, extensions that were auto-added due to a requires relationship should be shown as disabled checkboxes with a badge indicating which extension pulled them in. StoreExtensionToggle handles this out of the box via its isAutoAdded and requiredByExt props.
<StoreExtensionToggle
extName={ext}
isRequired={mod.requiredExtensions.includes(ext)}
isAutoAdded={autoAdded.has(ext)}
requiredByExt={autoAdded.get(ext)}
isChecked={selectedExtensions.has(ext)}
onToggle={() => toggleExtension(ext)}
/>
Full Example: Extension with Dependencies
// packages/shared/src/templates/features/my-feature.ts
import type { FeatureExtension, FeatureExtensionName } from "@smallstack/shared";
export const myReportsExtension: FeatureExtension = {
name: "my-reports" as FeatureExtensionName,
label: { en: "My Reports", de: "Meine Berichte" },
requires: [
"contacts-type" as FeatureExtensionName,
"todos-type" as FeatureExtensionName
],
check: ({ project }) => ({
canInstall: true,
hints: []
}),
execute: ({ applyEntity }) => {
applyEntity({ /* report type definition */ });
}
};
When a user selects my-reports in the Store UI, resolveExtensionDependencies automatically adds contacts-type and todos-type to the installation set, and those extensions are rendered as disabled (auto-added) in the toggle list.