# Architecture **Analysis Date:** 2026-05-03 ## Pattern Overview **Overall:** Single-instance Next.js 16 backend with in-process background workers (no external job queue). **Key Characteristics:** - App Router pages (`'use client'`) fetch data via API routes using `fetch()` - Three auto-starting background workers via side-effect imports (sync-scheduler, analyzer, RMM overshell) - Service layer in `lib/services/` with factories + configuration helpers - Postgres 16 as system of record (snake_case columns), Redis for caching only - Manual transformation from snake_case DB columns to camelCase API responses - Better Auth 1.4 for sessions + magic link + TOTP 2FA + Microsoft OAuth ## Layers **Route Layer (HTTP entry):** - Purpose: Accept HTTP requests, validate auth, delegate to services, return JSON responses - Location: `app/api/*/route.ts`, `app/*/page.tsx` - Contains: Next.js route handlers (GET/POST/PATCH/DELETE), page components - Depends on: Auth via `lib/auth-utils.ts`, services via `lib/services/` - Used by: Client-side fetch calls from UI components **Service Layer (business logic):** - Purpose: Sync data from external APIs, run background jobs, execute workflows, store results in Postgres - Location: `lib/services/*.ts` (~50 files) plus subdirectories (`analyzer/`, `rmm/`, `llm/`, `b2/`) - Contains: Integration clients (autotask, datto-rmm, itglue, veeam, msgraph, etc.), sync logic, job executors, pipeline orchestration - Depends on: Postgres client, external API clients, environment configuration - Used by: API routes (sync endpoints, webhook handlers) and background workers **Data Access Layer (Postgres):** - Purpose: Query, upsert, and manage state in Postgres - Location: `lib/services/postgres-client.ts` (singleton) + `migrations/NNN_*.sql` - Contains: Connection pool, query builder methods (`query()`, `transaction()`, `upsert()`, `bulkUpsert()`), migration definitions - Depends on: PostgreSQL 16 connection string from env - Used by: All services that read/write data **Background Workers (long-running processes):** - Purpose: Poll for work and execute sync/analysis/RMM tasks without blocking HTTP requests - Location: `lib/services/sync-scheduler.ts`, `lib/services/analyzer/worker.ts`, `lib/services/rmm/worker.ts` - Contains: node-cron scheduler, polling loops with exponential backoff, state machine handlers - Depends on: Postgres client, service layer integrations - Used by: Auto-start on module import (side effects); HTTP routes that need them running will import the module to start them **Component Layer (UI):** - Purpose: Render pages, dialogs, tables, charts, and handle client-side state - Location: `components/ui/` (shadcn primitives), `components/*/` (feature-specific), `app/*/page.tsx` - Contains: React components, hooks for fetch + state, sonner toasts, recharts visualizations - Depends on: API routes via `fetch()`, client auth via Better Auth SDK - Used by: Next.js pages and other components **Types & Schema (contracts):** - Purpose: Define TypeScript interfaces and database schema - Location: `lib/types/.ts`, `migrations/NNN_*.sql` - Contains: Entity types (ticket, company, analysis, rmm_execution, etc.), sync request shapes, API response envelopes - Depends on: (none — they define contracts) - Used by: Services, API routes, components ## Data Flow **Autotask Webhook → Analyzer Pipeline:** 1. **Webhook ingress** (`POST /api/webhooks/autotask`) — Public endpoint (no auth), HMAC-verified inside handler via `lib/services/webhook-service.ts` 2. **Enqueue job** — If ticket.created, fires webhook handler in `lib/services/webhook-service.ts` which inserts `analyzer_jobs` row with `status='queued'` 3. **Worker poll** — `lib/services/analyzer/worker.ts` auto-starts in production; every 2s polls for `analyzer_jobs.status='queued'`, claims one with `FOR UPDATE SKIP LOCKED` 4. **Pipeline orchestration** — `lib/services/analyzer/pipeline.ts` runs 7 stages: - Stage 0: Preprocess (filter noise, compute `content_hash` for idempotency) - Stage 1: Triage (Haiku — categorize, extract entities) - Stage 2: IT Glue retrieval (if configured, redacted docs only via `itglue-search.ts`) - Stage 3: Deep analysis (Sonnet — summary, gaps, root cause) - Stage 4: Deep reasoning (Opus — optional, skipped above $2.00 cost ceiling) - Stage 5: Persist to `analyzer_analyses` + stage execution rows - Stage 6: Fingerprint (Haiku — cross-ticket aggregation data) 5. **Link-aware bundles** — `lib/services/analyzer/link-discovery.ts` resolves related tickets; members get `pending_analyses` rows 6. **Aggregate reports** (optional) — Once all bundle members analyzed, `stages/aggregate-reduce.ts` fires **Periodic Sync Cadence:** 1. **Cron trigger** — `lib/services/sync-scheduler.ts` (node-cron singleton) reads `sync_schedules` table; fires at configured times 2. **Entity sync** — `lib/services/entity-sync.ts` per entity type (tickets, companies, resources, etc.), incremental via `lastTrackedModificationDateTime` when supported 3. **Postgres upsert** — `postgresClient.bulkUpsert()` writes batches to DB tables (`tickets`, `companies`, etc.) 4. **Integration-specific syncs** — Datto RMM devices/alerts, IT Glue configs/contacts, Veeam agents/alarms, Engagement data, Zoom, Duo, etc. **RMM Overshell Execution:** 1. **User trigger** (`POST /api/rmm/execute`) — Admin user picks registered script + device 2. **Validation & rate limit** — `lib/services/rmm/executor.ts` validates script ID, resolves device, enforces per-user limit (50 / 24h) 3. **Queue insertion** — Insert `pending` row in `rmm_executions` table, call Datto `client.runQuickJob()` 4. **Worker poll** — `lib/services/rmm/worker.ts` (5s cadence) polls in-flight executions, queries Datto for result status 5. **Output parsing** — Script's `parseOutput()` method transforms Datto output; result stored in `rmm_executions` **LogLift Evidence Ingest:** 1. **Webhook** (`POST /api/rmm/loglift/upload`) — Public, `x-openclaw-key` header auth 2. **Decompression** — Download gzipped JSON from B2, decompress, cap at 100 MB (zip-bomb guard) 3. **Storage** — Slim summary to `loglift_uploads` table, full payload to B2 via `lib/services/b2/client.ts` 4. **Auto-audit** — Resolve device → Autotask company → IT Glue config; if unique match, fire asset-first audit **IT Glue Write-back:** 1. **Audit runner** (`lib/services/analyzer/asset-audit/runner.ts`) — Post-analysis, runs LLM audit against IT Glue config/flexible asset 2. **Results** — Persisted to `itglue_audit_logs`, linked via `itglue_ticket_xrefs` 3. **Revert** — Patch `/api/analyzer/itglue/configurations/[id]/revert/[writeId]` rolls back changes **State Management:** - **HTTP requests:** Stateless; session from Better Auth cookie - **Background jobs:** State in Postgres (status columns: `queued` → `in_flight` → `complete` / `failed`) - **Caching:** Redis (optional, used for integration health checks, cache duration varies) - **Authorization:** Checked in route handlers via `requireAuth()` / `requireAdmin()` / `requirePermission()` from `lib/auth-utils.ts` ## Key Abstractions **Factory Pattern (Integration Clients):** - Purpose: Lazy-load integration clients with configured credentials; provide `isConfigured()` helper to check env vars - Examples: `lib/services/autotask-factory.ts`, `lib/services/datto-rmm-factory.ts`, `lib/services/msgraph-factory.ts` - Pattern: Export `getAutotaskClient()`, `getDattoRmmClient()`, `getMsgraphClient()` with caching; throw if credentials missing; bundled with `isConfigured()` for upstream checks - Why: Decouples client initialization from route handlers; allows conditional feature gates per env **PostgresClient Singleton:** - Purpose: Single connection pool for all Postgres queries; auto-lazy-initializes; provides ORM-like query builder - Examples: `postgresClient.query()`, `postgresClient.transaction()`, `postgresClient.upsert()`, `postgresClient.bulkUpsert()` - Pattern: Private constructor, static `getInstance()`, pool initialized on first use - Why: Prevents connection leaks; provides consistent interface across ~50 services **Sync Service (Entity-Agnostic):** - Purpose: Incremental/full sync of any entity type from external API → Postgres - Examples: `lib/services/entity-sync.ts` (Autotask), `lib/services/itglue-sync-service.ts`, `lib/services/veeam-sync-service.ts` - Pattern: Reads `lastTrackedModificationDateTime` from last_sync table; queries external API; batches upsert via `bulkUpsert()` - Why: Codifies the "last sync timestamp + incremental pull + batch insert" pattern across integrations **Analyzer Pipeline (7 Stages):** - Purpose: Orchestrate multi-stage LLM analysis with fallbacks, cost guards, and persistence - Examples: `lib/services/analyzer/pipeline.ts`, `lib/services/analyzer/stages/*.ts` - Pattern: Each stage returns structured output (gap analysis, category, cost estimate); cost guard checks ceiling before Stage 4; all stages persisted to `analyzer_stage_executions` - Why: Allows cost control (skip expensive Opus above $2.00), idempotency (content_hash), and debugging (inspect each stage's output) **Background Worker Polling Loop:** - Purpose: Auto-start in production, poll for work, claim rows with `FOR UPDATE SKIP LOCKED`, execute, persist result - Examples: `lib/services/analyzer/worker.ts`, `lib/services/rmm/worker.ts`, `lib/services/sync-scheduler.ts` - Pattern: Side-effect import auto-starts on module load; exponential backoff if no work; row locking for multi-instance safety (analyzer), or single-instance gate (sync scheduler) - Why: Keeps background work out of HTTP request path; analyzer is safe to scale (row locking); sync scheduler should run on one instance only **Better Auth Roles (RBAC):** - Purpose: Define three roles (`user`, `admin`, `super-admin`) with per-resource permissions - Examples: `lib/auth.ts` (auth config), `lib/permissions.ts` (permission matrix), `lib/auth-utils.ts` (runtime checks) - Pattern: Route handler calls `requireAdmin()` / `requirePermission()`, which decode session and check role; middleware only verifies session cookie exists - Why: Separates auth (middleware) from authorization (route handler); role-based gates are checked at the point of use ## Entry Points **HTTP Pages (Authenticated):** - Location: `app/*/page.tsx` - Triggers: Browser navigation to any route except public paths - Responsibilities: Render page shell with `AppNavigation` + `PageHeader`, fetch data from API, render client-side components, handle toast/dialog interactions **HTTP API Routes (Public & Authenticated):** - Location: `app/api/*/route.ts` - Triggers: `fetch()` from client, external webhooks (Autotask, Zabbix, RMM), scheduler HTTP calls - Responsibilities: Validate auth/webhook signature, delegate to service layer, return JSON response with appropriate status (200, 401, 403, 503, 500) **Webhook Handlers (Public):** - Location: `app/api/webhooks/autotask`, `app/api/zabbix/webhook`, `app/api/rmm/loglift` - Triggers: External systems (Autotask, Zabbix, OpenClaw) POST events - Responsibilities: Verify HMAC or custom header, parse event, enqueue jobs or upsert data, return 200 (even on error so Autotask doesn't deactivate) **Sync Endpoints (Public, called by scheduler):** - Location: `app/api/sync/*`, `app/api/datto-rmm/sync`, `app/api/itglue/sync`, `app/api/veeam/sync`, etc. - Triggers: `lib/services/sync-scheduler.ts` fires HTTP POST at configured times - Responsibilities: Call sync service, update `last_sync` timestamp, return 200 on success or error message **Background Workers (Auto-starting, in-process):** - Location: `lib/services/sync-scheduler.ts` (cron), `lib/services/analyzer/worker.ts` (2s poll), `lib/services/rmm/worker.ts` (5s poll) - Triggers: Auto-starts as side effect of module import; runs indefinitely in production - Responsibilities: Poll for work from DB, claim row, execute, persist result, handle errors + logging ## Error Handling **Strategy:** Defensive; assume external APIs can fail, return 200 on webhook failures (so Autotask doesn't deactivate), log all errors, flag analyses for human review if cost ceiling exceeded. **Patterns:** - **Webhook handlers** — Return 200 even if processing fails; log error so ops can investigate via audit log - **API route handlers** — `try/catch`, return `NextResponse.json({ error, message }, { status })` with conventions: - 401: Session missing or invalid - 403: Authenticated but lacks permission - 503: Missing/bad integration config (e.g., Autotask API key not set) - 500: Runtime error (query failed, external API timeout, etc.) - **Analyzer pipeline** — Cost ceiling guard at $2.00; above that, Stage 4 (Opus) skipped, analysis flagged with `needs_review=true` - **Sync services** — Incremental sync errors log + re-trigger on next schedule; full sync errors persist `last_error` to `sync_schedules` table - **Background workers** — Stale jobs reset on worker boot (no recovery); exponential backoff on empty polls; errors logged with job ID for manual inspection ## Cross-Cutting Concerns **Logging:** Console (stdout) in all services; elevated to syslog or Datadog in production. Analyzer logs all LLM calls + cost to `analyzer_cost_audit` table for billing reconciliation. **Validation:** Explicit checks in route handlers where it matters (e.g., device ID exists before RMM execute); no centralized validation framework. Zod used for auth/admin forms only. **Authentication:** Better Auth session stored in Postgres; cookie `Auth` + `Auth.Secure` sent on all requests. `middleware.ts` verifies session cookie exists for authenticated pages. Role checks happen in route handlers. **Authorization:** Per-resource permissions defined in `lib/permissions.ts` (tickets, configItems, admin, users, roles, auditLog, settings, itglue, rmm). API routes call `requirePermission(resource, action)` to enforce. UI hides links based on `session.user.role`. **Rate Limiting:** RMM execute endpoint has per-user limit (50 scripts / 24h via `lib/services/rate-limiter.ts`). No global rate limiter. **Integration Health:** `lib/services/integration-health.ts` polls each integration's health (e.g., Autotask token expiry, last sync age); stores status in `integration_health` table; fires alerts if integration is down or sync is stale. --- *Architecture analysis: 2026-05-03*