Files
claudetools/temp/SPEC-005-integration-catalog.md
Mike Swanson afb3a9ec6d sync: auto-sync from GURU-5070 at 2026-05-29 13:48:45
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-05-29 13:48:45
2026-05-29 13:48:51 -07:00

12 KiB

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)

// 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)

-- 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-004 MSP360
  • 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