
# Schema Extensions Quick Reference

> **Audience: Developers extending the platform.** For comprehensive implementation details, see the [internal developer docs](/docs/development/schema-extensions.md).

Schema extensions provide reusable, versioned JSON schemas for data types.

## Quick Start

### Using Built-in Extensions

```typescript
import type { Type } from "@smallstack/shared";

const emailType: Type = {
  schemaExtensions: [
    { name: "core", version: 1 },
    { name: "email", version: 1 }
  ],
  // ... other fields
};
```

### Creating Custom Extensions

```typescript
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: 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

```typescript
// 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

```typescript
{
  schemaExtensions: [
    { name: "core", version: 1 },
    { name: "contacts", version: 1 },
    { name: "todos", version: 1 }
  ],
  schemaOverrides: {
    // Type-specific customizations
  }
}
```

### Override Extension Fields

```typescript
{
  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.

```typescript
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.

```typescript
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.

```svelte
<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

```typescript
// 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

- [x-type-schema i18n Documentation](./x-type-schema-i18n.md)
- [Widget System Overview](./widget-system.md)
