Sync Pipeline
The sync pipeline transforms compiled PCD state into Arr API operations. Preview
(dry-run) and execution are separate code paths. Section support is declared per
arr_type in mappings.ts — Radarr, Sonarr, and Lidarr do not share identical
semantics even when API shapes look similar.
User workflows: Syncing Profiles. This page covers internals.
Section Registry
Section titled “Section Registry”Sync work is organized into sections registered in sync/registry.ts:
| Section | Handler import |
|---|---|
qualityProfiles |
qualityProfiles/handler.ts |
delayProfiles |
delayProfiles/handler.ts |
mediaManagement |
mediaManagement/handler.ts |
metadataProfiles |
metadataProfiles/handler.ts |
Each section implements SectionHandler with syncers extending BaseSyncer. Handlers
register via side-effect imports in sync/processor.ts.
Per-arr_type Dispatch
Section titled “Per-arr_type Dispatch”sync/mappings.ts defines supported sections per app:
| Arr type | Supported sections |
|---|---|
radarr |
qualityProfiles, delayProfiles, mediaManagement |
sonarr |
qualityProfiles, delayProfiles, mediaManagement |
lidarr |
all four including metadataProfiles |
isSyncSectionSupported() and related helpers fail fast with explicit error messages
when a section is requested for an unsupported arr_type. Media management subsections
(mediaSettings, naming, qualityDefinitions) are also gated per app.
Preview Path (Read-Only)
Section titled “Preview Path (Read-Only)”Preview never mutates Arr state:
- API:
POST /api/v1/sync/preview(see OpenAPI reference) processor.ts→generatePreview()inpreview/orchestrator.ts- Orchestrator walks
SYNC_SECTION_ORDER, invokes section syncers in preview mode - Returns per-section creates/updates/deletes and accumulated errors
Preview is safe to repeat while resolving PCD conflicts or selection mismatches.
Reviewed Preview Apply Boundary
Section titled “Reviewed Preview Apply Boundary”Creating a Sync Preview also creates a private, versioned review binding in the process-local TTL preview store. The binding is not returned by the preview API and is not written to the application database. A process restart or normal preview expiry therefore invalidates it safely rather than restoring an old authorization.
The binding covers the exact reviewed:
- instance ID and explicit
arr_type - successfully generated section set and the subset selected for Apply
- effective per-section configuration, including transient preview configuration
- desired PCD/config evidence, relevant live Arr evidence, and material plan for each section
Apply first atomically claims the ready preview (ready → applying). The reviewed
executor then claims every selected section as one operation without resetting another
sync’s active claim. After the claims are held, it reloads the exact enabled instance,
dispatches through its stored Radarr, Sonarr, or Lidarr type, and re-materializes separate
PCD/config, live Arr, and material-plan evidence for every selected section. Every
selected section must match before Praxrr creates snapshots, captures Sync History,
records confirmed outcomes, or performs the first Arr write.
A PCD-only, Arr-only, combined, scope, or unverifiable mismatch returns a typed 422 invalidation. The entire selected apply is rejected with zero writes, no Sync History run, and no confirmed entity outcomes. The old diff remains read-only; the UI states that nothing was applied and requires the operator to generate and review a new preview.
On a match, each syncer receives the prepared values produced during revalidation and the same effective section configuration that was reviewed. The planned changes remain review evidence, not proof of execution. Only actual writer results become confirmed entity outcomes and Sync History evidence.
This is an all-selected-before-any-write application boundary, not a transaction across upstream Arr APIs. Radarr, Sonarr, and Lidarr do not expose one common conditional-write contract, so an external actor can still change Arr state after Praxrr’s final validation read and before a write. Praxrr narrows that interval with prepared values and available value guards, but does not claim durable preview authorization or upstream atomicity.
Execution Path
Section titled “Execution Path”Execution runs through job handlers and direct processor calls:
| Trigger | Entry point |
|---|---|
on_pull |
triggerSyncs() after PCD git pull |
on_change |
triggerSyncs() after PCD file/ops change |
schedule |
Cron evaluation in evaluateScheduledSyncs() → arr.sync.* jobs |
| Manual | UI or API enqueue of sync jobs |
arr.sync.* job handlers invoke section syncers with live Arr clients from
arrInstanceClients.ts. Concurrency for multi-instance preview is bounded; execution
uses similar batching in processor.ts.
Startup pull marks instances active to prevent redundant on_pull fanout while
reconstructing selections from live Arr state.
Pipeline Flow
Section titled “Pipeline Flow”flowchart LR PCD["Compiled PCD cache"] Preview["preview/orchestrator"] Jobs["arr.sync.* jobs"] Arr["Arr HTTP APIs"] PCD --> Preview PCD --> Jobs Preview -. read-only .-> Arr Jobs --> Arr
In prose: both preview and execution read compiled PCD state. Preview diffs against Arr without writes. Reviewed Preview Apply adds the private validation boundary above before calling the existing writers. Jobs and matched reviewed applies perform the actual push.
Ownership Ledger and Pruning
Section titled “Ownership Ledger and Pruning”The arr_managed_entities table is a per-instance ownership ledger. Only a
confirmed create outcome for a custom format or quality profile inserts a row;
a confirmed update refreshes last_synced_at/arr_name on an existing row but
never adopts a same-named foreign entity (sync/ownership/record.ts). There is no
backfill: entities that predate the ledger stay unmanaged and are reachable only
through cleanup with includeUnmanaged.
When an instance has prune_removed_managed = 1, the quality-profiles section plans
deletions for ledger-owned entities that are no longer in the expected configuration
(sync/ownership/planPrunes.ts):
- A ledger row becomes a prune candidate only when the live entity with the same
remote id still carries the exact recorded
arr_name(guards against id reuse). - Quality profiles still assigned to media are blocked and listed as Kept (in use). That media lookup runs only while building a reviewed preview or re-materializing it for apply. Drift checks, sync-history capture, MCP previews, and canary previews do not download the library; those quality profiles stay blocked as unverified and are not deleted.
- A custom format scored by any surviving live quality profile, or by any desired profile, is kept. It is not listed under Kept (in use). A blocked or skipped quality-profile delete also keeps the formats that profile scores.
- If a selected PCD profile is missing from its source, that database keeps its namespace so a later sync does not collide with another source.
- Prunes appear in the preview as
action: 'delete', managed: trueand are hashed into the review binding’splanHash, so they exist only inside the reviewed preview → apply path. Scheduled,on_pull,on_change, and canary syncs never delete.
On a matched apply, sync/ownership/executePrunes.ts deletes quality profiles
before custom formats. Each item is re-read with a fresh GET immediately before
DELETE. A name that no longer matches is skipped (Arr entity changed since review; not deleted) and the ledger row is kept. HTTP 404 counts as success and drops
the row. Any other failure keeps the row. If an earlier write in the section
failed, every reviewed delete is skipped with an explicit outcome and the section
fails. When sync history is disabled, the deletes still run and a warning is
logged because they will have no history record. Applying sections that contain
managed deletes requires the admin scope for API tokens.
Confirmed prune deletes flow into Sync History like other outcomes, and planned
managed deletes appear in drift under the prune category. That includes ledger-owned
quality profiles listed in the preview’s blockedPrunes (in use, or in-use status
unverified): drift classifies their delete rows as prune, not unmanaged, even though
they will not be deleted until a reviewed preview verifies them. Changing an instance’s
url or type clears its ledger rows. The cleanup API (POST /api/v1/arr/cleanup)
scans ledger-managed items by default; includeUnmanaged: true restores the legacy
full scan.
Contract Notes
Section titled “Contract Notes”- Do not assume cross-Arr field parity when reading or extending syncers.
- Config names used as sync lookup keys must match persisted identifiers exactly.
- Quality profile compatibility filtering uses app-compatible quality names, not
arr_type='all'scores alone. - Reviewed validation and execution always dispatch through the binding’s explicit
arr_type; there is no sibling-Arr fallback. - A reviewed preview authorizes a bounded attempt only while its private TTL binding and all selected evidence still match. Confirmed outcomes describe actual writes and are a separate contract.
Source References
Section titled “Source References”packages/praxrr-app/src/lib/server/sync/processor.tspackages/praxrr-app/src/lib/server/sync/preview/orchestrator.tspackages/praxrr-app/src/lib/server/sync/preview/reviewBinding.tspackages/praxrr-app/src/lib/server/sync/preview/store.tspackages/praxrr-app/src/lib/server/sync/mappings.tspackages/praxrr-app/src/lib/server/sync/registry.tspackages/praxrr-app/src/lib/server/sync/ownership/record.tspackages/praxrr-app/src/lib/server/sync/ownership/planPrunes.tspackages/praxrr-app/src/lib/server/sync/ownership/executePrunes.tspackages/praxrr-app/src/lib/server/db/queries/arrManagedEntities.tspackages/praxrr-app/src/lib/server/jobs/handlers/arrSync.ts
Related
Section titled “Related”- PCD System — compiled state preview and sync read from
- Job System —
arr.sync.*scheduling and execution - Syncing Profiles — user guide
- Architecture Overview — data flow summary
- Troubleshooting — preview and sync errors