# 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 + `is<Name>Configured()` 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) |
