# 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; } 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, // drives the dynamic config form } pub struct FieldSpec { pub key: String, pub label: String, pub kind: FieldKind, // Text | Secret | Select(Vec) | Url | Bool pub required: bool, pub help: Option, } pub enum HealthStatus { Ok, Degraded(String), Error(String) } ``` A static `IntegrationRegistry` holds `HashMap>`, 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