PSA/ee/docs/plans/2025-11-24-ninjaone-rmm-integration-plan.md
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

38 KiB
Raw Permalink Blame History

NinjaOne RMM Integration Plan

Overview

This plan details the implementation of a comprehensive NinjaOne RMM integration for Alga PSA, enabling MSPs to synchronize device assets, receive real-time alerts, create tickets from RMM events, and initiate remote access sessions directly from the PSA interface.

Core Features

  • Device Synchronization: Bidirectional sync of devices/assets between NinjaOne and Alga PSA
  • Webhook Callbacks: Real-time notifications when changes occur in NinjaOne
  • Asset-to-Ticket Linking: Attach assets to tickets to track which device a ticket is for
  • Alert-to-Ticket Automation: Receive alerts and automatically create tickets based on configurable rules
  • Remote Access: Launch remote sessions to devices through NinjaOne from within Alga PSA
  • Patch Compliance Tracking: Monitor and display patch status for managed devices
  • Software Inventory: Sync and display installed software from RMM-managed devices

NinjaOne API Capabilities (Reference)

Category Key Endpoints Integration Use
Devices GET /devices, GET /device/{id} Core asset sync
Alerts GET /alerts, GET /device/{id}/alerts Ticket creation
Webhooks PUT /webhook Real-time sync
Organizations GET /organizations Client mapping
Device Link GET /device/{id}/dashboard-url Remote access
Queries Health report, Software, Patches Enhanced data

Webhook Event Types: NODE_CREATED, NODE_UPDATED, NODE_DELETED, TRIGGERED (alerts), RESET, plus 200+ activity types including hardware changes, software changes, patch events, and antivirus alerts.


Asset System Readiness Assessment

Already Supported (No Changes Needed)

  • Multi-tenant architecture with RLS policies
  • Extension tables for asset types (workstation, server, network device, mobile, printer)
  • Asset-to-ticket associations (asset_ticket_associations)
  • External entity mapping table (tenant_external_entity_mappings)
  • Asset history/audit trail
  • Maintenance scheduling system

Requires Enhancement

  • Agent status tracking (online/offline, last seen)
  • RMM alert storage and lifecycle management
  • Remote access URL storage/retrieval
  • Patch compliance fields
  • Asset event types in EventBus

Integration Settings UI Redesign

As integrations grow (QBO, Xero, Google Calendar, Microsoft Calendar, Email, and now NinjaOne), the current flat list within the Integrations tab becomes unwieldy. This plan includes a reorganization.

Current Structure (SettingsPage.tsx)

Settings > Integrations Tab
├── QboIntegrationSettings (Card)
├── XeroIntegrationSettings (Card)
├── Inbound Email Integration (Card)
└── Calendar Integrations (Card)

Proposed Structure

Settings > Integrations Tab
├── Integration Categories (Accordion or Sub-tabs)
│   ├── Accounting
│   │   ├── QuickBooks Online
│   │   └── Xero
│   ├── RMM & Endpoint Management
│   │   └── NinjaOne (NEW)
│   ├── Email & Communication
│   │   ├── Inbound Email
│   │   └── (Future: Outbound SMTP)
│   └── Calendar & Scheduling
│       ├── Google Calendar
│       └── Microsoft Calendar
└── (Future: PSA-to-PSA migrations)

UI Implementation Tasks

  • Create IntegrationCategory.tsx component with collapsible sections
  • Create IntegrationCard.tsx reusable component extracting common patterns from QBO/Xero
  • Refactor SettingsPage.tsx Integrations tab to use category-based layout
  • Add category icons (accounting, RMM, email, calendar)
  • Ensure responsive layout for mobile/tablet views

Phased Implementation Plan

Phase 0 Database Schema & Foundation

Schema: RMM Integration Configuration

  • Create migration YYYYMMDDHHMMSS_create_rmm_integration_tables.cjs
  • Create rmm_integrations table:
    CREATE TABLE rmm_integrations (
      tenant UUID NOT NULL,
      integration_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      integration_type TEXT NOT NULL DEFAULT 'ninjaone',
      display_name TEXT NOT NULL,
      api_instance TEXT NOT NULL,  -- 'app' | 'eu' | 'oc' | 'ca' (region)
      client_id TEXT NOT NULL,
      is_active BOOLEAN DEFAULT true,
      sync_enabled BOOLEAN DEFAULT true,
      sync_interval_minutes INTEGER DEFAULT 60,
      last_full_sync_at TIMESTAMPTZ,
      last_incremental_sync_at TIMESTAMPTZ,
      webhook_secret TEXT,
      webhook_registered_at TIMESTAMPTZ,
      settings JSONB DEFAULT '{}',  -- Additional config options
      created_at TIMESTAMPTZ DEFAULT NOW(),
      updated_at TIMESTAMPTZ DEFAULT NOW(),
      CONSTRAINT fk_tenant FOREIGN KEY (tenant) REFERENCES tenants(tenant)
    );
    CREATE INDEX idx_rmm_integrations_tenant ON rmm_integrations(tenant);
    
  • Create rmm_organization_mappings table for NinjaOne org → Alga client mapping:
    CREATE TABLE rmm_organization_mappings (
      tenant UUID NOT NULL,
      mapping_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      integration_id UUID NOT NULL REFERENCES rmm_integrations(integration_id) ON DELETE CASCADE,
      external_org_id TEXT NOT NULL,  -- NinjaOne organization ID
      external_org_name TEXT,
      client_id UUID REFERENCES companies(company_id),  -- Alga client
      auto_sync_devices BOOLEAN DEFAULT true,
      last_synced_at TIMESTAMPTZ,
      created_at TIMESTAMPTZ DEFAULT NOW(),
      UNIQUE(tenant, integration_id, external_org_id)
    );
    
  • Create rmm_alerts table:
    CREATE TABLE rmm_alerts (
      tenant UUID NOT NULL,
      alert_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      integration_id UUID REFERENCES rmm_integrations(integration_id) ON DELETE CASCADE,
      external_alert_id TEXT NOT NULL,
      external_device_id TEXT NOT NULL,
      asset_id UUID REFERENCES assets(asset_id) ON DELETE SET NULL,
      severity TEXT NOT NULL,  -- NONE, MINOR, MODERATE, MAJOR, CRITICAL
      priority TEXT,  -- NONE, LOW, MEDIUM, HIGH
      activity_type TEXT NOT NULL,
      status_code TEXT NOT NULL,
      message TEXT,
      source_data JSONB,  -- Full webhook payload
      triggered_at TIMESTAMPTZ NOT NULL,
      acknowledged_at TIMESTAMPTZ,
      acknowledged_by UUID REFERENCES users(user_id),
      resolved_at TIMESTAMPTZ,
      resolved_by UUID REFERENCES users(user_id),
      ticket_id UUID REFERENCES tickets(ticket_id) ON DELETE SET NULL,
      auto_ticket_created BOOLEAN DEFAULT false,
      created_at TIMESTAMPTZ DEFAULT NOW(),
      UNIQUE(tenant, integration_id, external_alert_id)
    );
    CREATE INDEX idx_rmm_alerts_asset ON rmm_alerts(asset_id);
    CREATE INDEX idx_rmm_alerts_ticket ON rmm_alerts(ticket_id);
    CREATE INDEX idx_rmm_alerts_status ON rmm_alerts(tenant, status_code, triggered_at DESC);
    
  • Create rmm_alert_rules table for alert-to-ticket automation:
    CREATE TABLE rmm_alert_rules (
      tenant UUID NOT NULL,
      rule_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      integration_id UUID REFERENCES rmm_integrations(integration_id) ON DELETE CASCADE,
      name TEXT NOT NULL,
      is_active BOOLEAN DEFAULT true,
      priority_order INTEGER DEFAULT 0,
      conditions JSONB NOT NULL,  -- { severity: [...], activityTypes: [...], orgIds: [...] }
      actions JSONB NOT NULL,  -- { createTicket: true, ticketPriority: 'high', assignToChannel: '...' }
      created_at TIMESTAMPTZ DEFAULT NOW(),
      updated_at TIMESTAMPTZ DEFAULT NOW()
    );
    

Schema: Asset Table Enhancements

  • Add RMM-specific columns to assets table:
    ALTER TABLE assets ADD COLUMN IF NOT EXISTS rmm_integration_id UUID REFERENCES rmm_integrations(integration_id) ON DELETE SET NULL;
    ALTER TABLE assets ADD COLUMN IF NOT EXISTS rmm_device_id TEXT;
    ALTER TABLE assets ADD COLUMN IF NOT EXISTS agent_status TEXT DEFAULT 'unknown';  -- online, offline, unknown
    ALTER TABLE assets ADD COLUMN IF NOT EXISTS last_seen_at TIMESTAMPTZ;
    ALTER TABLE assets ADD COLUMN IF NOT EXISTS remote_access_url TEXT;
    ALTER TABLE assets ADD COLUMN IF NOT EXISTS rmm_sync_status TEXT DEFAULT 'pending';  -- synced, pending, error
    ALTER TABLE assets ADD COLUMN IF NOT EXISTS rmm_last_synced_at TIMESTAMPTZ;
    
    CREATE INDEX idx_assets_rmm_device ON assets(rmm_integration_id, rmm_device_id);
    
  • Add patch/compliance columns to workstation and server extension tables:
    ALTER TABLE workstation_assets ADD COLUMN IF NOT EXISTS antivirus_status TEXT;
    ALTER TABLE workstation_assets ADD COLUMN IF NOT EXISTS antivirus_product TEXT;
    ALTER TABLE workstation_assets ADD COLUMN IF NOT EXISTS pending_os_patches INTEGER DEFAULT 0;
    ALTER TABLE workstation_assets ADD COLUMN IF NOT EXISTS pending_software_patches INTEGER DEFAULT 0;
    ALTER TABLE workstation_assets ADD COLUMN IF NOT EXISTS failed_patches INTEGER DEFAULT 0;
    ALTER TABLE workstation_assets ADD COLUMN IF NOT EXISTS last_patch_scan_at TIMESTAMPTZ;
    
    ALTER TABLE server_assets ADD COLUMN IF NOT EXISTS antivirus_status TEXT;
    ALTER TABLE server_assets ADD COLUMN IF NOT EXISTS antivirus_product TEXT;
    ALTER TABLE server_assets ADD COLUMN IF NOT EXISTS pending_os_patches INTEGER DEFAULT 0;
    ALTER TABLE server_assets ADD COLUMN IF NOT EXISTS pending_software_patches INTEGER DEFAULT 0;
    ALTER TABLE server_assets ADD COLUMN IF NOT EXISTS failed_patches INTEGER DEFAULT 0;
    ALTER TABLE server_assets ADD COLUMN IF NOT EXISTS last_patch_scan_at TIMESTAMPTZ;
    

Schema: Update External Entity Mappings Usage

  • Document usage of existing tenant_external_entity_mappings for device ID mapping:
    • integration_type: 'ninjaone'
    • alga_entity_type: 'asset'
    • alga_entity_id: Alga asset UUID
    • external_entity_id: NinjaOne device ID (string)
    • external_realm_id: NinjaOne organization ID (for scoping)

TypeScript Interfaces

  • Create server/src/interfaces/rmm.interfaces.ts:
    • RmmIntegration, RmmOrganizationMapping, RmmAlert, RmmAlertRule
    • NinjaOneDevice, NinjaOneOrganization, NinjaOneAlert
    • RmmConnectionStatus, RmmSyncStatus
  • Update server/src/interfaces/asset.interfaces.tsx with RMM fields

Event Bus Extensions

  • Add RMM event types to server/src/lib/eventBus/events.ts:
    // RMM Integration Events
    RMM_DEVICE_SYNCED = 'rmm.device.synced',
    RMM_DEVICE_CREATED = 'rmm.device.created',
    RMM_DEVICE_UPDATED = 'rmm.device.updated',
    RMM_DEVICE_DELETED = 'rmm.device.deleted',
    RMM_ALERT_RECEIVED = 'rmm.alert.received',
    RMM_ALERT_ACKNOWLEDGED = 'rmm.alert.acknowledged',
    RMM_ALERT_RESOLVED = 'rmm.alert.resolved',
    RMM_SYNC_STARTED = 'rmm.sync.started',
    RMM_SYNC_COMPLETED = 'rmm.sync.completed',
    RMM_SYNC_FAILED = 'rmm.sync.failed',
    

Phase 1 NinjaOne API Client & OAuth

OAuth Flow Implementation

  • Create server/src/app/api/integrations/ninjaone/connect/route.ts:
    • Generate CSRF token using crypto.randomBytes(16).toString('hex')
    • Create state payload with tenant ID and CSRF token
    • Encode state as base64url
    • Determine correct NinjaOne instance URL based on region selection
    • Redirect to NinjaOne OAuth authorization endpoint
    • OAuth scopes: monitoring, management, control (as needed)
  • Create server/src/app/api/integrations/ninjaone/callback/route.ts:
    • Validate state parameter (decode, verify tenant, check CSRF)
    • Exchange authorization code for access/refresh tokens
    • Store tokens securely in tenant secrets via secretProvider.setTenantSecret()
    • Create or update rmm_integrations record
    • Redirect to settings page with success/error status params
  • Implement token refresh logic in API client

API Client

  • Create server/src/lib/integrations/ninjaone/client.ts:
    class NinjaOneClient {
      constructor(tenantId: string, integrationId: string);
    
      // Token management
      private async getAccessToken(): Promise<string>;
      private async refreshTokenIfNeeded(): Promise<void>;
    
      // API methods
      async getOrganizations(): Promise<NinjaOneOrganization[]>;
      async getDevices(params?: DeviceQueryParams): Promise<NinjaOneDevice[]>;
      async getDeviceDetails(deviceId: number): Promise<NinjaOneDeviceDetailed>;
      async getDeviceAlerts(deviceId: number): Promise<NinjaOneAlert[]>;
      async getAlerts(params?: AlertQueryParams): Promise<NinjaOneAlert[]>;
      async getDeviceLink(deviceId: number): Promise<string>;  // Remote access URL
      async resetAlert(alertId: string): Promise<void>;
    
      // Webhook management
      async configureWebhook(webhookUrl: string, activities: string[]): Promise<void>;
      async removeWebhook(): Promise<void>;
    }
    
  • Create server/src/lib/integrations/ninjaone/types.ts:
    • NinjaOne API response types
    • Device, Organization, Alert, Activity types
    • Webhook payload types
  • Create server/src/lib/integrations/ninjaone/endpoints.ts:
    • API endpoint URL builders
    • Region-specific base URLs (app, eu, oc, ca)
  • Create server/src/lib/integrations/ninjaone/errors.ts:
    • Custom error classes for API errors
    • Rate limiting handling
    • Token expiration handling

Server Actions

  • Create server/src/lib/actions/integrations/ninjaoneActions.ts:
    // Connection management
    export async function getNinjaOneConnectionStatus(): Promise<RmmConnectionStatus>;
    export async function disconnectNinjaOne(integrationId: string): Promise<{ success: boolean }>;
    export async function updateNinjaOneSettings(integrationId: string, settings: Partial<RmmIntegration>): Promise<void>;
    
    // Organization mapping
    export async function getNinjaOneOrganizations(integrationId: string): Promise<NinjaOneOrganization[]>;
    export async function getOrganizationMappings(integrationId: string): Promise<RmmOrganizationMapping[]>;
    export async function createOrganizationMapping(data: CreateOrgMappingRequest): Promise<RmmOrganizationMapping>;
    export async function updateOrganizationMapping(mappingId: string, data: UpdateOrgMappingRequest): Promise<void>;
    export async function deleteOrganizationMapping(mappingId: string): Promise<void>;
    
    // Sync operations
    export async function triggerFullSync(integrationId: string): Promise<{ jobId: string }>;
    export async function getSyncStatus(integrationId: string): Promise<RmmSyncStatus>;
    
    // Alert management
    export async function getActiveAlerts(integrationId: string, filters?: AlertFilters): Promise<RmmAlert[]>;
    export async function acknowledgeAlert(alertId: string): Promise<void>;
    export async function createTicketFromAlert(alertId: string, options?: CreateTicketOptions): Promise<{ ticketId: string }>;
    
    // Remote access
    export async function getRemoteAccessUrl(assetId: string): Promise<{ url: string; expiresAt: string }>;
    
  • Implement RBAC checks using existing hasPermission() pattern
  • Use system_settings or new rmm_settings resource for permissions

Credential Storage

  • Store credentials in tenant secrets:
    • Key: ninjaone_credentials
    • Value: JSON with { [integrationId]: { accessToken, refreshToken, expiresAt, instanceUrl } }
  • Implement credential retrieval with automatic token refresh
  • Add credential validation on connection status check

Phase 2 Integration Settings UI

Reusable Integration Components

  • Create server/src/components/settings/integrations/IntegrationCard.tsx:
    • Reusable card component with status badge, connect/disconnect buttons
    • Props: title, description, status, onConnect, onDisconnect, children
    • Extract common patterns from QboIntegrationSettings.tsx
  • Create server/src/components/settings/integrations/IntegrationCategory.tsx:
    • Collapsible category container with icon and title
    • Props: title, icon, defaultOpen, children
  • Create server/src/components/settings/integrations/ConnectionStatusBadge.tsx:
    • Unified status badge component
    • States: connected, disconnected, error, syncing, expired

NinjaOne Settings Component

  • Create server/src/components/settings/integrations/NinjaOneIntegrationSettings.tsx:
    • Connection status display with last sync time
    • Connect button with region selector (North America, EMEA, APAC, Canada)
    • Disconnect confirmation modal
    • Sync settings (interval, auto-sync toggle)
    • Link to organization mapping
    • Link to alert rules configuration
  • Create server/src/components/settings/integrations/NinjaOneDisconnectModal.tsx:
    • Confirmation dialog following QboDisconnectConfirmModal pattern
    • Warning about data that will be affected
  • Create server/src/components/settings/integrations/NinjaOneRegionSelector.tsx:
    • Dropdown for selecting NinjaOne instance region
    • Display region-specific information

Organization Mapping UI

  • Create server/src/components/settings/integrations/ninjaone/OrganizationMappingManager.tsx:
    • List of NinjaOne organizations with mapping status
    • Dropdown to select corresponding Alga client for each org
    • Auto-sync toggle per organization
    • Bulk mapping actions
  • Create server/src/components/settings/integrations/ninjaone/OrganizationMappingRow.tsx:
    • Individual row component for org mapping
    • Status indicators (mapped, unmapped, sync error)

Alert Rules UI

  • Create server/src/components/settings/integrations/ninjaone/AlertRulesManager.tsx:
    • List of configured alert-to-ticket rules
    • Add/Edit/Delete rule actions
    • Rule priority ordering (drag-and-drop)
  • Create server/src/components/settings/integrations/ninjaone/AlertRuleForm.tsx:
    • Rule name and active toggle
    • Condition builder:
      • Severity filter (multi-select)
      • Activity type filter (multi-select with search)
      • Organization filter (optional)
    • Action configuration:
      • Create ticket toggle
      • Ticket priority mapping
      • Channel/team assignment
      • Notification settings

Settings Page Integration

  • Refactor server/src/components/settings/general/SettingsPage.tsx:
    • Implement category-based layout using IntegrationCategory
    • Add "RMM & Endpoint Management" category
    • Include NinjaOneIntegrationSettings in RMM category
    • Reorganize existing integrations into categories

Phase 3 Device Synchronization

Device Mapper

  • Create server/src/lib/integrations/ninjaone/mappers/deviceMapper.ts:
    export function mapNinjaOneDeviceToAsset(
      device: NinjaOneDeviceDetailed,
      existingAsset?: Asset
    ): CreateAssetRequest | UpdateAssetRequest;
    
    export function determineAssetType(device: NinjaOneDevice): AssetType;
    // nodeClass mapping: WINDOWS_WORKSTATION → workstation, WINDOWS_SERVER → server, etc.
    
    export function mapDeviceHardware(device: NinjaOneDeviceDetailed): WorkstationExtension | ServerExtension;
    
    export function mapNetworkInterfaces(interfaces: NinjaOneNetworkInterface[]): NetworkInterfaceData[];
    
  • Field mapping reference:
    NinjaOne Field Alga Asset Field
    id rmm_device_id
    systemName name
    nodeClass asset_type (mapped)
    organizationId client_id (via org mapping)
    offline agent_status
    lastContact last_seen_at
    system.manufacturer attributes.manufacturer
    system.model attributes.model
    os.name + os.version os_type, os_version
    processors[0] cpu_model, cpu_cores
    memory.capacity ram_gb
    volumes[].capacity storage_capacity (sum)

Sync Engine

  • Create server/src/lib/integrations/ninjaone/sync/syncEngine.ts:
    export class NinjaOneSyncEngine {
      constructor(tenantId: string, integrationId: string);
    
      async runFullSync(): Promise<SyncResult>;
      async runIncrementalSync(since: Date): Promise<SyncResult>;
      async syncDevice(deviceId: number): Promise<Asset>;
      async syncOrganization(orgId: number): Promise<SyncResult>;
    
      private async createAsset(device: NinjaOneDevice): Promise<Asset>;
      private async updateAsset(asset: Asset, device: NinjaOneDevice): Promise<Asset>;
      private async handleDeletedDevice(deviceId: number): Promise<void>;
    }
    
  • Create server/src/lib/integrations/ninjaone/sync/syncJob.ts:
    • Background job for scheduled sync
    • Progress tracking and reporting
    • Error handling and retry logic
    • Emit events for sync lifecycle
  • Implement conflict resolution:
    • Last-write-wins for most fields
    • Preserve manual Alga edits for certain fields (notes, custom attributes)
    • Log conflicts for review

Initial Sync Flow

  • Create sync initiation endpoint/action
  • Implement pagination for large device sets (NinjaOne uses cursor-based pagination)
  • Track sync progress in rmm_integrations.settings JSONB
  • Create sync history/log table or use existing job tracking

Sync Scheduling

  • Implement configurable sync interval (default: 60 minutes)
  • Create cron job or Temporal workflow for scheduled sync
  • Add manual "Sync Now" button in UI
  • Implement sync locking to prevent concurrent syncs

Phase 4 Webhook Handler & Real-Time Sync

Webhook Endpoint

  • Create server/src/app/api/webhooks/ninjaone/route.ts:
    export async function POST(request: Request) {
      // 1. Verify webhook signature (HMAC)
      // 2. Parse webhook payload
      // 3. Identify tenant from integration lookup
      // 4. Route to appropriate handler based on activityType
      // 5. Return 200 quickly, process async
    }
    
  • Implement webhook signature verification:
    • NinjaOne sends signature in header
    • Verify using stored webhook secret
  • Create server/src/lib/integrations/ninjaone/webhooks/webhookHandler.ts:
    export async function handleNinjaOneWebhook(payload: NinjaOneWebhookPayload): Promise<void>;
    
    export async function handleDeviceEvent(payload: DeviceActivityPayload): Promise<void>;
    export async function handleAlertEvent(payload: AlertActivityPayload): Promise<void>;
    export async function handleSystemEvent(payload: SystemActivityPayload): Promise<void>;
    

Webhook Event Handlers

  • Device lifecycle events:
    • NODE_CREATED: Create new asset in Alga
    • NODE_UPDATED: Update existing asset
    • NODE_DELETED: Mark asset as inactive or delete
    • NODE_MANUALLY_APPROVED: Activate newly approved device
  • Hardware change events:
    • CPU_ADDED, CPU_REMOVED
    • MEMORY_ADDED, MEMORY_REMOVED
    • DISK_DRIVE_ADDED, DISK_DRIVE_REMOVED
    • Update extension table fields
  • Status events:
    • SYSTEM_REBOOTED: Update last_seen, log event
    • USER_LOGGED_IN, USER_LOGGED_OUT: Update last_login in extension
  • Alert events (CONDITION type with TRIGGERED/RESET status):
    • Create rmm_alerts record
    • Evaluate alert rules for auto-ticket creation
    • Emit RMM_ALERT_RECEIVED event

Webhook Registration

  • Add webhook configuration to connection flow
  • Create webhook URL with tenant-specific path or token
  • Register webhook with NinjaOne API on connection
  • Remove webhook on disconnect
  • Handle webhook secret rotation

Async Processing

  • Queue webhook payloads for async processing (avoid timeout)
  • Implement idempotency using external_alert_id/activity id
  • Add retry logic for transient failures
  • Create webhook processing log for debugging

Phase 4.5 Webhook Auto-Registration

NinjaOne supports programmatic webhook configuration via the API. This allows us to automatically register the Alga webhook endpoint after OAuth authentication, eliminating manual configuration in the NinjaOne UI.

API Reference

  • PUT /v2/webhook - Configure webhook endpoint and activity filters
  • DELETE /v2/webhook - Remove webhook configuration

Webhook Registration Service

  • Create ee/server/src/lib/integrations/ninjaone/webhooks/webhookRegistration.ts:
    export interface WebhookConfig {
      url: string;
      activities: {
        statusCode?: string[];  // Filter by status codes
        activityType?: string[];  // Filter by activity types
      };
      expand?: string[];  // References to expand in payloads (device, organization)
      headers?: Array<{ name: string; value: string }>;  // Custom auth headers
      organizationIds?: number[];  // Optional org filter
    }
    
    export async function registerWebhook(
      client: NinjaOneClient,
      config: WebhookConfig
    ): Promise<void>;
    
    export async function removeWebhook(
      client: NinjaOneClient
    ): Promise<void>;
    
    export function getDefaultWebhookConfig(
      baseUrl: string,
      webhookSecret: string
    ): WebhookConfig;
    

Default Activity Subscriptions

Configure webhook to receive the most useful events:

  • Device Lifecycle: NODE_CREATED, NODE_UPDATED, NODE_DELETED, NODE_MANUALLY_APPROVED, NODE_AUTOMATICALLY_APPROVED
  • Alerts/Conditions: TRIGGERED, RESET, ACKNOWLEDGED
  • System Events: SYSTEM_REBOOTED, USER_LOGGED_IN, USER_LOGGED_OUT
  • Hardware Changes: CPU_ADDED, CPU_REMOVED, MEMORY_ADDED, MEMORY_REMOVED, DISK_DRIVE_ADDED, DISK_DRIVE_REMOVED, ADAPTER_ADDED, ADAPTER_REMOVED
  • Patch Management: PATCH_MANAGEMENT_SCAN_STARTED, PATCH_MANAGEMENT_SCAN_COMPLETED, PATCH_MANAGEMENT_INSTALLED, PATCH_MANAGEMENT_INSTALL_FAILED
  • Software Changes: SOFTWARE_ADDED, SOFTWARE_REMOVED
  • Antivirus: Severity-based alerts from supported AV products

Integration with OAuth Flow

  • Update ee/server/src/app/api/integrations/ninjaone/callback/route.ts:
    • After successful token exchange and storage
    • Generate webhook secret using crypto.randomBytes(32).toString('hex')
    • Store webhook secret in rmm_integrations settings
    • Call registerWebhook() with tenant-specific callback URL
    • Handle registration failures gracefully (log, retry later)
    • Update rmm_integrations.settings with webhookRegisteredAt timestamp

Integration with Disconnect Flow

  • Update disconnectNinjaOneIntegration action:
    • Call removeWebhook() before clearing credentials
    • Handle removal failures gracefully (webhook may already be removed)
    • Clear webhook secret from settings

Webhook URL Configuration

  • Add environment variable NINJAONE_WEBHOOK_BASE_URL for production URL
  • Generate tenant-scoped webhook URL:
    • Format: {baseUrl}/api/webhooks/ninjaone?tenant={tenantId}
    • Or use signed token approach for security
  • Update webhook handler to validate custom auth header

Error Handling & Retry

  • Handle rate limiting (429 responses)
  • Implement retry with exponential backoff
  • Log registration failures for admin visibility
  • Add "Re-register Webhook" button in settings UI for manual recovery

Testing

  • Unit test webhook config generation
  • Integration test webhook registration flow
  • Test disconnect cleans up webhook

Phase 5 Alert Integration & Ticket Creation

Alert Processing

  • Create server/src/lib/integrations/ninjaone/alerts/alertProcessor.ts:
    export async function processAlert(
      tenantId: string,
      integrationId: string,
      alertPayload: NinjaOneAlertPayload
    ): Promise<RmmAlert>;
    
    export async function evaluateAlertRules(
      tenantId: string,
      alert: RmmAlert
    ): Promise<AlertRuleMatch | null>;
    
    export async function executeAlertActions(
      alert: RmmAlert,
      rule: RmmAlertRule
    ): Promise<void>;
    

Ticket Creation from Alerts

  • Create server/src/lib/integrations/ninjaone/alerts/ticketCreator.ts:
    export async function createTicketFromAlert(
      alert: RmmAlert,
      options: CreateTicketFromAlertOptions
    ): Promise<Ticket>;
    
  • Ticket content generation:
    • Title: [NinjaOne Alert] {activityType} on {deviceName}
    • Description: Alert details, device info, severity
    • Priority mapping: CRITICAL → urgent, MAJOR → high, MODERATE → medium, MINOR → low
    • Auto-link to asset via asset_ticket_associations
    • Include device context (client, location, IP, last user)
  • Update rmm_alerts with ticket reference

Alert Management UI

  • Create server/src/components/alerts/RmmAlertsPanel.tsx:
    • List of active RMM alerts
    • Filter by severity, status, device
    • Acknowledge/resolve actions
    • Create ticket action
  • Add alerts indicator to asset detail drawer
  • Create alert detail modal/drawer

Alert Acknowledgment & Resolution

  • Implement acknowledge action (updates local record)
  • Implement resolve action (optionally resets in NinjaOne via API)
  • Track who acknowledged/resolved
  • Sync acknowledgment state bidirectionally (optional)

Phase 6 Remote Access Integration

Remote Access URL Retrieval

  • Implement getRemoteAccessUrl in NinjaOne client:
    • Call GET /device/{id}/dashboard-url endpoint
    • Parse and return the remote access URL
    • Handle cases where remote access is not available
  • Cache URLs with short TTL (5-10 minutes)
  • Handle URL expiration gracefully

Remote Access UI

  • Add "Remote Connect" button to AssetDetailDrawer.tsx:
    • Only show for RMM-managed assets
    • Show loading state while fetching URL
    • Open URL in new tab/window
    • Handle errors (device offline, no remote access configured)
  • Create server/src/components/assets/RemoteAccessButton.tsx:
    • Props: assetId, disabled, variant
    • Handle click to fetch and open URL
    • Show tooltip with device status
  • Add remote access to asset actions menu
  • Log remote access attempts for audit trail

Remote Access from Ticket Context

  • Add remote access button to ticket detail when asset is linked
  • Show linked asset's remote access status
  • Quick action in ticket actions menu

Phase 7 Enhanced Asset Display

Asset Card Enhancements

  • Update AssetCard.tsx to show RMM status:
    • Agent status indicator (online/offline badge)
    • Last seen timestamp
    • Sync status indicator
    • Patch compliance badge (if applicable)
  • Add RMM source indicator for synced assets
  • Show alert count badge when active alerts exist

Asset Detail Drawer Enhancements

  • Add "RMM Status" section to detail drawer:
    • Agent status with last contact time
    • Sync status and last sync time
    • Link to NinjaOne dashboard
    • Remote access button
  • Add "Active Alerts" section:
    • List of unresolved alerts for this device
    • Quick actions (acknowledge, create ticket)
  • Add "Patch Status" section:
    • Pending patches count
    • Failed patches count
    • Last scan time
    • Link to detailed patch report
  • Add "Software Inventory" section:
    • List of installed software from RMM
    • Version information
    • Search functionality

Asset List Filters

  • Add RMM-related filters to asset list:
    • Agent status (online, offline, unknown)
    • RMM managed (yes, no)
    • Has active alerts (future)
    • Patch compliance status (future)
  • Add bulk actions for RMM assets:
    • Trigger sync for selected
    • View in NinjaOne (bulk open)

Phase 8 Patch & Software Inventory Sync

Patch Status Sync

  • Create ee/server/src/lib/integrations/ninjaone/sync/patchSync.ts:
    • Fetch pending/failed patches from NinjaOne
    • Update extension table fields
    • Track patch scan timestamps
  • Add server actions for patch sync (triggerPatchStatusSync)
  • Add patch status to device sync flow (auto-sync during device sync)
  • Create scheduled patch status refresh (less frequent than device sync)

Software Inventory Sync

  • Create ee/server/src/lib/integrations/ninjaone/sync/softwareSync.ts:
    • Fetch installed software list
    • Store in extension table installed_software JSONB field
    • Track software changes in asset history
  • Implement software inventory UI component (AssetSoftwareInventory.tsx)
  • Add software search across assets (searchSoftwareAcrossAssets, searchSoftware action)

Compliance Dashboard

  • Create compliance summary widget for dashboard:
    • Devices online/offline count
    • Patches pending/failed count
    • Active alerts count
    • NinjaOneComplianceDashboard.tsx component
  • Add getRmmComplianceSummary server action
  • Add compliance reporting (future)

Asset List RMM Filters

  • Add agent status filter (Online, Offline, Unknown)
  • Add RMM managed filter (Managed, Not Managed)
  • Display RMM filter pills in active filters bar

Phase 9 Testing & Documentation

Unit Tests

  • Test NinjaOne API client methods
  • Test device mapper transformations
  • Test webhook signature verification
  • Test alert rule evaluation
  • Test ticket creation from alerts

Integration Tests

  • Test OAuth flow (mock NinjaOne OAuth server)
  • Test webhook endpoint with sample payloads
  • Test full sync workflow
  • Test alert-to-ticket flow

E2E Tests

  • Test connection flow in UI
  • Test organization mapping UI
  • Test alert rules configuration
  • Test remote access button functionality

Documentation

  • Create user documentation for NinjaOne setup
  • Document organization mapping best practices
  • Document alert rule configuration
  • Create troubleshooting guide
  • Add API documentation for webhook endpoint

File Structure Summary

server/src/
├── app/
│   └── api/
│       ├── integrations/
│       │   └── ninjaone/
│       │       ├── connect/route.ts
│       │       └── callback/route.ts
│       └── webhooks/
│           └── ninjaone/
│               └── route.ts
├── components/
│   └── settings/
│       └── integrations/
│           ├── IntegrationCard.tsx
│           ├── IntegrationCategory.tsx
│           ├── ConnectionStatusBadge.tsx
│           ├── NinjaOneIntegrationSettings.tsx
│           ├── NinjaOneDisconnectModal.tsx
│           ├── NinjaOneRegionSelector.tsx
│           └── ninjaone/
│               ├── OrganizationMappingManager.tsx
│               ├── OrganizationMappingRow.tsx
│               ├── AlertRulesManager.tsx
│               └── AlertRuleForm.tsx
├── lib/
│   ├── actions/
│   │   └── integrations/
│   │       └── ninjaoneActions.ts
│   └── integrations/
│       └── ninjaone/
│           ├── client.ts
│           ├── types.ts
│           ├── endpoints.ts
│           ├── errors.ts
│           ├── mappers/
│           │   └── deviceMapper.ts
│           ├── sync/
│           │   ├── syncEngine.ts
│           │   ├── syncJob.ts
│           │   ├── patchSync.ts
│           │   └── softwareSync.ts
│           ├── webhooks/
│           │   └── webhookHandler.ts
│           └── alerts/
│               ├── alertProcessor.ts
│               └── ticketCreator.ts
└── interfaces/
    └── rmm.interfaces.ts

server/migrations/
└── YYYYMMDDHHMMSS_create_rmm_integration_tables.cjs

Dependencies & Prerequisites

External

  • NinjaOne API application registration (OAuth client ID/secret)
  • Webhook endpoint accessible from NinjaOne servers (public URL or tunnel for dev)
  • NinjaOne account with API access enabled

Internal

  • Existing tenant_external_entity_mappings table
  • Existing ISecretProvider for credential storage
  • Existing asset management system
  • Existing ticket system
  • EventBus for event publishing

Acceptance Criteria

Phase 1-2 (Connection & UI)

  • User can connect NinjaOne account via OAuth from settings
  • Connection status displays correctly (connected, disconnected, error)
  • User can disconnect NinjaOne integration
  • Settings page shows integrations in organized categories

Phase 3 (Device Sync)

  • Initial sync imports all devices from mapped organizations
  • Devices are created with correct asset type based on nodeClass
  • Hardware details (CPU, RAM, storage) are populated in extension tables
  • Scheduled sync runs at configured interval
  • Manual "Sync Now" works correctly

Phase 4 (Webhooks)

  • Webhook endpoint receives and validates NinjaOne callbacks
  • Device changes in NinjaOne reflect in Alga within seconds
  • New devices are created automatically
  • Deleted devices are handled appropriately

Phase 5 (Alerts & Tickets)

  • Alerts from NinjaOne are stored in rmm_alerts table
  • Alert rules can be configured via UI
  • Tickets are auto-created based on matching rules
  • Tickets include device context and are linked to asset

Phase 6 (Remote Access)

  • "Remote Connect" button appears on RMM-synced assets
  • Clicking button opens NinjaOne remote session in new tab
  • Remote access works from asset detail and ticket context

Phase 7-8 (Enhanced Display & Compliance)

  • Asset cards show agent status and alert indicators
  • Asset detail shows comprehensive RMM status
  • Patch compliance status is displayed (AssetPatchStatusSection)
  • Software inventory is viewable (AssetSoftwareInventory)
  • Compliance dashboard widget shows fleet health
  • Asset list supports RMM filters (agent status, managed/unmanaged)

Future Considerations

Additional RMM Platforms

This architecture is designed to support multiple RMM platforms. Future integrations could include:

  • Datto RMM
  • ConnectWise Automate
  • Syncro
  • Atera

The rmm_integrations table and integration_type field support this extensibility.

Bidirectional Sync Enhancements

  • Push Alga asset changes to NinjaOne custom fields
  • Sync maintenance windows bidirectionally
  • Push ticket status updates to NinjaOne

Advanced Features

  • Script execution from Alga PSA
  • Patch deployment initiation
  • Software deployment
  • Device group management