Skip to content

docs(migration): RMM-to-Breeze migration guides and toolkit - #3250

Open
ToddHebebrand wants to merge 8 commits into
mainfrom
docs/rmm-migration-guides
Open

docs(migration): RMM-to-Breeze migration guides and toolkit#3250
ToddHebebrand wants to merge 8 commits into
mainfrom
docs/rmm-migration-guides

Conversation

@ToddHebebrand

Copy link
Copy Markdown
Collaborator

What

Adds a Migrating to Breeze docs section (apps/docs/src/content/docs/migration/) and wires it into the Starlight sidebar.

Page Contents
overview Vendor-neutral seven-phase playbook, what migrates vs regenerates vs is lost, sequencing rules, timeline
toolkit Six ready-to-run API recipes: tenancy bootstrap from CSV, bulk enrollment keys, the push payload, enrollment reconciliation, "who still runs the old agent", bulk script import
datto-rmm, ninjaone, connectwise-automate, kaseya-vsa, n-central, atera, syncro Per-vendor: hierarchy mapping, export commands, agent-push recipe, script/monitor/custom-field migration, uninstall + verification
other-rmms Pulseway, Action1, Automox, Level, Tactical RMM, ScreenConnect + a checklist for adapting the playbook to an unlisted RMM

The two ideas the guides are built on

  1. The incumbent RMM is the best deployment vehicle for the Breeze agent — it already has a SYSTEM-level, remotely-triggerable script engine on 100% of the fleet. Every vendor guide is a variation on that move, scheduled daily (not run once) so it sweeps up offline laptops.
  2. mgmtdetect already fingerprints 11 competing RMMs. That makes Breeze — not the incumbent's console — the authoritative decommission report, because it can see machines whose incumbent agent is already broken. Those are exactly the endpoints that strand and need a site visit.

Accuracy

The toolkit documents the API as it is, verified against the source rather than the existing reference docs:

  • Tenancy writes require a user JWT with MFA satisfied. X-API-Key is accepted only on the MCP server, dev-push, and device custom-field-value surfaces — authMiddleware (middleware/auth.ts:425) hard-requires Bearer. The API reference currently claims otherwise; filed as [Docs] API reference wrongly documents X-API-Key as general-purpose auth — the /devices example returns 401 #3247.
  • Enrollment keys take maxUsage up to 100,000 and ttlMinutes up to 525,600, .strict(), and ttlMinutes XOR expiresAt. The raw key is returned once, on create.
  • POST /scripts takes osTypes + language (not a shell field), and availability: "partner" — the right default for a migrated MSP toolkit.

All internal doc links verified to resolve against existing slugs. MDX scanned for unescaped JSX braces/angle brackets outside code fences — clean.

Gaps filed

Writing these surfaced where the product makes migration harder than it needs to be. Filed under epic #3249: #3242 (no bulk org/site import), #3243 (no M2M provisioning credential), #3244 (no fleet migration report from Management Posture), #3245 (no script import/export), #3246 (PSA getCompanies() implemented everywhere, called nowhere), #3247 (API-key docs bug), #3248 (no GPO/Intune guide). Each is listed in the toolkit's "Known Rough Edges" table with its documented workaround, so the docs are honest about current friction rather than quietly routing around it.

Not verified locally

The Astro build was not run — apps/docs dependencies are not installed in this environment and local Node is v20 against the repo's required v22. Link resolution and MDX-unsafe syntax were checked by script instead; the build check in CI is the real gate.

🤖 Generated with Claude Code

Adds a "Migrating to Breeze" docs section: a vendor-neutral seven-phase
playbook, a toolkit of ready-to-run API recipes, and per-vendor guides for
Datto RMM, NinjaOne, ConnectWise Automate, Kaseya VSA, N-able N-central,
Atera and Syncro, plus shorter notes for Pulseway, Action1, Automox, Level,
Tactical RMM and ScreenConnect.

The guides are built around two things the product already does well: the
incumbent RMM's own script engine is the best vehicle for deploying the
Breeze agent (it has SYSTEM-level reach on 100% of the fleet), and
mgmtdetect/Management Posture already fingerprints 11 competing RMMs, which
makes Breeze — not the incumbent's console — the authoritative source for
whether a machine is safe to decommission.

The toolkit documents the current API accurately rather than aspirationally:
tenancy writes need a user JWT with MFA satisfied (X-API-Key is accepted only
on the MCP, dev-push and device custom-field surfaces), enrollment keys take
maxUsage up to 100k and ttlMinutes up to 365 days, and POST /scripts takes
osTypes + language rather than a shell field. Gaps found while writing are
filed as #3242-#3248 under epic #3249 and listed in the toolkit's "Known
Rough Edges" table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 8, 2026

Copy link
Copy Markdown

Deploying breeze with  Cloudflare Pages  Cloudflare Pages

Latest commit: 338f4cb
Status: ✅  Deploy successful!
Preview URL: https://aef3460c.breeze-9te.pages.dev
Branch Preview URL: https://docs-rmm-migration-guides.breeze-9te.pages.dev

View logs

ToddHebebrand and others added 6 commits August 8, 2026 03:27
Design for the first phase of the RMM migration epic (#3249).

Introduces organization_external_links, a one-to-many external-system
linkage replacing the single-valued accounting_provider/accounting_external_id
pair that shipped with the QuickBooks importer. Single-valued linkage breaks
as soon as a second importer exists: an org sourced from a Datto CSV and also
linked to ConnectWise and QuickBooks can only hold one link, and the second
importer silently overwrites the first, breaking its idempotency.

partner_id is denormalised onto the link table so uniqueness can be scoped per
partner, and held honest by a composite FK to organizations(id, partner_id) --
the same mechanism used for users. The table deliberately has no jsonb column,
since any open container must be classified excludedOpen and would then be
dropped from tenant export.

Also defines the shared orgImport preview/commit seam that #3246 plugs into,
and records the deferred phases: QuickBooks migration onto the seam, the
accounting_* column drop, custom-field backfill (#3257) and first-class
contacts (#3258).

The linkage decision is marked open pending the independent advisor review,
which could not run (expired Codex auth).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Marks the spec ready for review and records why the one-to-many
organization_external_links table wins over generalising the shipped
single-valued accounting_* columns.

The deciding point is that generalising is not the cheap option it looks
like: renaming shipped columns still needs a migration, a reader sweep, and
an update to the organizations entry in CORE_TENANT_EXPORT_POLICY, since
removing or renaming a column on a registered table breaks the export-policy
test exactly as adding one does. It pays most of the cost and still buys a
one-link-per-org ceiling whose failure mode is silent duplicate-tenant
creation.

Also records the reversal-cost asymmetry that justifies the choice, and notes
that the decision was made without an independent advisor leg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…#3245)

Two more specs for the RMM migration epic (#3249).

Fleet migration report (#3244): aggregate devices.management_posture on the
fly rather than denormalising detections into a table. The jsonb is rewritten
by the agent on every scan, so a denormalised copy that misses a sync would
report a competing agent as removed while it is still installed -- which for
a decommission report is exactly the failure that strands endpoints. Notes
the LEFT JOIN LATERAL + COALESCE as load-bearing: a CROSS JOIN silently drops
never-scanned devices and makes the report read better than reality.

Script bundles (#3245): versioned JSON bundle carrying no tenancy identifiers,
with isSystem stripped unconditionally on import -- stricter than POST /scripts,
which permits it at system scope, because a bundle is untrusted input whoever
uploads it. Automations are excluded from bundles deliberately; a bundle that
could carry a schedule binding would make import arbitrary scheduled RCE
across a fleet. Loose .ps1/.sh files are converted to a bundle client-side, so
the API keeps one JSON intake path.

Both defer cleanly and neither adds a table, so no RLS/cascade/export
registration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An adversarial review of the three migration-epic specs found that each one's
flagship guarantee was contradicted by the concrete artifact in the same
document. The mechanical contracts (RLS shape, cascade lists, export policy)
were right; the end-to-end data flow was not.

#3242 -- the spec deferred migrating the QuickBooks WRITER to a later phase
while moving its READER to the link table in this PR. Every org QuickBooks
created after that would have had no link row, so the next import would not
match it and would mint a duplicate organization: precisely the silent
duplicate-tenant creation the link table exists to prevent. Now requires
dual-write plus a union read until the columns are dropped. Also records that
soft-deleted orgs keep the unique slot occupied, corrects the cascade-ordering
rationale (the delete order is computed topologically from pg_constraint, not
taken from the alphabetical list), and cites deployment_invites as the shipped
prior art for the composite FK.

#3244 -- neverScannedCount was not computable from the spec's own query. A
single GROUP BY collapsed three distinct populations (never scanned, scanned
with category absent, scanned with empty array) into one NULL-product row, so
a never-scanned device was indistinguishable from a verified-clean one. Split
into two queries: detections, and coverage denominators computed without the
lateral join. Also count(DISTINCT d.id) so a duplicated product entry cannot
double-count a device.

#3245 -- the security section missed three fields. parameters is z.any() with
no intake bound (the 64KB cap is execute-time only); an all-null
exitCodeSeverityMapping ships a script pre-configured never to alert; and
availability:'partner' rides a path with no canManagePartnerWidePolicies check
(#3262). Import now defaults to 'org' and depends on #3262 landing first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cle (#3261) specs + implementation plans

Two approved design specs plus their ready-to-implement plans:

- Proxy access (#3199): fix both 5-minute session death mechanisms (never-
  refreshed cookie + legacy tunnel_open TCP socket reaped agent-side), honest
  server-side status with lazy expiry, idempotent single-port proxy-connect
  replacing Enable Proxy Access, single entry point on /devices/network/:id
  ("Proxy to {ip}:{port} through agent"), in-place self-signed retry, locale
  fixes x7, live-target verification.
- Asset linking (#3261): hidden-and-automatic identity linking — durable
  unlink via auto_link_suppressed_at, auto-links unlinkable, modal loses all
  link controls, network device page becomes the single override surface,
  "Same device as {name}" function-first wording. States the privilege-free
  link invariant that makes MAC/IP auto-matching safe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five plans in the open work queue, ordered by dependency.

2026-08-08-scripts-partner-wide-gate (#3262) -- do first. Live authz gap:
POST /scripts gates partner-wide creation on scope alone, so a 'selected'
access partner user can push SYSTEM-level code to every org under the
partner. Includes a sweep task for sibling dual-ownership tables.

2026-08-08-org-external-links-and-bulk-import (#3242) -- new
organization_external_links table plus the shared orgImport seam. Task 4
(QuickBooks dual-write) is called out as non-deferrable: moving the reader
without the writer would mint duplicate organizations.

2026-08-08-partner-api-provisioning-writes (#3243) -- write scopes and three
POST routes on the Partner API, whose machine principal already exists. The
non-GET allowlist test is written first so its first run proves it catches
the new routes.

2026-08-08-fleet-migration-posture-report (#3244) -- two aggregate queries
over the existing management_posture jsonb, no new table. Flags up front that
one GROUP BY collapses never-scanned devices into verified-clean.

2026-08-08-script-bundle-import-export (#3245) -- blocked on #3262 for the
partner-wide path; the rest can proceed. Bounds parameters at intake (the
schema is z.any() and the 64KB cap is execute-time only) and rejects an
all-null exitCodeSeverityMapping.

Each is registered in the open/ inbox table with its status tag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
)

The per-device N+1 loop becomes one call to
GET /devices/management-posture/summary, with the never-scanned/stale
caveat spelled out and the product drill-down endpoint for device lists.
Drops the Management Posture row from Known Rough Edges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant