Schema Extensions Quick Reference

Reusable, versioned JSON schemas for data types

schema-extensionsjson-schematypesreference

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 products type and the warehouse-location type 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 contactPerson foreign-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 visitorId foreign-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: 1minLength: 5 Widgets read from schema
Change description/title Update docs Metadata only

❌ Prohibited Changes

Change Example Why It Breaks
Change property types email: stringnumber 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.

See Also