sync_batch.v1 is the legacy usage-only backend contract for statsai.
sync_batch.v2 extends it with hosted task bucket snapshots and task
verification uploads. It also supports an optional authoritative_snapshot
marker for deletion reconciliation.
sync_batch.v3 adds privacy-safe numeric code-change metrics and makes their
acknowledgement counts and authoritative snapshot IDs part of the versioned
contract. Versions 1 and 2 never treat those new fields as authoritative.
sync_batch.v4 adds provider-neutral quota-cycle contributions and makes their
acknowledgement counts and authoritative snapshot IDs part of the versioned
contract. Versions 1–3 never treat those new fields as authoritative.
sync_batch.v5 adds privacy-safe account-plan observations and aggregate
evidence-quality summaries, and makes their acknowledgement counts and
authoritative snapshot IDs part of the versioned contract. Versions 1–4 never
treat those collections or their snapshot IDs as authoritative. Because a v4
acknowledgement carries no counter for them, a collector must not place them in
a v4 batch even though a v4 backend would ignore the unknown keys.
sync_batch.v6 adds daily tool/MCP/skill activity rollups and per-source
coverage rows, plus their acknowledgement counts and authoritative snapshot
IDs. Versions 1–5 never treat absent activity fields as authoritative. Activity
names leave the device only when the collector preference include_activity is
on; turning it off sends an authoritative empty activity ID set so the backend
prunes hosted names. Invocation IDs, arguments, outputs, commands, prompts, and
file paths never appear in a v6 payload.
The same batch schema carries opt-in sessions rows. Each row is
session_rollup.v1 with a closed key set: hashed session id, provider,
source, account, time range, duration, usage, requests, cost, models, message
counts, project, bounded title, and title_source (event, task_span, or
archive). duration_seconds is the wall-clock span and is null when a
session recorded only a start; the backend must accept null before a collector
sends it. active_seconds is the time the agent was working, the union of the
turns the provider recorded, and is null when none were; collectors before it
omit the key. Events keyed by their
own provider record, such as Cursor's usage-export rows, belong to no session
and are not sent, and neither is a session with no project and no title. Prompts, responses, paths, and raw provider session ids stay on
the device. Session retirement is not implied by sync_batch.v6. A snapshot
retires hosted sessions only when it includes session_rollup_ids. An empty
array prunes them. Omitting the key leaves hosted sessions in place, which is
what the collector does when include_sessions alone is off. With projects
excluded, it sends the empty array until the target holds none of its sessions,
since each hosted session carries a project. Pre-v6 acknowledgements
omit the sessions counter.
The collector owns local scanning, normalization, idempotent local storage, and
privacy scrubbing. The backend owns authentication, validation, deduplication,
rollups, and user-facing queries. The production path sends sanitized batches to
a Cloudflare Worker backed by D1 and Better Auth device tokens.
Local Git collection is deliberately bounded: it inspects the current HEAD
and local branches, includes only commits from the last 90 days whose committer
email matches an identity the repository has been scanned under, and ignores
remote-tracking-only history. A repository no committer identity has ever been
known for is not scanned at all, rather than attributing commits speculatively
or reporting an empty scan as authoritative.
Stored Git scans are retained only while a current project or trace references
their repository. If an actively referenced repository temporarily fails to
scan, its last successful snapshot is retained with partial Git coverage;
unreferenced repository scans are retired from derived metrics and local cache,
including the aged metrics that the rolling window can no longer rebuild.
A repository is known by two names, its identity hash and its worktree root, and either can change while the repository stays exactly where it was: adding an origin remote re-keys it, and moving a worktree relocates it. Each stored repository is therefore claimed by the scan that is the same repository, and its aged metrics are rewritten onto the hash it goes by now, so retirement only ever compares against hashes still in use.
Exempting a renamed repository from retirement is not sufficient, because a re-keyed repository leaves aged metrics under a hash it has stopped using: once that scan row is gone, no later refresh has any record of the old hash, so those rows match nothing, survive every retirement decision, and are republished indefinitely. Lineage is established by a shared worktree root, or failing that by a shared commit — commit hashes are globally unique, so an overlap proves two scans are the same repository even when both of its names changed at once.
Remembered committer identities are looked up under both names for the same reason. A re-keyed repository is still found by its root, and a relocated one is still found by its hash; either lookup alone would hand the scan an empty set in the case the other covers, and the scan would then drop in-window commits made under an earlier address and retire them remotely. The root is matched exactly rather than by prefix, so a repository nested inside another never inherits its parent's identities.
Removing a source's data rebuilds metrics whether or not any reconstructed edits went with it, because committed churn is discovered from the project paths carried by usage rather than from traces.
The CLI produces a sync batch with:
cargo run -p statsai -- sync --sink stdout
cargo run -p statsai -- sync --sink http --endpoint http://127.0.0.1:8787/api/sync/batches
cargo run -p statsai -- sync --sink http --since-last
cargo run -p statsai -- sync --sink http --verifyThe JSON Schema is available with:
cargo run -p statsai -- schema sync-batchThe current production sync path strips record-level local evidence before sending:
SourceLocation.path_labelProviderAccount.plan_nameUsageEvent.source.source_record_idUsageEvent.parse_evidence.source_line_numberUsageEvent.parse_evidence.source_record_idUsageSummary.source.source_record_idUsageSummary.parse_evidence.source_line_numberUsageSummary.parse_evidence.source_record_idTaskSpan.source_record_idTaskSpan.session_idTaskSpan.thread_idSubscription.notes- code source text, diffs, file paths, tool arguments, and commit messages
Account-plan sync sends only the canonical account reference, provider, raw and display plan names, observation time, explicit provider bounds, evidence kind, confidence, and aggregate coverage/conflict counts. Email, provider user ID, conversation and turn IDs, artifact paths, tokens, and raw provenance remain local.
Code-change counting uses an explicit source-file extension allowlist.
Documentation, configuration, manifests, lockfiles, generated output, and
unknown text formats are excluded. Mutation records that claim a whole-file
deletion without exposing the deleted content reduce trace coverage instead of
emitting a zero-line edit.
Trace edit identities include the mutation section ordinal, so identical edits
within one tool call remain distinct while repeated scans remain idempotent.
Applied agent edits are published as one metric per day, project, and
repository rather than one per individual edit, so an archive holding hundreds
of thousands of edits still syncs a payload proportional to observed days.
Archive collection reconciles its stored file keys before rebuilding metrics;
when a previously imported archive file disappears, its trace edits and coverage
are removed while the retained conversation archive remains available locally.
Reconciliation is skipped when the archive root itself is unreachable, because
an unmounted volume or a renamed home directory produces the same empty file
list as an emptied archive and must not be read as a deletion. Removing a source
with --delete-data drops that source's reconstructed edits, coverage, identity
observations, plan observations, and conversation bindings, then rebuilds the
derived metrics immediately. The next authoritative snapshot therefore retires
hosted rows instead of continuing to republish deleted source evidence.
An edit is placed in a repository only when its path stays inside that
repository. A tool call naming an absolute path elsewhere, or escaping the
project with .., is still counted as an applied agent edit but carries no
repository_hash.
A shell tool call reduces trace coverage only when its command could reach a
file; confidently read-only inspection such as ls, git status, or
cargo test leaves coverage intact. A program is read-only only in the forms
that cannot write: find -delete, sort -o, and env <program> are judged by
what they would actually do. Interpreters are never classified read-only, since
sed, awk, and perl all reach the filesystem from inside their own program
text and no flag inspection can prove one of them safe. A patch hunk that arrives before any
file header is dropped and reported as unmeasured rather than charged to the
next file in the patch. A project path that is not inside a Git
repository is likewise not a failed scan: agents also run in scratch
directories, and those must not degrade Git coverage. Commits and trace records
dated beyond the sync target's future skew are left unmeasured, so a
clock-skewed record degrades coverage instead of failing every later sync: a
skipped commit degrades its repository's Git coverage, and a skipped trace edit
degrades the trace coverage stamped on every metric that run publishes. A trace
edit carrying no timestamp at all is skipped the same way, because metrics are
published per day and an edit belonging to no day cannot be carried by one.
Commits are attributed by committer email, and a repository remembers every
identity it has been scanned under rather than only the address user.email
holds right now. Reconfiguring that address does not rewrite the commits already
in the object database, so matching on the current value alone would report an
authoritative scan of zero commits, delete measured commits from the local store
and retire them remotely through the authoritative snapshot. The remembered
addresses are stored blinded, local to the device, and never synced.
A scan therefore fails only when no identity has ever been known for the
repository, which is an unanswerable question rather than an answer of "none". A
repository seen for the first time in that state stays unmeasured; one that
already knows an identity keeps measuring under it, so a temporarily missing
user.email costs no coverage at all.
Identities are never forgotten while the repository is still on record, so configuring a colleague's address once claims it for that repository until the repository itself is retired. Over-counting inside a repository the user already works in is preferred to deleting history the user did write, and retiring a repository once nothing references it drops its remembered identities along with its commit rows.
Authenticated HTTP preflight returns a 32-byte, user-scoped code-change identity
key. The collector uses that key to HMAC the local repository identity and raw
commit object ID into the uploaded ccm_... metric ID. Devices paired to the
same user therefore converge on one committed metric ID, while raw commit hashes
remain local and observers cannot enumerate public commits from the uploaded ID.
Repository identity canonicalizes common SSH and HTTPS remote forms before
deriving that ID and does not mix device-local refs or shallow-clone roots into
a remote-backed identity. Root commits identify only repositories without an
origin. A repository that has neither an origin nor a first commit has no shared
identity to derive, so it is keyed by its local path until a commit supplies a
root; without that, every such repository would collide on one identity and
merge unrelated work.
Attribution is withheld entirely, at every confidence level, from an edit that
changes too few distinct lines to single out a commit. A lone } or blank line
reaches a perfect overlap against whichever commit happens to touch the file,
which would credit an agent for human-written work. Such an edit still counts as
an applied agent edit, and its commit still counts as committed churn; only the
link between them is withheld. When project metadata is excluded, both project_id and the otherwise
identifying repository_hash are omitted; the blinded metric ID still provides
cross-device deduplication.
The CLI uses the key only while preparing the HTTP sync and does not persist it
in the local SQLite store or print it in the normal sync-status response.
Endpoints that do not expose the preflight capability get randomly generated
ccm_... IDs instead. Those IDs stay stable for a given store, but two devices
mint different IDs for the same commit, so a backend cannot deduplicate them;
the CLI warns when it uploads committed metrics without a blinding key.
repository_hash is a digest of a canonicalized remote URL, not a secret: an
observer holding a list of candidate remotes can confirm which repository a hash
belongs to. It is therefore treated as project-identifying metadata and is
omitted, together with project_id, unless project sync is enabled.
Committed metrics for days older than the 90-day Git observation window are retained as already-materialized history rather than rebuilt. They stay in the authoritative snapshot so a rescan never retires them, and only rescanned days are recomputed. Trace-matched metrics are not carried forward: an attribution claim depends on both a commit and the archived trace it was matched against, and once the commit ages out that claim can no longer be reverified, corrected, or retired when the trace behind it is deleted. Attribution is therefore a rolling window over the churn totals, which stay complete. A carried-forward committed metric is re-keyed to the account-scoped blinded ID as soon as one is available, so commits that materialized before hosted login still deduplicate across devices. Merge commits are excluded from committed totals: a merge diff replays the churn its branch already reported through the branch's own commits. Conflict resolutions carried only by a merge commit are consequently not counted.
ProjectInfo.path_label is retained for owner-facing project location displays,
manual project linking, and hosted task review. Hashed path, source, event, and
summary identifiers remain so the server can deduplicate records and keep
stable location identity.
Canonical provider account identity may now sync through
ProviderAccount.provider_user_id and ProviderAccount.email. Hosted task
snapshots can also include bounded task titles, summary previews, todo excerpts,
repo labels, branch labels, path labels, and task verification actions. The
backend uses identity plus project metadata to route those hosted task records.
Cost payloads may include provider_reported_micro_usd,
estimated_api_equivalent_micro_usd, or task-level
estimated_cost_micro_usd. Receivers prefer these integer micro-USD values and
fall back to legacy rounded-cent fields when they are absent.
When a local pricing ruleset advances, StatsAI reprices persisted normalized
events and refreshes the affected sync_rollups in place. Changed rollups are
marked dirty so a later incremental HTTP sync publishes the corrected values
without --rebuild-rollups or --full. Provider-reported cost fields are
never replaced by estimates.
User-defined aliases are still retained in ProviderAccount.account_label for
display, but they are no longer the primary account key.
For local end-to-end development, run any compatible HTTP service and point the CLI at it. The CLI now defaults to the hosted production URLs, so export the localhost pair explicitly when you want a local session that stays separate from hosted sync:
export STATSAI_API_URL="http://127.0.0.1:8787"
export STATSAI_WEB_URL="http://127.0.0.1:3000"
cargo run -p statsai -- auth login
cargo run -p statsai -- sync --sink http --endpoint http://127.0.0.1:8787/api/sync/batches
cargo run -p statsai -- sync --sink http --endpoint http://127.0.0.1:8787/api/sync/batches --since-last
cargo run -p statsai -- sync --sink http --verify
cargo run -p statsai -- sync --statusCredential-bearing requests require HTTPS, except for local development using
an explicit numeric loopback host such as 127.0.0.1 or [::1]. Plaintext
hostnames, including localhost, are rejected.
The daemon still supports /v1/sync/batches for loopback-only diagnostics, but
rejects batches containing authoritative_snapshot because it does not stage
device ownership or reconcile deletions. It rejects batches carrying
quota_cycle_contributions for the same reason: a local store keeps quota
observations and derives its own cycles from them, so acknowledging another
device's cycles would tell the sender they had been stored when they had not.
It likewise rejects activity_rollups and activity_coverage: hosted activity
is a Cloudflare-only collection, and the loopback daemon has nowhere to store
another device's names. It rejects sessions for the same reason: hosted
session rollups are stored only by /api/sync/batches.
/api/sync/batches is the production contract. A compatible backend should:
- require an authenticated device access token
- accept
Authorization: Bearer <device_access_token>from stored auth,--auth-token, orSTATSAI_SYNC_TOKEN - validate the request body against
sync_batch.v1throughsync_batch.v6 - reject unsupported
schema_versionvalues - deduplicate sources, accounts, source-account assignments, subscriptions, summaries, and equivalent account-plan evidence when server-side deduplication is needed
- treat collector IDs as stable client-provided IDs, not database primary keys exposed to users
- compute daily, monthly, and dashboard rollups server-side from accepted summaries
- atomically replace accepted task bucket snapshots per
(user, device, project_bucket) - treat the ordered
authoritative_snapshotfragments sharing onesnapshot_idas the complete set of metadata and summary IDs owned by the authenticated device; v3 fragments also carry code-change metric IDs; v4 fragments also carry quota-cycle contribution IDs; v5 fragments also carry account-plan observation and evidence-summary IDs; v6 fragments also carry activity rollup and coverage IDs and, when the collector opts into session retirement,session_rollup_ids; each fragment carries zero-basedpart_indexand a commonpart_count, with at most 200 IDs across its ID arrays - stage snapshot ownership without pruning until the final in-order fragment; then apply ownership and deletion reconciliation atomically, pruning a hosted entity only when no device still owns its canonical row
- reject missing or out-of-order snapshot fragments; send them only after all data chunks in the same logical full sync have succeeded, while batches without snapshot fragments remain incremental
- project hosted task verifications onto the latest bucket snapshot when serving task reads
- return accepted, updated, duplicate, and rejected counts
A chunk that fails is answered by one of two remedies, and never the other's. A rejected size (HTTP 413) is a decision about that batch and is answered by splitting it into smaller batches. A transient infrastructure failure (HTTP 500, 502, 503, or 504) is the absence of a decision — the batch was neither accepted nor rejected, only the answer was lost — and is answered by resending the identical chunk after a doubling backoff, three times before the run gives up. Resending is safe because ingest records the batch ID in the same transaction that applies the payload: a resent chunk either applies exactly once or is acknowledged as a duplicate. Statuses are read without requiring a JSON body, since these failures come from the infrastructure in front of the worker and answer in plain text.
HTTP 429 is not resent on that doubling schedule: it carries the endpoint's
own Retry-After, and retrying sooner would work against the limit it asked
for. The sender reads retryAfterSeconds from the JSON body, or a
Retry-After: <seconds> token echoed from the header on a non-JSON body,
sleeps for that delay (clamped to 1–120 seconds), and resends the identical
chunk, three times before the run gives up. A 429 with no delay advertised
still fails the run immediately. Batch count does not trip this limit;
throughput does. Small, fast batches on a new device are more likely to fill
a 30-request rolling minute than a large device whose payloads pace themselves.
Any other 4xx is a decision that repeating cannot change, so it fails the
run immediately.
{
"schema_version": "sync_ack.v1",
"batch_id": "batch_1710000000000",
"accepted": {
"sources": 1,
"accounts": 1,
"source_account_assignments": 1,
"subscriptions": 0,
"events": 1,
"summaries": 0
},
"duplicates": {
"sources": 0,
"accounts": 0,
"source_account_assignments": 0,
"events": 0,
"summaries": 0,
"subscriptions": 0
},
"rejected": []
}sync_batch.v2 returns sync_ack.v2, which adds task_buckets and
task_verifications counters under both accepted and duplicates.
sync_batch.v3 returns sync_ack.v3, which additionally adds the
code_change_metrics counter.
sync_batch.v4 returns sync_ack.v4, which additionally adds the
quota_cycle_contributions counter.
sync_batch.v5 returns sync_ack.v5, which additionally adds the
account_plan_observations and account_evidence_summaries counters.
sync_batch.v6 returns sync_ack.v6, which additionally adds the
activity_rollups, activity_coverage, and sessions counters. Earlier
acknowledgements omit sessions.
Collectors require the acknowledgement version to match the submitted batch
version exactly; a v1 acknowledgement cannot successfully acknowledge a v2
batch, a v2 acknowledgement cannot acknowledge a v3 batch, a v3
acknowledgement cannot acknowledge a v4 batch, a v4 acknowledgement cannot
acknowledge a v5 batch, and a v5 acknowledgement cannot acknowledge a v6
batch.
The current loopback daemon returns this shape and reports duplicate events
when the existing store already has the semantic event. Source, account,
source-account assignment, subscription, and summary upserts are currently
reported as accepted writes. Its quota_cycle_contributions counter is
therefore always zero: a batch carrying any is refused outright rather than
acknowledged, since the daemon has nowhere to store them.
Two kinds of addition behave very differently, and the difference decides the release order.
A new top-level collection is backward compatible. Batch collections carry
#[serde(default, skip_serializing_if = "Vec::is_empty")], and the backend
reads named fields without a top-level allowlist, so a deployment that predates
the collection ignores it rather than failing. That is not silent data loss:
collectors require accepted + duplicates == submitted for every collection
they send, so an endpoint that ignores one reports zero against a non-zero
submission and the sync fails loudly with a count mismatch. Local sync state
stays dirty and the records are resent. Adding a collection therefore needs no
schema-version bump.
A new field on an existing record is not. Every synced record is checked
against a closed set of permitted keys, and an unrecognized key refuses the
whole batch with 400 invalid_sync_batch. This is deliberate: an unexpected
key may carry a path, address, or message that this contract excludes, and the
refusal guarantees none of it is stored. The consequence is a hard release
order:
Deploy the backend before releasing a collector that adds a record field. Until it is deployed, upgraded collectors cannot sync anything — not just the affected collection.
To keep the two causes distinguishable, the refusal names the offending field:
{
"error": "invalid_sync_batch",
"rejected": [
{
"kind": "quota_cycle_contributions",
"id": "quota_cycle_0f2c…",
"reason": "unknown_field:has_schedule_overlap"
}
]
}Nested records report a dotted path, such as
unknown_field:boundary_slices.working_directory. Only the field name is
returned; its value is never echoed. Collectors render this as
endpoint does not recognize \` on ` rather than an opaque
HTTP error.
Malformed values inside a known field remain a plain 400 with no rejected
detail — that is a client defect, not version skew.
After a successful sync, the collector records local sync state keyed by sink
and target. The state stores the last successful batch, event cursor, summary
cursor, and failure count. Passing --since-last sends only events and
summaries after the recorded cursor for that sink target while still including
the current source, account, source-account assignment, and subscription
metadata.
Full HTTP rollup syncs send their authoritative snapshot as the final logical chunk. The marker lists all current source, provider-account, source-account-assignment, subscription, summary, code-change metric, quota-cycle contribution, account-plan-observation, and evidence-summary IDs, including empty lists. The backend tracks ownership per authenticated device and keeps device-local IDs separate from server-canonical IDs so account alias reconciliation cannot delete the canonical row. Incremental and legacy batches omit the marker and never prune absent records.
When a device completes its first authoritative snapshot against a database that
predates ownership tracking, the backend also reconciles legacy summary rows
whose stored device_id matches that device. Rows represented by the completed
snapshot are retained through their active ownership mapping; omitted unowned
rows are pruned. Legacy rows from other devices, and canonical rows still owned
by any device, are preserved.
The HTTP sink parses sync_ack.v1 through sync_ack.v6 before
updating local state. File and stdout sinks update state after their local write
succeeds.
The production backend uses Better Auth on Cloudflare Workers plus D1. The CLI
opens the web app configured by STATSAI_WEB_URL, pairs the local device
through a loopback callback, stores a device refresh token in a backend-scoped
local auth file, and sends sync batches to the Worker API:
POST /api/sync/batches
D1 stores app-owned tables for devices, device tokens, sources, provider accounts, source-account assignments, subscriptions, daily rollups, monthly rollups, dashboard snapshots, and sync batch metadata. Better Auth owns its auth/session/account tables in the same D1 database. That backend lives outside this public CLI repo.
cargo run -p statsai -- auth login
cargo run -p statsai -- auth status
cargo run -p statsai -- sync --sink http --since-lastAuth token precedence for sync is:
--auth-token > STATSAI_SYNC_TOKEN > stored Cloudflare device access token
The Worker rejects raw event cloud sync by default and accepts sanitized daily
summary rollups plus metadata, along with hosted task snapshots and hosted task
verification actions for sync_batch.v2 and later, plus code-change
metrics for sync_batch.v3 and later, plus quota-cycle contributions for
sync_batch.v4 and later, plus privacy-safe account-plan evidence for
sync_batch.v5, plus opt-in activity rollups and coverage for
sync_batch.v6, plus opt-in session rollups on that same v6 schema. The collector now prepares those
daily rollups before HTTP sync, so a normal Cloudflare sync can populate the
dashboard without shipping raw events. Repeated batches are idempotent by
stable IDs.
The dashboard reads compact API responses backed by D1 rollups instead of
scanning all synced records in the browser.
HTTP chunking is a transport concern and must not change canonical hosted
state. Account aliases are persisted per user and device in
sync_entity_owners. Every later assignment, subscription, and summary chunk
resolves its device-local account ID through that mapping, even when the chunk
contains no account records. Account-plan observations and evidence summaries
use the same mapping. When a newly observed alias matches historical
rows from that device, the backend repairs their indexed account ID and JSON
payload in the same D1 transaction. The repair first discovers affected daily
months and period rows, then rebuilds their monthly rollups and the all-time
dashboard snapshot in that transaction. Targeted month rebuilds switch to one
bulk rebuild for wide histories, and all lookup/materialization statements are
included in the sync D1 query budget. The preliminary budget check includes
only work known before reconciliation; actual alias-repair and impact-analysis
queries are added to the exact estimate after aliases have been resolved.
Provider user IDs are stronger identity evidence than email addresses. Email may connect records only when it does not bridge two different non-empty provider user IDs. Ambiguous email-only identities remain separate.
Each accepted batch stores a SHA-256 digest of its normalized payload. Retrying
the same batch ID and payload returns the duplicate acknowledgement; reusing
the ID with different content returns batch_id_payload_conflict. The receipt
insert is the first statement in the same atomic D1 batch as all mutations, so
a competing request cannot change mirrored rows before losing the receipt
claim. Historical receipts created before digest support retain their previous
retry behavior.
GET /api/sync/status returns mirror counts for the authenticated device,
computed from active ownership records. User-wide canonical counts remain in
the consistency diagnostics and are not compared with a single device's local
store.
Subscription rows are retained as evidence. Subscription API and dashboard context reads project that evidence into entitlements: verified provider or local-auth evidence wins over manual evidence for each canonical account's connected billing-window cluster, while disconnected periods remain distinct. Different provider subscription IDs are never merged. Interval observations are parsed once and clustered with sorted sweeps, keeping projection work O(n log n) even when stored evidence spans many sync batches.
SYNC_REQUIRE_CANONICAL_ACCOUNTS=1 rejects any non-null child account
reference that cannot be resolved to an account already stored or included in
the same batch. It is disabled by default for the additive deployment. Enable
it only after:
- applying all D1 migrations and recording a Time Travel bookmark;
- completing a full sync from every active device so historical aliases are repaired;
- verifying that no non-null account references are absent from
provider_accountsacross assignments, subscriptions, daily rollups, and period summaries; - confirming shadow dashboard totals and per-device mirror counts;
- changing the production variable to
1and deploying the Worker.
The preflight orphan query must return zero for every row:
SELECT 'source_account_assignments' AS relation, COUNT(*) AS orphan_count
FROM source_account_assignments child
LEFT JOIN provider_accounts parent
ON parent.user_id = child.user_id
AND parent.provider_account_id = child.provider_account_id
WHERE child.provider_account_id IS NOT NULL
AND parent.provider_account_id IS NULL
UNION ALL
SELECT 'subscriptions', COUNT(*)
FROM subscriptions child
LEFT JOIN provider_accounts parent
ON parent.user_id = child.user_id
AND parent.provider_account_id = child.provider_account_id
WHERE child.provider_account_id IS NOT NULL
AND parent.provider_account_id IS NULL
UNION ALL
SELECT 'daily_rollups', COUNT(*)
FROM daily_rollups child
LEFT JOIN provider_accounts parent
ON parent.user_id = child.user_id
AND parent.provider_account_id = child.provider_account_id
WHERE child.provider_account_id IS NOT NULL
AND parent.provider_account_id IS NULL
UNION ALL
SELECT 'period_summaries', COUNT(*)
FROM period_summaries child
LEFT JOIN provider_accounts parent
ON parent.user_id = child.user_id
AND parent.provider_account_id = child.provider_account_id
WHERE child.provider_account_id IS NOT NULL
AND parent.provider_account_id IS NULL;Once enabled, a child chunk that arrives before its account metadata is rejected without storing partial data. The client may send the account chunk and safely retry the original child batch ID.
- Whether the first backend stores sanitized event payloads as JSON blobs first, then promotes indexed columns later.
- Whether periodic sync should use a launch agent/service, an app daemon, or both.