The batch ingest route lives under /api/measurements, not the Coach.
Aligns the errorCode with the sibling DELETE route, which already
emits measurement.delete.too_large.
The previous race-reconciliation block compared post-insert rows in
the DB against the rows we attempted to insert, downgrading any
"inserted-but-not-stored" entries to "duplicate". Under standard
Postgres semantics that branch never fires: `skipDuplicates`
absorbs duplicate-key conflicts but the row is still present in the
table (written by the other batch that won the race). So every
attempted row was "stored" and the loop ran as a no-op, leaving
the per-entry envelope inconsistent with the aggregate counts when
two batches collide. The v1.5 iOS sync cursor depends on the
envelope sums matching the aggregate counts to checkpoint
correctly.
Pragmatic fix: trust `createMany.count` for the aggregate
`insertedCount` and downgrade exactly `toInsert.length -
insertedCount` per-entry "inserted" statuses to "duplicate". The
post-fix loop no longer touches `insertedCount` (it was already
correct from createMany) so the count cannot drift negative. Order
doesn't matter for the client — both statuses checkpoint past the
row and the DB ends up with the same single-copy state.
Concurrent-write integration test asserts the three invariants the
iOS sync cursor relies on: per-entry sums equal aggregate counts,
aggregate counts stay non-negative, and the DB row count matches
the combined `inserted` total.
The Apple Health batch endpoint accepted 500-row payloads with no
per-user write cap. A leaked wildcard iOS token could sustain
unbounded inserts (500 rows × N batches/sec) and degrade the
database for every other user on the host.
Gate the write path with the shared `checkRateLimit` helper at
60 batches/min/user — generous for legitimate iOS sync (the
HealthKit observer coalesces well below one batch/minute on a
quiet device, and 60/min × 500 rows = 30 000 rows/min headroom)
and tight enough that a misbehaving client bottoms out within a
minute. Returns 429 with an unambiguous message so the iOS
client can back off and retry rather than assume the row landed.
Integration test exercises the over-limit path by pre-seeding the
`rate_limits` counter at the cap.
`POST /api/measurements/batch` accepts up to 500 HealthKit-shaped
entries per call, runs each through the new mapping helper, and writes
them under `source = APPLE_HEALTH` with the inbound sample UUID as the
external id. The composite unique index on
`(user_id, type, source, external_id)` does the per-entry dedup;
`Idempotency-Key` covers the batch-level retry case so the iOS observer
queue can replay in flight without double-writing.
Per-entry status (`inserted` / `duplicate` / `skipped`) lets the
client advance its sync cursor accurately even when individual rows
land on different paths. Skip reasons cover unknown HealthKit
identifiers and out-of-plausible-range values so a single bogus sensor
reading never rejects an otherwise clean batch.
Insertion uses chunked `createMany` with `skipDuplicates: true` so a
race between two concurrent retries reconciles cleanly via the unique
index instead of failing the transaction. Wide Event annotation logs
`processed / inserted / duplicates / skipped` for ops visibility.
Six integration tests against a real Postgres testcontainer cover the
happy path, the per-entry duplicate path, the over-cap 422, mixed
unmappable + out-of-range entries, per-stage sleep rows, and
`Idempotency-Key` replay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>