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)
- Partner-scoped. Every configuration and audit row carries
partner_id NOT NULL REFERENCES partners(id). The API derivespartner_idfrom the caller's JWT; it is never client-supplied. This mirrors the existingmspbackups_config(partner_id, UNIQUE(partner_id))pattern and enforces tenant isolation server-side (closing the same horizontal-privilege class as the knowncredentials/:id/revealfinding). - Generic JSONB config storage. A single
integration_configurationstable holds all per-partner configs in asettings JSONBcolumn. MSP360's existingmspbackups_configis migrated into this table (see Migration). No per-plugin config tables. - 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.
- Available-integration metadata lives in code, not the DB. The plugin registry (the
IntegrationPlugintrait 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 — nointegrationscatalog table to drift out of sync with shipped code. - Reuse AES-256-GCM (migration
016credentials encryption) for secret-typed fields. Secret fields withinsettingsare 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
integrationscatalog 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:
- Add the new tables (above).
- Data migration: for each
mspbackups_configrow, insert anintegration_configurationsrow withintegration_key='msp360',status='Active', andsettings= the MSP360 fields (secret fields re-encrypted via the credentials AES-256-GCM module). - Refactor the
mspbackupsmodule to read/write its config through the generic store (an accessor that serializes/deserializes the MSP360 settings shape), OR keepmspbackups_configas the system of record and have themsp360plugin adapt to it during a transition window. Recommended: dual-write + read-from-new behind a flag, verify parity, then dropmspbackups_configin a follow-up migration — never a hard cutover on a working integration. - The existing
MSPBackups.tsxpage 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_configuntil parity is confirmed in production).
API (partner-scoped; keyed by plugin key)
GET /api/integrations— catalog (registry metadata) + this partner's status per integrationGET /api/integrations/:key— detail + field spec + current (masked) config + statusPOST /api/integrations/:key/configure— validate + store (partner from JWT); auditPOST /api/integrations/:key/reconfigure— update existingPOST /api/integrations/:key/test— runhealth_checkon demandDELETE /api/integrations/:key— remove this partner's configurationGET /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 ofIntegrationCardtiles (name, category, status badge), filterable by category.pages/IntegrationDetail.tsx— dynamicConfigurationFormrendered from the plugin'sFieldSpec[], status panel, audit list, "Test connection" button.- Reuse the existing status-badge helper pattern (explicit
getStatusBadgeClass()function — not aRecordconst; see project anti-patterns).
Security
- Tenant isolation: every query filters by
partner_idfrom 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
detailsmust redact secrets. - Input validation: each plugin validates
settingsagainst itsFieldSpecbefore persist. - Audit: all config mutations logged with user + partner + action.
Rollout (combined catalog + Syncro)
- Infra: migrations (new tables), plugin trait + registry, health-check scheduler, encryption helpers.
- Plugins: MSP360 adapter (dual-write/verify) + Syncro plugin (SPEC-002 functionality behind the plugin interface).
- API: partner-scoped endpoints + RBAC + tests (incl. cross-partner isolation).
- Dashboard: Integration Center + detail/config form + status/audit.
- Cutover: behind
feature/integration-centerflag; verify MSP360 parity in prod; dropmspbackups_configin 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
syncroplugin. - SPEC-004 (MSP360) — existing; migrated into the catalog as the
msp360plugin. - partners / multi-tenancy (ADR-001) — already in place (
partnerstable,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