# Pulse — Architecture Pulse is a single Next.js 16 app (`output: 'standalone'`) that backs Wulf Consulting's PSA workflows. It pulls data from Autotask, Datto RMM, IT Glue, MS Graph, Veeam, and ~10 other systems into Postgres, runs background workers for sync / AI analysis / RMM execution, and serves dashboards + admin tooling on port **3100**. This file is the load-bearing reference for *how the system is wired*. Per- feature deep dives live in `docs/`. UI/visual conventions live in `DESIGN.md`. ## 1. Runtime topology One Node process, one Postgres, one Redis. Background work runs **in-process** inside the Next server — there is no external job queue. ``` ┌─────────────────────────────────────────────────────────┐ │ Next.js 16 (port 3100, output: 'standalone') │ │ │ │ HTTP routes ──► API handlers ──► Postgres / Redis │ │ │ │ Side-effect imports auto-start three workers: │ │ • SyncScheduler (node-cron) │ │ • AnalyzerWorker (poll analyzer_jobs every 2s) │ │ • RmmOvershellWorker (poll rmm_executions every 5s) │ └─────────────────────────────────────────────────────────┘ │ │ │ ▼ ▼ ▼ Postgres 16 Redis 7 External APIs (state) (cache only) (Autotask, RMM, …) ``` **Workers are side-effect imports.** Touching one of these modules from a server-side import path starts the loop: | Worker | File | Trigger | |---|---|---| | Sync scheduler | `lib/services/sync-scheduler.ts` | Self-init at module bottom | | Analyzer | `lib/services/analyzer/worker.ts` | Auto-starts when `NODE_ENV=production` or `ANALYZER_WORKER_AUTOSTART=1` | | RMM Overshell | `lib/services/rmm/worker.ts` | Same gate as analyzer | Routes that need a worker running deliberately import its module — e.g. `app/api/analyzer/tickets/[ticketNumber]/analyze/route.ts` imports `lib/services/analyzer/worker.ts` purely so the loop starts on first request. **Don't eager-import these from hot paths or shared utilities.** There is **no out-of-process queue**. Scaling out to multiple Next instances means each instance runs duplicate pollers. The analyzer worker uses `SELECT … FOR UPDATE SKIP LOCKED` so jobs run exactly once across instances, but the sync scheduler does not — run it on a single dedicated instance, or gate it behind an env flag on replicas. ## 2. Data flow ### Autotask (primary system of record) - **Webhook** — `POST /api/webhooks/autotask`, public per `middleware.ts`, HMAC-verified inside the handler. Returns 200 even on processing errors so Autotask doesn't deactivate the subscription. Async handlers may enqueue analyzer jobs. - **Periodic sync** — `lib/services/entity-sync.ts` runs per scheduled task. Incremental via `lastTrackedModificationDateTime` when supported; full upsert otherwise. Targets `tickets`, `companies`, `contacts`, `resources`, `tasks`, `time_entries`, etc. ### Datto RMM - **Sync** — devices, sites, alerts → `datto_rmm_*` tables. - **Overshell executor** — `lib/services/rmm/executor.ts` validates a registered script ID, resolves the target device, enforces a per-user rate limit (50 / 24h), inserts a `pending` row in `rmm_executions`, and calls `client.runQuickJob()`. The worker polls Datto for the result and parses output via the script's `parseOutput()` method. Scripts are **code- registered** (`lib/services/rmm/scripts/`) — adding one is a TS change, not a DB change. - **LogLift** — `POST /api/rmm/loglift/upload` (public, `x-openclaw-key` header). Receives a B2 object key, downloads + decompresses the gzipped JSON (capped at 100 MB), stores a slim summary in `loglift_uploads` and the full payload in B2 (`lib/services/b2/client.ts`). Resolves the device → Autotask company → IT Glue configuration; if a unique IT Glue match is found, fires an asset-first audit automatically. ### IT Glue - **Sync** — `lib/services/itglue-sync-service.ts` pulls org types, configs, contacts, locations, flexible assets, etc. → `itg_*` tables. Note: flexible assets must be listed per type (API 422 otherwise — see commit `a0a6e7f`). - **Search (analyzer)** — `lib/services/analyzer/itglue-search.ts` returns redacted documents only. Credentials/PII pass through `redact()` before any LLM sees them. Callers must not bypass redaction; the raw client is for non- LLM use. - **Audit + write-back** — `lib/services/analyzer/asset-audit/` runs LLM-driven audits against IT Glue configurations or flexible assets, writes results to `itglue_audit_logs`, and links tickets via `itglue_ticket_xrefs`. Reverts go through `…/revert/[writeId]`. ### MS Graph (Engagement) - App-only auth, **specific tenant ID** (not `common`). Reports API returns CSV; parsed inline. Joins to Autotask hours via `resources.email = graph_users.email`. See `lib/services/engagement-sync-service.ts`. ### Other integrations Each has a factory + `isConfigured()` helper in `lib/services/`. All credentials come from env; clients throw if missing. | System | Role | Files | |---|---|---| | Veeam VSPC | Backup status, RPO, ticket analysis | `veeam-*-service.ts` | | Auvik | Network monitoring; tenant mappings | `auvik-client.ts` | | Addigy | Apple endpoints; org mappings | `addigy-factory.ts` | | Mimecast | Mail security | `mimecast-sync-service.ts` | | SentinelOne | EDR | `sentinelone-sync-service.ts` | | Duo | MFA | `duo-sync-service.ts` | | Zoom | Meetings | `zoom-sync-service.ts` | | QuickBooks | Billing reconciliation | `qbo-sync-service.ts` | | Zabbix | WAN monitoring; webhook at `/api/zabbix/webhook` | — | | Salesbldr | Sales pipeline | — | ## 3. Analyzer pipeline `lib/services/analyzer/pipeline.ts` orchestrates seven stages. Provider is chosen per request (`anthropic` default, `openrouter` opt-in); models are mapped per stage in `lib/services/llm/models.ts`. | Stage | Model (Anthropic) | Purpose | |---|---|---| | 0 — Preprocess | — | Filter workflow noise, tag entities, compute `content_hash` (idempotency key) | | 1 — Triage | Haiku | Categorize, extract entities, initial priority | | 2 — IT Glue retrieval | — | Redacted doc lookup, skipped if IT Glue not configured | | 3 — Deep analysis | Sonnet | Summary, gaps, what-was-done, what-should-have-been-done | | 4 — Deep reasoning | Opus (optional) | Apply corrections, propose IT Glue updates, re-rank | | 5 — Persist | — | Write `analyzer_analyses` row + per-stage execution rows | | 6 — Fingerprint | Haiku | Structured fingerprint for cross-ticket aggregation | **Idempotency.** If `content_hash` already exists for the ticket and `force=false`, the pipeline returns the existing analysis. The hash is provider-scoped — Claude and DeepSeek analyses of the same ticket are separate rows. **Cost ceiling.** Stage 4 is skipped if estimated total cost exceeds **$2.00**; the analysis is flagged for human review. All LLM/RMM activity is logged to `analyzer_cost_audit`. **Link-aware bundles.** `lib/services/analyzer/link-discovery.ts` resolves related tickets two ways: (1) explicit — regex T-numbers in descriptions/notes, "RELATED TICKETS:" blocks, the `problem_ticket_id` column; (2) suggested (opt-in) — Haiku ranks recent same-company tickets by semantic similarity. When a bundle is analyzed, members get `pending_analyses` rows; once all complete, an aggregate report fires. **Aggregate reports.** `stages/aggregate-reduce.ts` pairs SQL distributions (category, client, resolution path, root cause) with a Sonnet pass that identifies documentation gaps, process gaps, client patterns, recurrence clusters. Persisted to `analyzer_aggregate_reports`. **Asset audit (Phase 4).** `lib/services/analyzer/asset-audit/runner.ts` runs post-analysis. Two modes: all-time evidence across every analysis linked to the asset, or ticket-first (Phase 4.1) narrowed to a single analysis ID. ## 4. Background jobs | Worker | Cadence | Scope | Concurrency model | |---|---|---|---| | `AnalyzerWorker` | 2s poll | Claims `analyzer_jobs.status='queued'` | `FOR UPDATE SKIP LOCKED`, exactly-once across instances | | `RmmOvershellWorker` | 5s poll | Polls in-flight `rmm_executions`; advances state by querying Datto | Single in-process loop | | `SyncScheduler` | node-cron | Per-schedule rows in DB (Autotask, IT Glue, Veeam, Engagement, Zoom, Duo, …) | **Not** safe for multi-instance — overlaps possible | | `integration-health-alerts` | Cron-fired from sync scheduler | Detects stale syncs, publishes alerts | Single in-process | Stale in-flight analyzer jobs are reset on worker boot (commit `378e68a`) so a crashed pod doesn't leave jobs orphaned. ## 5. Auth & permissions **Better Auth 1.4** with magic link + TOTP 2FA + Microsoft OAuth. Sessions live in Postgres (no Redis session store). Account-linking is enabled for Microsoft so admin-invited users join their MS account in one click. **Roles.** `user`, `admin`, `super-admin`. Default admin bootstrapped from `DEFAULT_ADMIN_EMAIL` via `lib/bootstrap.ts`. **Resources** (`lib/permissions.ts`) — `tickets`, `configItems`, `admin`, `users`, `roles`, `auditLog`, `settings`, `itglue`, `rmm`. The `itglue` and `rmm` resources were added with the Overshell + IT Glue write-back work; user role gets read-only on both. **API auth pattern.** Every route handler calls one of: ```ts const { session, error } = await requireAuth(); const { session, error } = await requireAdmin(); const { session, error } = await requireSuperAdmin(); const { session, error } = await requirePermission('itglue', 'write'); if (error) return error; ``` `middleware.ts` only checks for a session cookie — role/permission checks live in the route handler. **Public routes** (hardcoded in `middleware.ts`): `/api/auth/*`, `/api/webhooks/*`, `/api/sync/*`, `/api/health`, `/api/zabbix/webhook`, `/api/rmm/loglift`, `/api/mobile/*`, `/api/openclaw/*`, `/legal`, `/api/kiosk`, `/api/qbo/*`. **Add to that list whenever you introduce a new public endpoint.** ## 6. Database 89 numbered migrations (`migrations/NNN_*.sql`), applied in **alphabetical** order on Postgres init only. Existing volumes do not re-run them — for schema changes against an existing DB, use `scripts/apply-migrations` (verify behavior first; varies by age of script). Topical groupings: | Range | Topic | |---|---| | 001–014 | Core schema, auth tables, admin settings | | 015–026 | Ticket fields, queues, RMM site mappings, integration health | | 027–032 | Datto RMM, Veeam agents/alarms, priorities, ticket categories, RMM webhooks | | 037–044 | IT Glue (large), Veeam RPO, contract services, engagement | | 045–055 | Zoom, Teams, morning summary, ping suppression, ticket digest, Zabbix WAN, QBO, Mimecast | | 056–068 | UDFs, Autotask tags, Duo, project phases, recurring revenue, Veeam ticket analysis | | 069–074 | Analyzer (jobs, analyses, stage executions, aggregate reports, cost audit, link-aware bundles, provider) | | 075–076 | IT Glue audit + ticket xrefs | | 077–078 | RMM Overshell, LogLift uploads | | 079–080 | Endpoint data model, device-xref `company_id` | **Watch out for:** - Duplicate numbers exist (002, 004, 009). Apply order is filesystem-sort alphabetical, not numeric. Don't introduce more. - Conventions: `IF NOT EXISTS` for tables/indexes, `ON CONFLICT DO NOTHING` for seed data, audit columns `created_at` / `updated_at` / `synced_at` / `is_deleted` / `deleted_at`. - `priorities` has no `is_deleted` column — caught the hard way (`9acf48e`). - Columns are **`snake_case`**; API responses are **`camelCase`**. Handlers transform manually. No ORM. DB access is the singleton at `lib/services/postgres-client.ts` — `postgresClient.query()`, `.transaction()`, `.upsert()`, `.bulkUpsert()`. ## 7. Deployment **Docker Compose** at the repo root. - `postgres` — Postgres 16, port 5432. Migrations volume mounted at `/docker-entrypoint-initdb.d/`. Init runs once per volume. - `redis` — Redis 7, port 6380 (host) / 6379 (container). Cache only. - `app` — built from `Dockerfile` (turbopack, `output: 'standalone'`); runs `node server.js`. Port 3100. `.env.local` mounted read-only. Traefik labels for `pulse.wulfconsulting.cloud` (HTTPS via Cloudflare cert). `npm run build` uses turbopack (Next 16 default). `npm run dev` for local; `npx tsc --noEmit --pretty` for type check; `npm test` (vitest) for the analyzer / RMM / B2 / link-discovery unit tests. **No CI**; type-check is the only safety net for code that doesn't have unit tests. ## 8. Invariants & gotchas 1. **Worker side-effect imports.** Importing `sync-scheduler.ts`, `analyzer/worker.ts`, or `rmm/worker.ts` from a hot path starts the loop. 2. **No external queue.** Multiple instances duplicate pollers. Analyzer is safe via row locking; sync scheduler is not — pin to one instance. 3. **IT Glue redaction is mandatory** for any LLM-bound query. Use `itglue-search.ts`, never the raw client. 4. **Provider-scoped idempotency.** `force=false` only short-circuits if the same provider produced the existing analysis. 5. **Cost ceiling at $2.00** before Stage 4. Above that, Opus is skipped and the analysis is flagged. 6. **Webhook handlers return 200 on failure** (Autotask) to avoid deactivation. Errors are logged, not surfaced. 7. **Postgres init runs migrations once.** Existing volumes won't re-run them. 8. **Duplicate migration numbers.** Apply order is alphabetic. 9. **`.env` is committed.** Treat the values as potentially real production secrets; don't log or echo them. 10. **RMM script registry is in code.** `lib/services/rmm/scripts/` — unregistered scripts can't execute. 11. **LogLift zip-bomb guard** caps inflated payloads at 100 MB. 12. **Stale analyzer jobs reset on worker boot.** Don't rely on `in_flight` state surviving restarts. ## 9. Where to look | Concern | Start here | |---|---| | HTTP routes | `app/api/**/route.ts` | | Pages | `app/**/page.tsx` | | Worker boot | `lib/services/{sync-scheduler,analyzer/worker,rmm/worker}.ts` | | Postgres access | `lib/services/postgres-client.ts` | | Auth wiring | `lib/auth.ts`, `lib/auth-utils.ts`, `lib/permissions.ts`, `middleware.ts` | | Analyzer pipeline | `lib/services/analyzer/pipeline.ts` + `stages/` | | LLM dispatch | `lib/services/llm/{call,models,pricing}.ts` | | RMM executor | `lib/services/rmm/{executor,worker,target-resolver}.ts` | | IT Glue write-back | `lib/services/analyzer/asset-audit/` | | Per-feature notes | `docs/` (one file per system) |