Files
HealthLog/prisma/migrations/0041_apns_token_unique_partial/migration.sql
T
Marc-André BombeckandClaude Opus 4.7 d5202e5dd8 fix(devices): enforce global apnsToken uniqueness via partial index
Two collision paths let the same APNs token attach to multiple Device
rows for one user, fanning a single push out to two rows and double-
charging the APNs quota:

  1. The cross-user-hijack guard filtered `NOT: { userId }` so it
     skipped same-user collisions entirely.
  2. The `apns_token` index was non-unique.

Drop the `NOT: { userId }` clause so the lookup catches both cases —
cross-user collisions still 409, same-user collisions fall through to
the existing legacy-token upsert path. Pair with migration 0041
introducing a partial UNIQUE index over `apns_token IS NOT NULL` and
dropping the redundant non-unique sibling. Apple's APNs contract
guarantees one token per (device, app), so global uniqueness matches
the upstream invariant.

Migration is strictly additive at the rolling-upgrade boundary; the
NULL-allowed partial-index shape preserves legacy iOS rows without an
APNs handshake. Schema-side comment documents why the partial index is
introduced via raw SQL (Prisma can't express it natively) and warns
against re-adding the non-unique sibling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:13:47 +02:00

34 lines
1.5 KiB
SQL

-- v1.4.23 W6 reconcile — global uniqueness for `devices.apns_token`.
--
-- Apple's APNs contract guarantees one token per (device, app) — two
-- Device rows pointing at the same `apns_token` would mean the same
-- physical iPhone is registered twice and `sendViaApns()` would fan a
-- single notification out twice, double-charging APNs quota and
-- spamming the user.
--
-- Pair this DDL with the route-side fix in `POST /api/devices` that
-- drops `NOT: { userId }` from the cross-user-hijack guard so the
-- same-user collision path returns the existing 200 idempotent upsert
-- response, and the cross-user collision path keeps its existing 409.
--
-- Strictly additive at the rolling-upgrade boundary:
-- * The new partial unique index covers ONLY `apns_token IS NOT NULL`,
-- so the historical reality of multiple Device rows with NULL
-- `apns_token` (legacy iOS builds without the APNs handshake) stays
-- valid.
-- * The old non-unique `devices_apns_token_idx` is dropped because
-- the partial unique index supersedes it for the dispatcher's
-- per-user lookup.
-- * The CHECK from migration 0037 (token + environment paired) is
-- unaffected.
--
-- If a duplicate row exists in production at upgrade time the migration
-- will fail loudly — that's the intended signal that an iOS install
-- collision is already corrupting deliveries.
CREATE UNIQUE INDEX "devices_apns_token_unique"
ON "devices" ("apns_token")
WHERE "apns_token" IS NOT NULL;
DROP INDEX IF EXISTS "devices_apns_token_idx";