Repository navigation
Ticket importers: per-source conversion modules, run inside time-boxed migration windows #8313
Replies: 1 comment 6 replies
|
Direction is right — go ahead with the step-1 spec. Migration windows as a scoped, time-boxed service-layer mode for one service principal, with the public write routes unchanged and every imported row marked with the window id, is exactly the shape I'd want. The two lessons from Zammad (a backfill is repeatable, and a module converts into the target's model) should go into the spec's opening as the design constraints. My answers to the open questions, as V1 defaults:
Two constraints for the spec: the window and import marker are partner-axis data (one principal, one partner, many orgs) — follow the partner-axis RLS shape and the cascade registration in CLAUDE.md; and the import worker needs a per-tenant concurrency cap from day one, as you note, so one migration can't starve other tenants. Status: direction approved; step-1 spec PR under |
Uh oh!
There was an error while loading. Please reload this page.
Follow-up to #7181, as agreed there: backfilling ticket history is its own feature, built on top of the Partner API ticket write surface (waves 3 and 4) rather than beside it. In practice that means the same
ticketServicefunctions and the same service-principal machinery, with no parallel write path.Problem
An MSP moving its desk to Breeze ticketing wants Breeze to be the single source of truth. That means bringing the history along, not just mirroring what happens from now on. Keeping the old system around for its history means two systems and two places to search, indefinitely.
The everyday write routes are the wrong tool for that, and on purpose: they model live work. A backfill through them:
isPublic: truenotifies);400 ASSIGNEE_NOT_ELIGIBLE, because the assignee must be an active user);Those behaviours are right for live work. A backfill needs them paused, for a bounded time and on purpose.
The real case: our Zammad
Numbers from our self-hosted Zammad, as posted on #7181:
Two lessons from it:
Proposal
Two pieces, deliberately separable.
1. Migration windows (a platform primitive)
A migration window is an explicit, time-boxed mode for one service principal: the one an import runs as (see below).
Import mode lives in the service layer, so the public write routes do not change. Whether an external tool may also open a window for its own principal can be decided later, without reworking anything.
2. Import modules (conversion interfaces), inside Breeze
One module per source: Zammad first, then Freshdesk, Zendesk, OTRS, Autotask, ConnectWise, HaloPSA, or a generic CSV/JSON format. A partner admin picks a source in the UI, connects it, reviews a dry run, and starts the import, which runs on a worker. Each module:
ticket_external_refsas the record of where each ticket came from, which is the one place foreign identity is allowed to touch the model;ticketServicefunctions the write routes use, inside a window;ticket_external_refsfor tickets; comments and attachments need the same), so a rerun skips what is already imported;The contract never learns that Zammad exists. A new source is a new module, never a new branch in
ticketService.Why inside Breeze. It gives the MSP one place to connect a source, review the mapping and watch progress, with nothing extra to install or host. That is the experience Zammad's migrators give. Writes go straight through the service layer, so import mode never becomes public API contract. Storing the source's credentials is not new territory either: Breeze already keeps third-party credentials as a matter of course (PSA, accounting, Pax8, EDR, backup providers, SSO), encrypted through
encryptedColumnRegistryand classified for tenant export. A source token is one more credential on an existing path.The costs are real, and manageable:
An external CLI or SDK stays possible later for in-house sources, because the window is a platform primitive rather than something tied to the wizard.
Attachments
The import path should require the object-storage backend. Breeze keeps attachment bytes in S3 when a bucket is configured, but without one it falls back to inline
bytea(blobStorage.selectBackend), and a 3.4 GB backfill should not land in Postgres. Our Zammad keeps every file inside its own database, and that is a habit worth not inheriting.Out of scope
Open questions
Suggested order
If the direction looks right, I'll write the design for step 1 as a spec PR under
docs/superpowers/specs/, the way we did for #7181.All reactions