# Module Management & Dependencies

**Date:** 2026-05-31
**Status:** Draft
**Depends on:** `foundation-auth-rbac`
**Referenced by:** (all module specs)

---

## Overview

Every Zync tenant can independently enable or disable any product module. This spec defines: the canonical module registry, inter-module dependency rules (hard vs. soft), the Settings → Modules UI (`/settings/modules`), the enable/disable flows including confirmation modals and cascading logic, data preservation semantics when a module is disabled, the `tenant_modules` table, and all supporting API endpoints.

The **System module** is a synthetic module used internally to anchor infrastructure-level resources. It is always enabled, never shown to users, and cannot be toggled through any UI (including the system-admin panel).

---

## Module Registry

Each module has a stable string ID used throughout code and DB. Display names and descriptions are i18n-keyed but the English defaults are specified here.

| Module ID | Display Name | Description | Nav group |
|---|---|---|---|
| `system` | *(internal)* | Core infrastructure — auth, shell, settings. Always on. | — |
| `crm` | Support Center | Helpdesk ticket management, customer communication. | Business |
| `customers` | Customers | Contact and organization management. | Business |
| `time_management` | Time Tracking | Log work hours against tasks and projects. | Workspace |
| `projects` | Projects | Project planning, milestones, and team assignment. | Workspace |
| `tasks` | Tasks | Task board, assignments, priorities, subtasks. | Workspace |
| `invoices` | Invoices | Draft, send, and track invoices. | Financials |
| `expenses` | Expenses | Log and categorize business expenses. | Financials |
| `billing` | Billing | Recurring billing plans and subscriptions. | Financials |
| `calendar` | Calendar | Scheduling, appointments, and availability. | Workspace |
| `marketing` | Marketing | Leads pipeline, campaigns, catalogs. | Business |
| `reports` | Reports | Financial, time, and operational analytics. | Resources |
| `kb` | Knowledge Base | Internal wiki and documentation. | Resources |
| `contractor_payouts` | Contractor Payouts | Pay contractors based on logged hours. | Financials |
| `ai_assistant` | AI Assistant | Cross-module AI productivity features. | *(global)* |

**Total toggleable modules: 14** (`system` excluded from all UI).

---

## Dependency Graph

### Definitions

- **Hard dependency:** Module B hard-depends on module A. If A is disabled, B is automatically disabled at the same time. The user is warned before confirmation. B cannot be re-enabled while A is disabled.
- **Soft dependency:** Module B soft-depends on module A. If A is disabled, B remains enabled but specific cross-module features in B become inactive. A warning is shown during the disable flow but no automatic disabling occurs.

### Dependency Matrix

`→` means "depends on". Left column = dependent module, top row = dependency.

| Dependent ↓ / Provides → | `tasks` | `customers` | `invoices` | `time_management` | `calendar` | `expenses` | `projects` |
|---|---|---|---|---|---|---|---|
| `projects` | soft | soft | — | — | — | — | — |
| `time_management` | **hard** | — | — | — | — | — | — |
| `invoices` | — | soft | — | — | — | — | — |
| `billing` | — | soft | **hard** | — | — | — | — |
| `crm` | — | soft | — | — | — | — | — |
| `contractor_payouts` | — | — | — | **hard** | — | — | — |
| `marketing` | — | soft | soft | — | soft | — | — |
| `reports` | — | — | soft | soft | — | soft | — |
| `ai_assistant` | soft | soft | soft | soft | soft | soft | soft |
| `expenses` | — | — | — | — | — | — | — |
| `tasks` | — | — | — | — | — | — | — |
| `calendar` | — | — | — | — | — | — | — |
| `kb` | — | — | — | — | — | — | — |

### Dependency Detail

```
tasks
  └─ no dependencies

customers
  └─ no dependencies

calendar
  └─ no dependencies

expenses
  └─ no dependencies

kb
  └─ no dependencies

projects
  ├─ soft → customers   (customer field on project hidden when customers disabled)
  └─ soft → tasks       (subtask panel hidden when tasks disabled)

time_management
  └─ hard → tasks       (time entries are linked to task records; cannot function without tasks)

invoices
  └─ soft → customers   (invoice recipient lookup hidden; manual name entry still works)

billing
  ├─ hard → invoices    (recurring billing generates invoice records; cannot function without invoices)
  └─ soft → customers   (subscription customer link hidden; standalone billing still works)

crm
  └─ soft → customers   (ticket customer context panel hidden when customers disabled)

contractor_payouts
  └─ hard → time_management  (payouts computed from approved time entries; cannot function without them)

marketing
  ├─ soft → customers   (lead-to-customer conversion unavailable)
  ├─ soft → calendar    (booking/scheduling integration unavailable)
  └─ soft → invoices    (mini-ecommerce checkout unavailable)

reports
  ├─ soft → invoices    (tax and revenue reports unavailable)
  ├─ soft → expenses    (expense reports unavailable)
  └─ soft → time_management  (utilization and billable-hours reports unavailable)

ai_assistant
  └─ soft → all         (AI features scoped to each module are unavailable when module is disabled)
```

### Cascading Disable Chains

When a user disables a module that is a hard dependency for others, **all transitively hard-dependent modules** are also disabled. Traversal is depth-first. Example:

- Disabling `tasks` → auto-disables `time_management` → auto-disables `contractor_payouts`
- Disabling `invoices` → auto-disables `billing`
- Disabling `time_management` (directly) → auto-disables `contractor_payouts`

Soft dependents are collected at all transitive levels and listed as "functionality warnings" but are never auto-disabled.

---

## UI: Settings → Modules

### Route

`/settings/modules`

Access: `OWNER` and `ADMIN` roles only (permission: `settings:modules:write` to toggle; `settings:modules:read` to view). `MEMBER` and below cannot reach this page — 403 → redirect to `/settings`.

### Page Layout (ASCII Wireframe)

```
┌─────────────────────────────────────────────────────────────────────┐
│  Settings                                                           │
│  ──────────────────────────────────────────────────────────────     │
│  < Business  < Locale  < Integrations  [Modules] ← active tab      │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Modules                                                            │
│  Manage which features are available for your workspace.            │
│                                                                     │
│  ┌──────────────────────┐  ┌──────────────────────┐                 │
│  │  [icon]  Tasks        │  │  [icon]  Projects     │                │
│  │  Board, assignments,  │  │  Planning, milestones │                │
│  │  priorities           │  │  team assignment      │                │
│  │                       │  │                       │                │
│  │  ● Enabled    [●  ]   │  │  ● Enabled    [●  ]   │                │
│  │                       │  │                       │                │
│  │  [▼ Details]          │  │  [▼ Details]          │                │
│  └──────────────────────┘  └──────────────────────┘                 │
│                                                                     │
│  ┌──────────────────────┐  ┌──────────────────────┐                 │
│  │  [icon]  Invoices     │  │  [icon]  Billing      │                │
│  │  Draft, send, track   │  │  Recurring plans      │                │
│  │  invoices             │  │  and subscriptions    │                │
│  │                       │  │                       │                │
│  │  ○ Disabled   [  ○]   │  │  ○ Disabled   [  ○]   │                │
│  │  (requires: Invoices) │  │                       │                │
│  │                       │  │                       │                │
│  │  [▼ Details]          │  │  [▼ Details]          │                │
│  └──────────────────────┘  └──────────────────────┘                 │
│                                                                     │
│  ...                                                                │
└─────────────────────────────────────────────────────────────────────┘
```

### Module Card (collapsed)

```
┌──────────────────────────────────┐
│  [icon]  [Display Name]          │
│  [Short description, 1-2 lines]  │
│                                  │
│  ● Enabled       [ toggle ]      │
│  (dependency note if blocked)    │
│                                  │
│  [▼ Details]                     │
└──────────────────────────────────┘
```

- **Toggle switch:** Enabled = green, Disabled = grey.
- **Blocked state:** If module cannot be enabled because a hard dependency is disabled, toggle is greyed-out and a note reads "Requires: [Module Name]" (links to that module card).
- **"Details" chevron:** Expands the card in-place (no navigation).

### Module Card (expanded)

```
┌────────────────────────────────────────────────┐
│  [icon]  [Display Name]                        │
│  [Full description]                            │
│                                                │
│  ● Enabled                       [ toggle ]   │
│                                                │
│  ─── Integration Status ──────────────────     │
│  ● Connected: Google Calendar                  │
│  ○ Not configured: iCal feed                   │
│                                                │
│  ─── Module Dependencies ─────────────────     │
│  This module uses:                             │
│    ● Tasks (enabled)             [Go to card]  │
│    ● Customers (enabled)         [Go card]     │
│                                                │
│  Depended on by:                               │
│    ● Time Tracking (enabled)                   │
│                                                │
│  [▲ Collapse]                                  │
└────────────────────────────────────────────────┘
```

- Integration status section is only shown for modules that have external integrations (Calendar, CRM, Invoices).
- "Go to card" scrolls and highlights the referenced module card.
- Modules with no dependencies show: "No dependencies."

### Grid Layout Spec

- Desktop (≥1024px): 3-column grid. `grid-template-columns: repeat(3, 1fr)`.
- Tablet (768–1023px): 2-column grid.
- Mobile (<768px): 1-column stack.
- Card min-height: 140px (collapsed), auto (expanded).
- Cards ordered: Tasks, Projects, Time Tracking, Calendar, Customers, CRM, Marketing, Invoices, Billing, Expenses, Contractor Payouts, Reports, Knowledge Base, AI Assistant.

---

## Disable Flow

### State Machine

```
[Enabled] ──toggle-click──► [ConfirmModal open]
                                  │
                   ┌──────────────┴───────────────┐
              [Cancel]                        [Confirm]
                  │                               │
           [Enabled]              ┌───────────────▼──────────────┐
                                  │  API PATCH tenant_module      │
                                  │  disable: [target] + cascade  │
                                  └───────────────┬──────────────┘
                                                  │
                                       ┌──────────▼──────────┐
                                       │ [Disabled]           │
                                       │ Toast: "[X] disabled"│
                                       └──────────────────────┘
```

### Confirmation Modal

Triggered on any disable toggle. Title: **"Disable [Module Name]?"**

**Section 1 — Immediate disables (hard deps, shown only when applicable):**
```
⚠ The following modules will also be disabled:

  • Time Tracking
  • Contractor Payouts

Their data is preserved but inaccessible until re-enabled.
```

**Section 2 — Degraded functionality (soft deps, always evaluated):**
```
ℹ These modules will lose some functionality:

  • Projects — Customer assignment field will be hidden
  • Marketing — Lead-to-customer conversion unavailable
```
If no soft deps affected: section 2 is omitted entirely.

**Section 3 — Data notice (always shown):**
```
All data for the disabled module(s) is preserved in your workspace
and will become accessible again if the module is re-enabled.
```

**Actions:** `[Cancel]` (secondary)  `[Disable]` (destructive red)

The `[Disable]` button label changes to `[Disable 3 Modules]` if cascade count > 1.

### After Disable

1. Module record(s) set to `enabled = false` in `tenant_modules`.
2. Sidebar nav item(s) removed immediately (driven by module state in app store).
3. Any active route within a disabled module redirects to `/` with toast: "[Module Name] has been disabled."
4. Cross-module UI elements that reference the disabled module are hidden (see Impact Rules).
5. All API routes for that module respond `403 Forbidden` with `{ error: "module_disabled", module: "tasks" }`.

---

## Enable Flow

### State Machine

```
[Disabled] ──toggle-click──► API PATCH enable ──► [Enabled]
                                                       │
                                              Toast: "[X] enabled"
                                              Nav item restored
```

No confirmation modal. Immediate. If a hard dependency is missing (e.g. attempting to enable `billing` when `invoices` is disabled), the toggle is non-interactive and shows a blocked-state badge. The user must first enable the dependency.

### Blocked Enable State

If hard deps are not met:
- Toggle is `disabled` (non-interactive, visually grey).
- Below the toggle: "Requires [Invoices] to be enabled first." — the module name is a link that scrolls to that card.
- No API call is made.

---

## Data Behavior When Disabled

| Resource | Behavior |
|---|---|
| Database rows | Fully preserved. No deletion, no archiving. |
| File attachments | Preserved in object storage. |
| API routes | All module routes return `403 { error: "module_disabled" }`. |
| Sidebar nav item | Hidden. Not rendered in sidebar component tree. |
| Route access | Any direct URL navigation to a disabled module route triggers middleware redirect to `/` + toast. |
| Cross-module fields | Fields referencing a disabled module are conditionally hidden in UI (not removed from schema). Example: project's "Customer" field hidden when `customers` disabled. |
| Global search | Entities from disabled modules excluded from CommandModal search results. |
| Reports | Report sections that require a disabled module are greyed out with "Enable [Module] to view this report." |
| Notifications | Notifications referencing disabled module content are suppressed from the bell dropdown. They remain in the DB. |

---

## Impact Rules per Dependency Relationship

| Relationship | Trigger | UI Impact | Data Impact |
|---|---|---|---|
| `time_management` hard→ `tasks` | tasks disabled | Time Tracking disabled automatically | time_entries still exist; tasks FK preserved |
| `contractor_payouts` hard→ `time_management` | time_management disabled | Contractor Payouts disabled automatically | payout_records preserved |
| `billing` hard→ `invoices` | invoices disabled | Billing disabled automatically | billing_subscriptions preserved; invoices FK preserved |
| `projects` soft→ `customers` | customers disabled | Customer field hidden on project form and project detail | project.customer_id preserved in DB |
| `projects` soft→ `tasks` | tasks disabled | Subtask panel hidden in project detail | project–task links preserved |
| `invoices` soft→ `customers` | customers disabled | Recipient autocomplete hidden; free-text name field shown instead | invoice.customer_id preserved |
| `billing` soft→ `customers` | customers disabled | Subscription customer link hidden | billing_subscription.customer_id preserved |
| `crm` soft→ `customers` | customers disabled | Customer context side-panel hidden on ticket detail | ticket.customer_id preserved |
| `marketing` soft→ `customers` | customers disabled | Lead conversion to customer button hidden | lead.converted_customer_id preserved |
| `marketing` soft→ `calendar` | calendar disabled | Booking/scheduling widget hidden | appointment records preserved |
| `marketing` soft→ `invoices` | invoices disabled | Checkout/mini-ecommerce flow hidden | order records preserved |
| `reports` soft→ `invoices` | invoices disabled | Revenue and tax report sections show "module disabled" state | no data loss |
| `reports` soft→ `expenses` | expenses disabled | Expense report sections show "module disabled" state | no data loss |
| `reports` soft→ `time_management` | time_management disabled | Utilization and billable-hours report sections show "module disabled" state | no data loss |
| `ai_assistant` soft→ all | any module disabled | AI features for that module's context removed from AI toolbar | no data loss |

---

## Default Module State for New Tenants

All 14 toggleable modules are **enabled by default** when a new tenant is created. The tenant creation API call seeds all 14 rows in `tenant_modules` with `enabled = true`.

**Onboarding configuration:** During the onboarding wizard (post-signup), step "Choose your tools" presents module cards with checkboxes. Unchecking a module during onboarding calls the standard disable API before the tenant dashboard is first loaded. This is the only time modules are presented as a checklist rather than toggle cards. The System module is never shown.

---

## Data Model

### `tenant_modules` table

```sql
CREATE TABLE tenant_modules (
  tenant_id     UUID          NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  module_id     TEXT          NOT NULL,  -- enum: see module_id values in registry
  enabled       BOOLEAN       NOT NULL DEFAULT TRUE,
  enabled_at    TIMESTAMPTZ,             -- last time it was enabled
  disabled_at   TIMESTAMPTZ,             -- last time it was disabled
  disabled_by   UUID          REFERENCES users(id) ON DELETE SET NULL,  -- who toggled it off
  override_by_system_admin BOOLEAN NOT NULL DEFAULT FALSE,  -- true = system admin forced state
  created_at    TIMESTAMPTZ   NOT NULL DEFAULT now(),
  updated_at    TIMESTAMPTZ   NOT NULL DEFAULT now(),

  PRIMARY KEY (tenant_id, module_id),
  CONSTRAINT module_id_valid CHECK (module_id IN (
    'system', 'crm', 'customers', 'time_management', 'projects', 'tasks',
    'invoices', 'expenses', 'billing', 'calendar', 'marketing', 'reports',
    'kb', 'contractor_payouts', 'ai_assistant'
  ))
);

CREATE INDEX idx_tenant_modules_tenant_id ON tenant_modules (tenant_id);
```

### Module Manifest (code-level, not DB)

The dependency graph lives in code, not in the database. This avoids runtime graph queries and keeps the authority in the application layer.

File: `packages/modules/src/manifest.ts`

```ts
export type ModuleId =
  | 'system' | 'crm' | 'customers' | 'time_management' | 'projects'
  | 'tasks' | 'invoices' | 'expenses' | 'billing' | 'calendar'
  | 'marketing' | 'reports' | 'kb' | 'contractor_payouts' | 'ai_assistant';

export type DependencyKind = 'hard' | 'soft';

export interface ModuleDependency {
  moduleId: ModuleId;
  kind: DependencyKind;
  /** Human-readable description of what breaks */
  impactDescription: string;
}

export interface ModuleDefinition {
  id: ModuleId;
  displayName: string;
  descriptionKey: string;  // i18n key
  icon: string;            // icon component name
  navGroup: 'workspace' | 'business' | 'financials' | 'resources' | 'global' | null;
  dependencies: ModuleDependency[];
  alwaysOn: boolean;       // true only for 'system'
}

export const MODULE_MANIFEST: ModuleDefinition[] = [
  { id: 'system', displayName: 'System', descriptionKey: '...', icon: '...', navGroup: null, alwaysOn: true, dependencies: [] },
  { id: 'tasks',  displayName: 'Tasks', ..., alwaysOn: false, dependencies: [] },
  {
    id: 'projects', ..., alwaysOn: false,
    dependencies: [
      { moduleId: 'customers', kind: 'soft', impactDescription: 'Customer assignment field will be hidden on projects.' },
      { moduleId: 'tasks',     kind: 'soft', impactDescription: 'Subtask panel will be hidden in project detail.' },
    ],
  },
  {
    id: 'time_management', ..., alwaysOn: false,
    dependencies: [
      { moduleId: 'tasks', kind: 'hard', impactDescription: 'Time entries cannot be created without tasks.' },
    ],
  },
  // ... (all modules follow the same shape)
];
```

### Helper functions (`packages/modules/src/dependencies.ts`)

```ts
/** Returns all modules that would be auto-disabled (hard deps, transitive) if moduleId is disabled */
export function getCascadeDisables(moduleId: ModuleId, enabledModules: ModuleId[]): ModuleId[]

/** Returns all modules that would lose functionality (soft deps) if moduleId is disabled */
export function getSoftImpacts(moduleId: ModuleId, enabledModules: ModuleId[]): Array<{ moduleId: ModuleId; impactDescription: string }>

/** Returns whether a module can be enabled given the current enabled set */
export function canEnable(moduleId: ModuleId, enabledModules: ModuleId[]): { allowed: boolean; missingHardDeps: ModuleId[] }
```

### Seeding New Tenant

On tenant creation, the auth service calls:

```sql
INSERT INTO tenant_modules (tenant_id, module_id, enabled, enabled_at)
SELECT $1, unnest(ARRAY[
  'crm','customers','time_management','projects','tasks',
  'invoices','expenses','billing','calendar','marketing',
  'reports','kb','contractor_payouts','ai_assistant'
]), true, now()
ON CONFLICT DO NOTHING;
```

The `system` module is intentionally not inserted — it is treated as always-on by application logic, never from the DB.

---

## API Endpoints

All endpoints require an authenticated session. Tenant is inferred from JWT `tid` claim.

### `GET /api/settings/modules`

Returns the full module state for the active tenant, enriched with dependency info from the manifest.

**Permission:** `settings:modules:read`

**Response `200`:**
```json
{
  "modules": [
    {
      "id": "tasks",
      "displayName": "Tasks",
      "enabled": true,
      "enabledAt": "2026-01-15T10:00:00Z",
      "disabledAt": null,
      "alwaysOn": false,
      "canDisable": true,
      "canEnable": true,
      "blockedBy": [],
      "dependencies": [
        { "moduleId": "customers", "kind": "soft", "enabled": true }
      ],
      "dependedOnBy": [
        { "moduleId": "time_management", "kind": "hard", "enabled": true },
        { "moduleId": "projects", "kind": "soft", "enabled": true }
      ]
    }
  ]
}
```

### `PATCH /api/settings/modules/:moduleId`

Enable or disable a single module. Cascade and soft-impact lists are computed server-side.

**Permission:** `settings:modules:write`

**Request body:**
```json
{ "enabled": false }
```

**Business logic (server-side):**
1. Validate `moduleId` is not `system` → 400 if so.
2. If disabling: compute cascade (hard deps) and soft impacts.
3. Disable target module + all cascade modules in a single transaction.
4. Return result.

**Response `200`:**
```json
{
  "updated": ["invoices", "billing"],
  "softImpacts": [
    { "moduleId": "marketing", "impactDescription": "Mini-ecommerce checkout unavailable." }
  ]
}
```

**Error `400` — blocked enable:**
```json
{
  "error": "missing_hard_dependencies",
  "missingDeps": ["invoices"]
}
```

**Error `403` — insufficient permission:**
```json
{ "error": "forbidden" }
```

**Error `422` — attempting to disable system module:**
```json
{ "error": "module_always_on", "module": "system" }
```

### `GET /api/settings/modules/:moduleId/impact`

Preview the impact of toggling a module without committing. Used to populate the confirmation modal client-side before the user confirms.

**Permission:** `settings:modules:read`

**Query params:** `?action=disable` (default) or `?action=enable`

**Response `200`:**
```json
{
  "action": "disable",
  "moduleId": "tasks",
  "cascadeDisables": ["time_management", "contractor_payouts"],
  "softImpacts": [
    { "moduleId": "projects", "impactDescription": "Subtask panel will be hidden in project detail." },
    { "moduleId": "ai_assistant", "impactDescription": "AI features for Tasks context will be unavailable." }
  ],
  "blockedBy": []
}
```

### System Admin Override

**`PATCH /api/admin/tenants/:tenantId/modules/:moduleId`**

Available only to system admin sessions (`type: 'admin'` in JWT).

**Request body:**
```json
{ "enabled": true, "reason": "Support override for tenant onboarding" }
```

Sets `override_by_system_admin = true` on the row. Module state set unconditionally (bypasses dependency checks). Action is logged to the audit log.

**Response `200`:**
```json
{ "updated": ["tasks"], "overrideApplied": true }
```

---

## Frontend Implementation Notes

### Module State Store

Module state is loaded once on app boot (alongside the session) and stored in a global store (Zustand or equivalent):

```ts
interface ModuleStore {
  modules: Record<ModuleId, { enabled: boolean }>;
  isEnabled: (id: ModuleId) => boolean;
  setEnabled: (id: ModuleId, enabled: boolean) => void;
  loadModules: (tenantId: string) => Promise<void>;
}
```

`isEnabled('system')` always returns `true` without a store lookup.

### Route Guard

All module routes are wrapped by a `<ModuleGuard moduleId="tasks">` component:

```tsx
function ModuleGuard({ moduleId, children }) {
  const enabled = useModuleStore(s => s.isEnabled(moduleId));
  if (!enabled) return <Navigate to="/" replace />;
  return children;
}
```

A toast is fired by the guard before redirect: "[Module Name] is not enabled for your workspace."

### Sidebar Visibility

The sidebar nav items are rendered conditionally:
```tsx
{isEnabled('projects') && <NavItem to="/projects" icon={FolderIcon} label="Projects" />}
```

The `system` module has no nav item.

### Cross-module Field Hiding

UI components that render cross-module fields check module state inline:

```tsx
{isEnabled('customers') && (
  <Field name="customer_id" label="Customer">
    <CustomerSelect />
  </Field>
)}
```

---

## Permissions

Two new permission keys are added to the RBAC seed data:

| Permission key | Description |
|---|---|
| `settings:modules:read` | View module states at `/settings/modules` |
| `settings:modules:write` | Toggle module enabled/disabled state |

Default role grants:

| Role | `settings:modules:read` | `settings:modules:write` |
|---|---|---|
| `OWNER` | ✓ | ✓ |
| `ADMIN` | ✓ | ✓ |
| `MEMBER` | — | — |
| `VIEWER` | — | — |
| `CONTRACTOR` | — | — |

---

## Architecture Decisions

| Decision | Choice | Reason |
|---|---|---|
| Dependency graph location | Code (manifest.ts), not DB | Graph structure is release-time knowledge, not runtime data. Avoids complex graph queries on hot paths. Schema migrations not required when deps change. |
| Cascade computation | Server-side at PATCH time | Single authority for business logic. Client-side preview via `/impact` endpoint is for UX only. |
| Module ID type | Enum (CHECK constraint + TS union) | Prevents silent typos from creating phantom module rows. Enum in both DB and TypeScript keeps them in sync. |
| `system` module not stored in DB | Convention: app code treats it as always-on | No DB row = can never be accidentally disabled by a bug or migration. Simplifies all "is module enabled" checks. |
| Disable action is transactional | Single DB transaction for target + cascade | Prevents partial state (e.g. `billing` disabled but `invoices` still enabled). |
| No "module tiers" in this spec | Module availability independent of billing tier | Tier-gating is handled by the entitlements layer (foundation-auth-rbac). Module enable/disable is purely a tenant workspace preference. Future: tier enforcement can wrap `PATCH /modules/:id` via existing tier middleware. |
| Data preservation on disable | Data is never deleted or archived | Safe default. Storage cost is minimal. Re-enable should be friction-free. Destructive "purge module data" is a separate, explicit, irreversible action not in this spec. |
| Onboarding module selection | Reuses standard disable API | No special onboarding endpoint. Onboarding wizard calls the same PATCH endpoint. Keeps module state management in one place. |
| Admin override flag | `override_by_system_admin` column | Distinguishes system-forced state from tenant choice. Future: show "managed by Zync support" badge in UI when flag is set. Tenant cannot toggle a system-admin-overridden module (enforced by PATCH handler). |
