# SPEC-005: Integration Catalog ("Integration Center")

**Status:** Approved for build (planning locked 2026-05-29)
**Priority:** P2 — built in tandem with SPEC-002 (Syncro PSA)
**Requested By:** Mike Swanson (2026-05-18; scope locked 2026-05-29)
**Estimated Effort:** Large (catalog + Syncro plugin + MSP360 migration)

---

## Overview

The GuruRMM **Integration Center** is a centralized, **partner-scoped** surface for managing third-party integrations (Syncro PSA, MSP360 Managed Backup, and future providers). Each **partner (MSP)** browses the available catalog, configures the integrations they want for their own tenant, and monitors health/status — without per-tool fragmentation.

This is an **internal catalog**, not a public marketplace. App-store-style discovery, monetization/entitlement, and third-party-submitted integrations are explicitly **out of scope** (may be revisited as a separate "Store" initiative later).

**Audience:** Partner-level users (ADR-001 Dev -> Partner -> Client model). Partner-admins configure; partner-users have read-only status visibility. Dev/ACG (partner #1) sees all partners.

**User Benefits**
- Centralized, one-screen management of all integrations for a partner.
- One-click configure with validation; clear status (Not Configured / Configured / Active / Error).
- Per-partner health monitoring + audit trail.

---

## Locked Decisions (2026-05-29)

1. **Partner-scoped.** Every configuration and audit row carries `partner_id NOT NULL REFERENCES partners(id)`. The API derives `partner_id` from the caller's JWT; it is never client-supplied. This mirrors the existing `mspbackups_config(partner_id, UNIQUE(partner_id))` pattern and enforces tenant isolation server-side (closing the same horizontal-privilege class as the known `credentials/:id/reveal` finding).
2. **Generic JSONB config storage.** A single `integration_configurations` table holds all per-partner configs in a `settings JSONB` column. MSP360's existing `mspbackups_config` is **migrated into this table** (see Migration). No per-plugin config tables.
3. **Catalog + Syncro built together.** MSP360 is wrapped as the first plugin (its config migrated in); Syncro (SPEC-002) is implemented as the first *new* plugin through the catalog pattern, proving the full configure flow. Both ship with the catalog.
4. **Available-integration metadata lives in code**, not the DB. The plugin registry (the `IntegrationPlugin` trait implementations) is the source of truth for the catalog listing (name, provider, category, required fields). The DB stores only per-partner configurations, status, and audit — no `integrations` catalog table to drift out of sync with shipped code.
5. **Reuse AES-256-GCM** (migration `016` credentials encryption) for secret-typed fields. Secret fields within `settings` are encrypted at rest and masked on read; non-secret fields stored plaintext in the JSONB.

---

## Scope

### Included (v1)
- Integration Center UI: browse the catalog (from the code registry), configure, and monitor — partner-scoped.
- Plugins: **MSP360** (migrated from standalone) and **Syncro PSA** (new, built alongside).
- One-click configuration flow with per-plugin field validation.
- Status tracking: Not Configured / Configured / Active / Error, with last-health-check + error message.
- Per-partner audit logging of all configuration changes.
- Plugin/extension architecture (trait + registry) so new integrations are additive.

### Out of Scope
- Public/marketplace discovery, third-party-submitted integrations.
- Monetization / licensing / entitlement.
- Multiple instances of the same integration per partner (v1 = one instance per integration per partner; multi-instance deferred to Phase 2).
- Non-IT integrations.

### Success Criteria
- A partner can configure Syncro and MSP360 from one screen; status reflects real health.
- MSP360 migration is transparent — the existing MSPBackups feature keeps working, no partner reconfiguration.
- Tenant isolation verified: no partner can read or affect another partner's integration config.

---

## Architecture

### Plugin registry (code = source of truth for the catalog)

```rust
// server/src/integrations/plugin_interface.rs
#[async_trait]
pub trait IntegrationPlugin: Send + Sync {
    fn metadata(&self) -> IntegrationMetadata;                 // static catalog info
    async fn validate(&self, settings: &serde_json::Value) -> Result<()>;       // pre-save validation
    async fn configure(&self, partner_id: Uuid, settings: serde_json::Value) -> Result<()>;
    async fn health_check(&self, partner_id: Uuid) -> Result<HealthStatus>;
}

pub struct IntegrationMetadata {
    pub key: String,            // stable id, e.g. "syncro", "msp360"
    pub name: String,
    pub provider: String,
    pub category: IntegrationCategory,  // Psa, Backup, Rmm, Monitoring, ...
    pub description: String,
    pub fields: Vec<FieldSpec>, // drives the dynamic config form
}

pub struct FieldSpec {
    pub key: String,
    pub label: String,
    pub kind: FieldKind,        // Text | Secret | Select(Vec<String>) | Url | Bool
    pub required: bool,
    pub help: Option<String>,
}

pub enum HealthStatus { Ok, Degraded(String), Error(String) }
```

A static `IntegrationRegistry` holds `HashMap<String /*key*/, Arc<dyn IntegrationPlugin>>`, built at startup. `GET /api/integrations` returns registry metadata joined with the calling partner's stored config status.

### Data model (generic, partner-scoped)

```sql
-- New: per-partner integration configuration (generic JSONB)
CREATE TABLE integration_configurations (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    partner_id      UUID NOT NULL REFERENCES partners(id) ON DELETE CASCADE,
    integration_key VARCHAR(64) NOT NULL,        -- matches plugin registry key
    settings        JSONB NOT NULL DEFAULT '{}', -- secret-typed fields encrypted at rest
    status          VARCHAR(20) NOT NULL DEFAULT 'Configured'
                    CHECK (status IN ('Not Configured','Configured','Active','Error')),
    last_health_check TIMESTAMPTZ,
    error_message   TEXT,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT uk_integration_partner_key UNIQUE (partner_id, integration_key)
);
CREATE INDEX idx_integration_cfg_partner ON integration_configurations(partner_id);

-- New: per-partner audit
CREATE TABLE integration_audit_logs (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    partner_id      UUID NOT NULL REFERENCES partners(id) ON DELETE CASCADE,
    user_id         UUID REFERENCES users(id),
    integration_key VARCHAR(64) NOT NULL,
    action          VARCHAR(100) NOT NULL,       -- configured | reconfigured | enabled | disabled | health_error
    details         JSONB,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_integration_audit_partner ON integration_audit_logs(partner_id);
CREATE INDEX idx_integration_audit_created ON integration_audit_logs(created_at DESC);
```

> Note: there is intentionally **no `integrations` catalog table** — available integrations come from the code registry (Decision #4).

### MSP360 migration (live integration — handle with care)

`mspbackups_config` is live and partner-scoped already. Migration:
1. Add the new tables (above).
2. Data migration: for each `mspbackups_config` row, insert an `integration_configurations` row with `integration_key='msp360'`, `status='Active'`, and `settings` = the MSP360 fields (secret fields re-encrypted via the credentials AES-256-GCM module).
3. Refactor the `mspbackups` module to read/write its config through the generic store (an accessor that serializes/deserializes the MSP360 settings shape), OR keep `mspbackups_config` as the system of record and have the `msp360` plugin **adapt** to it during a transition window. **Recommended: dual-write + read-from-new behind a flag, verify parity, then drop `mspbackups_config`** in a follow-up migration — never a hard cutover on a working integration.
4. The existing `MSPBackups.tsx` page stays; the Integration Center adds a unified entry that deep-links to it.

> This is the one genuinely risky step. It must ship behind a feature flag with a verified rollback (keep `mspbackups_config` until parity is confirmed in production).

### API (partner-scoped; keyed by plugin key)

- `GET    /api/integrations` — catalog (registry metadata) + this partner's status per integration
- `GET    /api/integrations/:key` — detail + field spec + current (masked) config + status
- `POST   /api/integrations/:key/configure` — validate + store (partner from JWT); audit
- `POST   /api/integrations/:key/reconfigure` — update existing
- `POST   /api/integrations/:key/test` — run `health_check` on demand
- `DELETE /api/integrations/:key` — remove this partner's configuration
- `GET    /api/integrations/:key/audit` — partner-scoped audit log

RBAC: configure/reconfigure/delete require partner-admin; reads require partner membership. `partner_id` always from JWT, never the body/path.

### Health checks
A scheduled job (every ~15 min) iterates each partner's configured integrations, calls `health_check(partner_id)`, updates `status`/`last_health_check`/`error_message`, and writes an audit row on any status transition. Error transitions may raise an alert via the existing alerting subsystem.

### Dashboard
- `pages/IntegrationCenter.tsx` — grid of `IntegrationCard` tiles (name, category, status badge), filterable by category.
- `pages/IntegrationDetail.tsx` — dynamic `ConfigurationForm` rendered from the plugin's `FieldSpec[]`, status panel, audit list, "Test connection" button.
- Reuse the existing status-badge helper pattern (explicit `getStatusBadgeClass()` function — not a `Record` const; see project anti-patterns).

---

## Security
- **Tenant isolation:** every query filters by `partner_id` from JWT. Server-side enforced; covered by tests that attempt cross-partner access.
- **Secrets:** secret-typed fields encrypted at rest (AES-256-GCM, migration 016 module); masked in all API responses; never logged. Audit `details` must redact secrets.
- **Input validation:** each plugin validates `settings` against its `FieldSpec` before persist.
- **Audit:** all config mutations logged with user + partner + action.

---

## Rollout (combined catalog + Syncro)
1. **Infra:** migrations (new tables), plugin trait + registry, health-check scheduler, encryption helpers.
2. **Plugins:** MSP360 adapter (dual-write/verify) + Syncro plugin (SPEC-002 functionality behind the plugin interface).
3. **API:** partner-scoped endpoints + RBAC + tests (incl. cross-partner isolation).
4. **Dashboard:** Integration Center + detail/config form + status/audit.
5. **Cutover:** behind `feature/integration-center` flag; verify MSP360 parity in prod; drop `mspbackups_config` in a follow-up migration.

Holistic-development rule applies: backend + API + dashboard + docs ship together (DESIGN.md).

---

## Dependencies
- **SPEC-002 (Syncro PSA)** — built in tandem; its connection/config logic implemented as the `syncro` plugin.
- **SPEC-004 (MSP360)** — existing; migrated into the catalog as the `msp360` plugin.
- **partners / multi-tenancy (ADR-001)** — already in place (`partners` table, `clients.partner_id`).

## Resolved Open Questions
- Multiple instances per partner? **No in v1** (UNIQUE(partner_id, integration_key)); Phase 2.
- Config storage? **Generic JSONB** (Decision #2).
- Build order? **Catalog + Syncro together** (Decision #3).
- Non-admin visibility? **Read-only status**, yes.

---

## References
- [SPEC-002 Syncro PSA](./SPEC-002-syncro-psa-integration.md), [SPEC-004 MSP360](./SPEC-004-mspbackups-integration.md)
- ADR-001 multi-tenancy (Dev/Partner/Client); migration `016` (credentials AES-256-GCM); `034/035/044` (MSP360)
- Patterns: plugin/registry, generic JSONB config, partner-scoped tenancy
