
# Store & Template System

The Store provides a curated catalogue of pre-built modules that can be installed into a project. Each module encapsulates one or more features together with optional extensions, allowing rapid application development with consistent data modelling patterns.

> **Migration note**: The legacy `/settings/templates` route has been removed. Users navigating to the old URL will receive a 404. All references should be updated to point directly to `/settings/store`.

## Overview

The store system is built from three registry concepts defined in `packages/shared/src/templates/`:

| Concept | Description |
|---|---|
| **StoreModule** | A single installable unit of functionality (e.g. Contacts, Real Estate) |
| **StoreTag** | A classification label attached to modules (industry or function) |
| **StoreCollection** | A curated bundle of modules with pre-selected extensions |

Registries are populated at startup via a side-effect import of `store.init.ts`, which calls `registerModule`, `registerTag`, and `registerCollection` for every entry defined in `store.modules.ts`, `store.tags.ts`, and `store.collections.ts`.

---

## Navigating to the Store

1. Open a project in the backoffice.
2. Go to **Project Settings**.
3. Click the **Store** tile (icon: `fa-puzzle-piece`).

The store page is served from:

```
/projects/[projectId]/settings/store
```

> **Note**: The old `/projects/[projectId]/settings/templates` route no longer exists. Update any bookmarks or links to use the store URL directly.

---

## Store UI

### Layout

The store page (`+page.svelte` → `StoreLayout.svelte`) is composed of:

- **Search bar** – full-text search across module names and descriptions (`searchModules()`).
- **Updates banner** (`StoreUpdatesBanner.svelte`) – shown when one or more installed modules have a newer version available. Lists each outdated module with current and available version numbers, and the latest changelog entry.
- **Sidebar** (`StoreSidebar.svelte`) – filter panel with three sections:
  - *All Modules* – clears any active filter.
  - *Industry* tags – e.g. Real Estate, Handyman.
  - *Function* tags – e.g. CRM, Blog.
  - *Collections* – curated bundles (see below).
- **Module list** (`StoreModuleList.svelte`) – renders a `StoreModuleRow` for each filtered module.

### Module Row (`StoreModuleRow.svelte`)

Each row displays:
- Module icon, label, and current/installed version.
- Tag labels derived from the module's `tags` array.
- A status badge or action button:
  - **Install** (primary) – module not yet installed.
  - **Update** (warning) – a newer version is available.
  - **Installed** (success badge) – up to date.
- An expand chevron that reveals the module description and its extension list.

Expanding a row shows all extensions via `StoreExtensionToggle.svelte`. Required extensions are checked and disabled; optional extensions can be toggled by the user. Dependencies between extensions are resolved automatically via `resolveExtensionDependencies()`.

Clicking **Install** or **Update** opens the `InstallModuleModal`.

### Extension Toggle (`StoreExtensionToggle.svelte`)

Each extension is shown with:
- Its translated label.
- A badge indicating whether it is **required**, **auto-added** (pulled in as a dependency of another selected extension, showing the requiring extension's name), or **optional**.
- A disabled checkbox when required or auto-added.

### Install Module Modal (`InstallModuleModal.svelte`)

The modal drives the installation flow:

1. Calls `executeTemplate()` with the module's feature name passed directly via the `features` array, along with the user's chosen extensions and a `priorExecutionResult` seeded from existing project data (see [Cross-Module Dependency Resolution](#cross-module-dependency-resolution)).
2. Sends a `PUT` request to `/api/projects/[projectId]/extensions` with an `ApplyTemplateBodyWithModule` payload that includes `moduleName`, `moduleVersion`, and `installedExtensions`.
3. Re-executes already-installed modules to resolve cross-module dependencies (see [Re-Execution of Installed Modules](#re-execution-of-installed-modules)).
4. On success the project's `installedModules` array is updated server-side (see [Module Installation Tracking](#module-installation-tracking)).

---

## Creating a New Project with Modules

The **New Project** page (`/projects/new`) allows modules to be selected before the project is created. Project creation always goes through this UI flow — modules cannot be selected via the API.

### UI Flow

1. Navigate to `/projects/new`.
2. Optionally click a **Collection** in the sidebar to pre-select an entire bundle of modules with their recommended extensions. The project name field is auto-populated from the collection label.
3. Toggle individual modules on/off; expand a module row to adjust its extensions.
4. Enter or adjust the project name using the **project name input** field.
5. Click **Create** to submit.

After creation, the browser is redirected to the new project URL (`/projects/[projectId]`). The page waits for a full load before proceeding.

> **Note**: The `POST /api/projects` endpoint no longer accepts a `templateName` parameter. Module selection is exclusively handled through the new project UI. Any integrations or scripts that previously passed `templateName` to the API must be updated to use the UI flow.

On creation the server applies all selected module templates in a single transaction.

---

## StoreModule

Defined in `store.types.ts`:

```typescript
interface StoreModule {
  name: FeatureName;           // unique identifier (matches FeatureName enum)
  label: InlineTranslation;
  description: InlineTranslation;
  icon: string;                // URL to module icon image
  version: string;             // semver string, e.g. "1.2.0"
  tags: string[];              // StoreTag names
  requiredExtensions: FeatureExtensionName[];
  optionalExtensions?: FeatureExtensionName[];
  extensions: FeatureExtensionName[];   // all available extensions
  changelog?: ChangelogEntry[];
}
```

### FeatureExtension Dependencies

Extensions can declare dependencies on other extensions via the optional `requires` field (added in this release):

```typescript
interface FeatureExtension {
  name: string;
  label: InlineTranslation;
  /** Other extensions that must be installed before this one */
  requires?: FeatureExtensionName[];
  check: (options: FeatureExtensionCheckOptions) => FeatureExtensionCheckResult;
  execute: (options: FeatureExtensionExecuteOptions) => void;
}
```

When a user selects an extension that has `requires`, the required extensions are automatically added to the selection (shown with an "auto-added" badge). This is handled by `resolveExtensionDependencies()` in `packages/shared`.

---

## StoreCollection

Collections are curated bundles defined in `store.collections.ts`:

```typescript
interface StoreCollection {
  name: string;
  label: InlineTranslation;
  description: InlineTranslation;
  icon: string;
  color: string;
  modules: FeatureName[];
  preSelectedExtensions?: FeatureExtensionName[];
}
```

### Built-in Collections

| Collection | Modules included | Description |
|---|---|---|
| **Real Estate Pack** | Contact, Todo, Real Estate | — |
| **CRM Pack** | Contact, Email | — |
| **Blog Pack** | Blog | — |
| **Handyman Pack** | Handyman | — |
| **Warehouse Pack** | Warehouse | Product catalog and inventory management — ready for warehouse teams |
| **SaaS Website Pack** | Website SaaS | — |
| **Landing Page Pack** | Website Landing Page | — |

Selecting a collection in the sidebar filters the module list to the collection's modules and pre-selects the recommended extensions.

The **Warehouse Pack** (`warehouse-pack`) is identified by the color `#65A30D` and the icon at `https://img.icons8.com/fluency/96/warehouse.png`. Selecting it on the new project page auto-populates the project name with "Warehouse" / "Lagerverwaltung".

---

## StoreTag

Tags classify modules for filtering. Each tag has a `type` of either `"industry"` or `"function"`:

```typescript
interface StoreTag {
  name: string;
  label: InlineTranslation;
  type: "industry" | "function";
  icon?: string;   // FontAwesome class
}
```

All tags are registered through `store.tags.ts` and can be queried with `getTagsByType("industry")` or `getTagsByType("function")`.

---

## Module Reference

### Check-in Module (`FeatureName.CHECKIN`)

Visitor registration and check-in management with GDPR-compliant data retention, QR code scanning, and email notifications.

- **Features**: `VISITOR`, `CONTACT`

> **Note**: The `comingSoon` flag has been removed — this module is now fully available for installation.

#### Extensions

| Extension | Name constant | Type | Required? |
|---|---|---|---|
| Visitor Data Type | `VISITORS_TYPE` | Data type | ✅ Required |
| Visitor Events Data Type | `VISITOR_EVENTS_TYPE` | Data type | ✅ Required |
| Check-in Application | `CHECKIN_APP` | Application | ✅ Required |
| GDPR Visitor Cleanup | `CHECKIN_GDPR` | Action (cron) | ✅ Required |
| Check-in Notifications | `CHECKIN_NOTIFICATIONS` | Actions (email) | Optional |
| Visitors Backoffice Module | `VISITORS_BOM` | BOM | Optional |
| Visitor Events Backoffice Module | `VISITOR_EVENTS_BOM` | BOM | Optional |

#### Extension Details

##### Visitor Data Type (`VISITORS_TYPE`)

Creates a data type for storing visitor records. Fields include:

- `firstName` — First name (string)
- `lastName` — Last name (string)
- `email` — Email address (string)
- `company` — Company name (string)
- `contactPersonId` — Foreign-key reference to a Contact record (linked to `FeatureRef.CONTACT_TYPE` when the Contact module is installed)
- `latestEventType` — Most recent check-in event type (`"in"` | `"out"`)
- `latestEventAt` — Timestamp of the latest event (datetime)
- `deleteOn` — Scheduled deletion timestamp for GDPR retention (datetime)
- `deleteOnPreference` — Visitor-selected retention period in milliseconds (number)

Navigation entry: `/visitors` (icon: `fa-address-book`)

##### Visitor Events Data Type (`VISITOR_EVENTS_TYPE`)

Creates a data type for tracking individual sign-in and sign-out events. Fields include:

- `visitorId` — Foreign-key reference to a Visitor record (linked to `FeatureRef.VISITOR_TYPE`)
- `type` — Event type enum: `"in"` (Sign In) | `"out"` (Sign Out)
- `createdAt` — Event timestamp (datetime)

Navigation entry: `/visitor-events` (icon: `fa-clock-rotate-left`)

##### Check-in Application (`CHECKIN_APP`)

Creates a SvelteKit application with a single public-facing page at `/` containing the `CheckIn` widget (`WIDGET_CHECKIN`). The application is configured with:

- No navbar or tab navbar (kiosk-style layout)
- Public access (no authentication required)
- Default widget data pre-populated with welcome text and GDPR small print in English and German
- QR code scanner enabled by default
- Configurable deletion period options for visitor data retention

The `CheckIn` widget (`WIDGET_CHECKIN` / `"CheckIn"`) can also be added independently to any page via the widget catalogue (category: `content`, visible in catalogue).

##### GDPR Visitor Cleanup (`CHECKIN_GDPR`)

Creates a cron-triggered action that runs daily at 02:00 (`0 2 * * *`) to automatically delete visitor records and associated events whose `deleteOn` timestamp has passed. The action is pre-configured with:

- `visitorCollectionName` — derived from the Visitor type
- `visitorEventCollectionName` — derived from the Visitor Events type

> **Dependency**: This extension requires both `VISITORS_TYPE` and `VISITOR_EVENTS_TYPE` to be present. If either is absent, the extension is skipped silently.

##### Check-in Notifications (`CHECKIN_NOTIFICATIONS`)

Creates three separate email actions triggered via API:

| Action | Purpose |
|---|---|
| Visitor Pass | Sends a visitor pass email to the registered visitor |
| Contact Person Notification | Notifies the designated contact person of an arrival |
| Calendar Invitation | Sends a calendar invite (iCal format) to relevant parties |

Each action uses a `SendMail` block connected to an API trigger. The trigger's configuration schema includes a `sendEmailConfigurationId` field with a foreign-ID selector pre-scoped to the project's send-email configurations collection.

##### Visitors Backoffice Module (`VISITORS_BOM`)

Adds a **Visitors** management page to the backoffice navigation sidebar with:

- Header text: "Visitors" / "Besucher"
- Navigation entry icon: `fa-address-book`
- Navigation path: `/visitors`

##### Visitor Events Backoffice Module (`VISITOR_EVENTS_BOM`)

Adds a **Visitor Events** log page to the backoffice navigation sidebar with:

- Header text: "Visitor Events" / "Besucherereignisse"
- Navigation entry icon: `fa-clock-rotate-left`
- Navigation path: `/visitor-events`

#### Data Model Cross-Module Dependencies

| Extension | Looks for | Effect when found |
|---|---|---|
| `visitor.type` | `FeatureRef.CONTACT_TYPE` | Adds `contactPersonId` foreign-key field linking visitors to contacts |
| `visitor-events.type` | `FeatureRef.VISITOR_TYPE` | Adds `visitorId` foreign-key field linking events to visitor records |
| `checkin-app` | `FeatureRef.VISITOR_TYPE` | Configures widget `visitorTypeId` |
| `checkin-app` | `FeatureRef.VISITOR_EVENTS_TYPE` | Configures widget `visitorEventTypeId` |
| `checkin-app` | `FeatureRef.CONTACT_TYPE` | Configures widget `contactsTypeId` |
| `checkin-gdpr` | `FeatureRef.VISITOR_TYPE` | Sets `visitorCollectionName` on the cron trigger |
| `checkin-gdpr` | `FeatureRef.VISITOR_EVENTS_TYPE` | Sets `visitorEventCollectionName` on the cron trigger |

#### Check-in Widget (`CheckInWidget.svelte`)

The `CheckIn` widget provides a self-contained visitor registration UI. Its schema-driven configuration options include:

| Field | Type | Description |
|---|---|---|
| `title` | `InlineTranslation` | Heading shown on the welcome screen |
| `description` | `InlineTranslation` | Sub-heading / instructions |
| `successSignInMessage` | `InlineTranslation` | Message displayed after a successful check-in |
| `successSignOutMessage` | `InlineTranslation` | Message displayed after a successful check-out |
| `smallPrint` | `InlineTranslation` | GDPR / privacy notice text |
| `scannerEnabled` | `boolean` | Whether the QR code scanner button is shown |
| `scannerButtonText` | `InlineTranslation` | Label for the scanner button |
| `deletionPeriods` | `DeletionPeriod[]` | Selectable GDPR retention periods offered to the visitor |
| `visitorTypeId` | `string` | ID of the Visitor data type |
| `visitorEventTypeId` | `string` | ID of the Visitor Events data type |
| `contactsTypeId` | `string` | ID of the Contacts data type |

The widget's internal state machine moves through the following states: `welcome` → `register` → `success` (check-in path) or `welcome` → `scanner` → `profile` → `success` (QR scan / check-out path).

Fields `latestEventType`, `latestEventAt`, `deleteOn`, and `deleteOnPreference` are automatically managed by the widget and are hidden from the registration form shown to visitors.

#### Visitor API Endpoints

Three server-side API endpoints are registered under `/api/projects/[projectId]/visitors/`:

**`POST /api/projects/[projectId]/visitors`**

Creates a new visitor record.

Request body:
```json
{
  "collectionName": "<visitor collection name>",
  "data": { "firstName": "Jane", "lastName": "Doe", "..." }
}
```

Response: `201 Created` with `{ "id": "<new visitor id>" }`

---

**`GET /api/projects/[projectId]/visitors/[visitorId]`**

Retrieves a single visitor record.

Query parameters:
- `collectionName` (required) — the visitor collection name

Response: `200 OK` with the visitor document, or `404` if not found. Returns `400` if `collectionName` is omitted.

---

**`PATCH /api/projects/[projectId]/visitors/[visitorId]`**

Partially updates a visitor record.

Request body:
```json
{
  "collectionName": "<visitor collection name>",
  "data": { "latestEventType": "out", "latestEventAt": 1700000000000 }
}
```

Response: `200 OK` with `{ "ok": true }`

---

**`DELETE /api/projects/[projectId]/visitors/[visitorId]`**

Deletes a visitor record.

Query parameters:
- `collectionName` (required) — the visitor collection name

Response: `200 OK` with `{ "ok": true }`, or `400` if `collectionName` is omitted.

---

**`POST /api/projects/[projectId]/visitors/[visitorId]/events`**

Creates a new visitor event (sign-in or sign-out) linked to the given visitor.

Request body:
```json
{
  "collectionName": "<visitor events collection name>",
  "data": { "visitorId": "<visitorId>", "type": "in", "createdAt": 1700000000000 }
}
```

Response: `201 Created` with `{ "id": "<new event id>" }`

#### iCal Generation (`packages/server/src/utils/ical.ts`)

A utility function `generateICalContent(options: ICalEventOptions)` is available on the server for generating RFC 5545-compliant `.ics` file content, used by the Calendar Invitation notification action.

```typescript
interface ICalEventOptions {
  title: string;
  description?: string;
  location?: string;
  start: Date;
  end: Date;
  organizer?: { name: string; email: string };
  attendee?: { name: string; email: string };
  url?: string;
}
```

The generated content uses `PRODID:-//smallstack//Check-in//EN`, `METHOD:REQUEST`, and includes proper text escaping (backslashes, semicolons, commas, newlines) per the RFC.

---

### Warehouse Module (`FeatureName.WAREHOUSE`) — v1.0.0

Inventory management with product catalog, stock levels, locations and movements.

- **Tags**: `logistics`, `ecommerce`
- **Icon**: `https://img.icons8.com/fluency/96/warehouse.png`
- **Color**: `#65A30D`

#### Extensions

| Extension | Name constant | Type | Required? |
|---|---|---|---|
| Product Data Type | `PRODUCTS_TYPE` | Data type | ✅ Required |
| Products Backoffice Module | `PRODUCTS_BOM` | BOM | Optional |
| Storage Location Data Type | `WAREHOUSE_LOCATION_TYPE` | Data type | ✅ Required |
| Storage Locations Backoffice Module | `WAREHOUSE_LOCATION_BOM` | BOM | Optional |
| Warehouse Stock Data Type | `WAREHOUSE_TYPE` | Data type | ✅ Required |
| Warehouse Backoffice Module | `WAREHOUSE_BOM` | BOM | Optional |

The three data-type extensions (`PRODUCTS_TYPE`, `WAREHOUSE_LOCATION_TYPE`, `WAREHOUSE_TYPE`) are required and always installed. The three BOM (Backoffice Module) extensions are optional and provide the corresponding navigation entries and list views in the backoffice sidebar.

> **Note**: The Warehouse Stock type (`WAREHOUSE_TYPE`) depends on both the Product type and the Storage Location type being present. If either is skipped, the warehouse stock type is also skipped automatically.

#### Data Model

The Warehouse module installs three interconnected data types:

**Products** (`FeatureRef.PRODUCTS_TYPE`)
- Multilingual title, short title, description, and short description
- `tags` — array of strings
- `prices` — array of price objects (value + price zone ID)
- `options` — product options (e.g. size, colour)
- `variants` — product variants
- `files` — attached files
- `status` — `"active"` | `"inactive"` (default: `"active"`)
- `supplier` — supplier information
- Required field: `title`
- Navigation: `/products` (icon: `fa-box-open`)

**Storage Locations** (`FeatureRef.WAREHOUSE_LOCATION_TYPE`)
- `name` — multilingual name of the storage location (e.g. "Hauptlager München", "Keller")
- `zone` — area or zone within the warehouse (e.g. Receiving, Shipping, Cold Storage)
- `description` — free-text description (textarea)
- Required field: `name`
- Navigation: `/warehouse-locations` (icon: `fa-map-marker-alt`)

**Warehouse Stock** (`FeatureRef.WAREHOUSE_TYPE`)
- `name` — multilingual name of the stock item
- `productId` — foreign-key reference to a Product record (uses `INPUT_WIDGET_FOREIGN_ID` / `VIEW_WIDGET_FOREIGN_ID`)
- `locationId` — foreign-key reference to a Storage Location record
- `quantity` — numeric stock quantity
- `minQuantity` — minimum stock threshold
- `unit` — unit of measurement
- `status` — `"in_stock"` | `"low_stock"` | `"out_of_stock"` (default: `"in_stock"`)
- `notes` — free-text notes (textarea)
- Required fields: `name`, `productId`, `locationId`, `quantity`
- Navigation: `/warehouse` (icon: `fa-warehouse`)

---

### Product Module (`FeatureName.PRODUCT`) — v1.0.0

Standalone product catalog with variants, pricing and media.

- **Tags**: `ecommerce`, `logistics`
- **Icon**: `https://img.icons8.com/fluency/96/box.png`
- **Color**: `#EA580C`

#### Extensions

| Extension | Name constant | Type | Required? |
|---|---|---|---|
| Product Data Type | `PRODUCTS_TYPE` | Data type | ✅ Required |
| Products Backoffice Module | `PRODUCTS_BOM` | BOM | Optional |

The Product module can be installed independently when only a product catalog is needed, without the full warehouse stock and location management provided by the Warehouse module.

---

## Module Installation Tracking

When a module is installed through the store, the server records the installation on the project document.

### API endpoint

```
PUT /api/projects/[projectId]/extensions
```

The request body extends `ApplyTemplateBody` with optional module tracking fields:

```typescript
interface ApplyTemplateBodyWithModule extends ApplyTemplateBody {
  /** When provided, records the installation on the project */
  moduleName?: string;
  moduleVersion?: string;
  installedExtensions?: string[];
}
```

### Server logic (`+server.ts`)

If `moduleName` and `moduleVersion` are present in the request body, the handler (inside the existing MongoDB transaction) writes to the `projects` collection:

- **New module**: `$push` an `InstalledModule` document onto `project.installedModules`.
- **Existing module** (re-install or update): `$set` the `updatedAt`, `updatedVersion`, and `installedExtensions` fields on the matching array element using the positional operator (`$`).

The `InstalledModule` shape (from `@smallstack/shared`):

```typescript
interface InstalledModule {
  moduleName: string;
  installedAt: string;       // ISO 8601
  installedVersion: string;
  installedExtensions: InstalledExtension[];
  updatedAt?: string;
  updatedVersion?: string;
}

interface InstalledExtension {
  extensionName: string;
  installedAt: string;
}
```

All writes participate in the same session/transaction as the template entity creation, so a failed entity application will also roll back the tracking update.

---

## Registry API (`store.registry.ts`)

Helper functions available from `@smallstack/shared`:

| Function | Description |
|---|---|
| `listModules()` | All registered `StoreModule` objects |
| `getModule(name)` | Look up a module by `FeatureName` |
| `searchModules(query)` | Full-text search over module names, labels, and descriptions |
| `listCollections()` | All registered `StoreCollection` objects |
| `getTagsByType(type)` | Tags filtered by `"industry"` or `"function"` |
| `getTag(name)` | Look up a tag by name |
| `isModuleInstalled(project, name)` | Whether a module is recorded in `project.installedModules` |
| `getInstalledVersion(project, name)` | Returns the installed version string or `undefined` |
| `hasModuleUpdate(project, name)` | `true` when the registry version is higher than the installed version |
| `getModulesWithUpdates(project)` | Array of `StoreModule` objects that have updates available |
| `resolveExtensionDependencies(selected, getExt)` | Expands a selection to include all required extensions |

---

## Template Execution (`executeTemplate`)

The `executeTemplate()` function in `packages/shared/src/services/template-execution.service.ts` drives all module installation logic. It accepts a `features` array of feature name strings directly, rather than a `FeatureTemplate` wrapper object.

```typescript
interface TemplateExecutionOptions {
  /** Feature names whose extensions should be executed */
  features: string[];
  currentData: FeatureExtensionExecuteCurrentDataOptions;
  answers?: { [extensionName: string]: { [code: string]: string } };
  autoSelectRecommended?: boolean;
  selectedExtensions?: FeatureExtensionName[];
  priorExecutionResult?: FeatureTemplateExecutionResult;
}
```

Callers pass the module's feature name(s) directly:

```typescript
executeTemplate({
  features: [mod.name],
  currentData: ...,
  autoSelectRecommended: true,
  priorExecutionResult: accumulatedResult,
});
```

This applies in all three call sites:
- **Install Module Modal** — installs a single module into an existing project.
- **New Project page** — accumulates results across all selected modules before project creation.
- **Re-execution of installed modules** — updates dependent modules after a new module is installed.

---

## Placeholder ID Generation & Updates

When templates define relationships between newly added entities they rely on stable IDs to preserve referential integrity. The platform uses placeholder tokens of the form `#generateId(<token>)#`.

### Lifecycle

1. **Collection**: All placeholder tokens are recursively collected from the template payload.
2. **Mapping**: A deterministic mapping from placeholder token → newly generated ObjectId is built. The same placeholder string always resolves to the same ID within one update execution.
3. **Replacement**: All occurrences of placeholder tokens are replaced before any entity is created.
4. **Persistence**: The mapping is stored with the corresponding `ProjectUpdate` so later incremental updates can extend the existing mapping.
5. **Validation**: After replacement a defensive assertion aborts the update if any `#generateId(` pattern still exists.

#### Reserved Tokens

- `#projectId#` – replaced with the current project identifier wherever it appears in navigation definitions or other project-scoped structures.

---

## Update Operation Model (Discriminated Union)

Template version diffs are expressed as a discriminated union:

```typescript
interface AddOperation {
  action: "add";
  collectionName: string;
  templateId: string;      // placeholder id
  from: null;
  to: unknown;             // full document payload
  modifier: Modifier;
  description: InlineTranslation;
  safetyCheck?: SafetyCheckResult;
}

interface ChangeOrRemoveOperation {
  action: "change" | "remove";
  collectionName: string;
  templateId: string;
  actualId: string;        // existing DB id resolved from prior mapping
  from: unknown;           // expected current value
  to: unknown;             // new value or null for remove
  modifier: Modifier;
  description: InlineTranslation;
  safetyCheck?: SafetyCheckResult;
}

type UpdateOperation = AddOperation | ChangeOrRemoveOperation;
```

**Invariants**:
- `action === 'add'` ⇒ no `actualId` field; inherently safe.
- `action !== 'add'` ⇒ `actualId` must be present; safety-checked against the live DB value.

---

## Template Versioning Policy

### Released Template Immutability

**CRITICAL RULE**: Once a project template is released and deployed to production, it must NEVER be modified. Any changes require creating a new version.

#### Why This Rule Exists
- **Data Integrity**: Existing projects depend on the template structure.
- **Update Safety**: The template update system relies on version comparison.
- **Rollback Safety**: Users can revert to previous template versions.
- **Testing Consistency**: Tests and validations are tied to specific versions.

#### Version Management Process

1. **New Features/Changes**: Always increment the version number.
2. **File Naming**: Follow the convention `template-name.v{X}.json`.
3. **Testing Only**: Use a separate template code (e.g. `real-estate-test`); never modify released versions.

#### Allowed Operations on Released Templates

- ✅ Bug fixes in new versions — create a higher version
- ✅ Adding new fields — create a higher version
- ✅ Performance improvements — create a higher version
- ❌ Modifying existing versioned files
- ❌ Removing fields
- ❌ Breaking changes to existing field behaviour

---

## Extension Deselection

When installing a module, users can toggle optional extensions on or off. The `executeTemplate()` function accepts an optional `selectedExtensions` parameter — an array of `FeatureExtensionName` values. When provided, only extensions in this list are executed during the check, execute, and postExecute phases. Required extensions should always be included.

This filtering applies in three places:

1. **Install Module Modal** — passes the user's toggle selection as `selectedExtensions`.
2. **New Project page** — passes the extensions chosen during project creation.
3. **Re-execution of dependent modules** — uses the `installedExtensions` stored on the project's `InstalledModule` record, so previously deselected extensions remain excluded.

If `selectedExtensions` is omitted, all extensions are executed (backward-compatible default).

---

## Cross-Module Dependency Resolution

Some extensions produce entities that reference entities from other modules. For example, the Real Estate type extension checks for an existing Contact type (via `FeatureRef.CONTACT_TYPE`) to add a contact-linking field. When modules are installed one-by-one in any order, these cross-module references must resolve regardless of installation sequence.

### The Problem

`executeTemplate()` builds entities in `currentExecutionResult`, keyed by `FeatureRef`. Extensions look up peer entities in this map (e.g. `currentExecutionResult[FeatureRef.CONTACT_TYPE]`). When installing Real Estate into a project that already has Contacts, the Contact entities exist in the database but **not** in `currentExecutionResult` — so the cross-module link is silently skipped.

### The Solution: `buildPriorResultFromCurrentData()`

Before calling `executeTemplate()`, the Install Module Modal calls `buildPriorResultFromCurrentData(currentData)`. This helper scans all entity collections in the current project data for entries with a `featureRef` field and builds a `FeatureTemplateExecutionResult` map with `action: "ignore"` for each. This map is passed as `priorExecutionResult` to seed the execution context.

The execution engine merges `priorExecutionResult` into `currentExecutionResult` before running extensions. Extensions can now find peer entities from previously installed modules. After execution, prior keys are stripped from the result so the caller only sees newly produced entities.

**Entity collections scanned**: types, compoundWidgets, actions, aiAgents, applications, roles, configurations, navigationEntries.

### Known Cross-Module Dependencies

| Extension | Looks for | Effect when found |
|---|---|---|
| `real-estate.type` | `FeatureRef.CONTACT_TYPE` | Adds contact-linking field to Real Estate type |
| `contact.type` | `FeatureRef.EMAILS_TYPE` | Adds saved-queries filter for email-linked contacts |
| `email.type` | `FeatureRef.CONTACT_TYPE` | Adds contact widget to Email type |
| `warehouse.type` | `FeatureRef.WAREHOUSE_LOCATION_TYPE` | Links stock items to storage locations via foreign-key field |
| `warehouse.type` | `FeatureRef.PRODUCTS_TYPE` | Links stock items to products via foreign-key field |
| `visitor.type` | `FeatureRef.CONTACT_TYPE` | Adds `contactPersonId` foreign-key field to Visitor type |
| `visitor-events.type` | `FeatureRef.VISITOR_TYPE` | Adds `visitorId` foreign-key field to Visitor Events type |
| `checkin-app` | `FeatureRef.VISITOR_TYPE` | Configures the Check-in widget `visitorTypeId` |
| `checkin-app` | `FeatureRef.VISITOR_EVENTS_TYPE` | Configures the Check-in widget `visitorEventTypeId` |
| `checkin-app` | `FeatureRef.CONTACT_TYPE` | Configures the Check-in widget `contactsTypeId` |
| `checkin-gdpr` | `FeatureRef.VISITOR_TYPE` | Sets `visitorCollectionName` on the GDPR cron trigger |
| `checkin-gdpr` | `FeatureRef.VISITOR_EVENTS_TYPE` | Sets `visitorEventCollectionName` on the GDPR cron trigger |

---

## Re-Execution of Installed Modules

When a new module is installed, already-installed modules may need to update their entities to pick up newly available cross-module links. For example, installing Contacts after Real Estate should retroactively add the contact-linking field to the Real Estate type.

### How It Works

After the primary module is installed, `InstallModuleModal` calls `getInstalledModulesForReExecution(project, newModuleName)` which returns all other installed modules. For each:

1. `executeTemplate()` is called with the dependent module's feature name passed directly via the `features` array, along with:
   - `priorExecutionResult` — accumulated results from the primary install plus any prior re-executions
   - `selectedExtensions` — the extensions originally chosen when the dependent module was installed (read from `project.installedModules`)
2. Only entities with `action: "update"` are sent to the server — new entities and ignored entities are skipped.
3. The server applies updates to existing entities without creating duplicates.

This ensures that cross-module features work regardless of installation order.

---

## Integration

The store integrates with:

- **Widget System**: Automatic UI generation based on extension-defined field types.
- **Actions System**: Workflow automation provided by action extensions.
- **File Sharing**: Document attachment extensions.
- **SignalDB**: Local-first synchronisation and offline support.
- **Permissions / RLS**: Row-level security enforced on every `PUT /extensions` call (`RLS_UPDATE_PERMISSION`).
- **MongoDB Transactions**: All entity creation and module tracking writes are wrapped in a single session to guarantee atomicity.

---

## Future Enhancements

Planned improvements:
- **Community modules**: Third-party contributed store modules.
- **Module dependencies**: Automatic installation of prerequisite modules.
- **Rollback support**: Uninstall or downgrade an installed module.
- **AI-assisted configuration**: Suggest modules from a natural language project description.
- **Import/export**: Share custom module configurations between projects.
