Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
38 KiB
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.tsxcomponent with collapsible sections - Create
IntegrationCard.tsxreusable component extracting common patterns from QBO/Xero - Refactor
SettingsPage.tsxIntegrations 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_integrationstable: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_mappingstable 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_alertstable: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_rulestable 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
assetstable: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_mappingsfor device ID mapping:integration_type:'ninjaone'alga_entity_type:'asset'alga_entity_id: Alga asset UUIDexternal_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,RmmAlertRuleNinjaOneDevice,NinjaOneOrganization,NinjaOneAlertRmmConnectionStatus,RmmSyncStatus
- Update
server/src/interfaces/asset.interfaces.tsxwith 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)
- Generate CSRF token using
- 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_integrationsrecord - 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_settingsor newrmm_settingsresource for permissions
Credential Storage
- Store credentials in tenant secrets:
- Key:
ninjaone_credentials - Value: JSON with
{ [integrationId]: { accessToken, refreshToken, expiresAt, instanceUrl } }
- Key:
- 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
QboDisconnectConfirmModalpattern - Warning about data that will be affected
- Confirmation dialog following
- 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
NinjaOneIntegrationSettingsin RMM category - Reorganize existing integrations into categories
- Implement category-based layout using
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 idrmm_device_idsystemNamenamenodeClassasset_type(mapped)organizationIdclient_id(via org mapping)offlineagent_statuslastContactlast_seen_atsystem.manufacturerattributes.manufacturersystem.modelattributes.modelos.name+os.versionos_type,os_versionprocessors[0]cpu_model,cpu_coresmemory.capacityram_gbvolumes[].capacitystorage_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.settingsJSONB - 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 AlgaNODE_UPDATED: Update existing assetNODE_DELETED: Mark asset as inactive or deleteNODE_MANUALLY_APPROVED: Activate newly approved device
- Hardware change events:
CPU_ADDED,CPU_REMOVEDMEMORY_ADDED,MEMORY_REMOVEDDISK_DRIVE_ADDED,DISK_DRIVE_REMOVED- Update extension table fields
- Status events:
SYSTEM_REBOOTED: Update last_seen, log eventUSER_LOGGED_IN,USER_LOGGED_OUT: Update last_login in extension
- Alert events (CONDITION type with TRIGGERED/RESET status):
- Create
rmm_alertsrecord - Evaluate alert rules for auto-ticket creation
- Emit
RMM_ALERT_RECEIVEDevent
- Create
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 filtersDELETE /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.settingswithwebhookRegisteredAttimestamp
Integration with Disconnect Flow
- Update
disconnectNinjaOneIntegrationaction:- Call
removeWebhook()before clearing credentials - Handle removal failures gracefully (webhook may already be removed)
- Clear webhook secret from settings
- Call
Webhook URL Configuration
- Add environment variable
NINJAONE_WEBHOOK_BASE_URLfor production URL - Generate tenant-scoped webhook URL:
- Format:
{baseUrl}/api/webhooks/ninjaone?tenant={tenantId} - Or use signed token approach for security
- Format:
- 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)
- Title:
- Update
rmm_alertswith 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
getRemoteAccessUrlin NinjaOne client:- Call
GET /device/{id}/dashboard-urlendpoint - Parse and return the remote access URL
- Handle cases where remote access is not available
- Call
- 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
- Props:
- 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.tsxto 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_softwareJSONB field - Track software changes in asset history
- Implement software inventory UI component (
AssetSoftwareInventory.tsx) - Add software search across assets (
searchSoftwareAcrossAssets,searchSoftwareaction)
Compliance Dashboard
- Create compliance summary widget for dashboard:
- Devices online/offline count
- Patches pending/failed count
- Active alerts count
NinjaOneComplianceDashboard.tsxcomponent
- Add
getRmmComplianceSummaryserver 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_mappingstable - Existing
ISecretProviderfor 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