Privacy-limited movement evidence
Issue #168 adds the minimum precise evidence needed for later Intermediate
movement checks without creating a general location-history product. Browser
geolocation remains an untrusted claim; authentication supplies the player
identity, and the API/database own validation and persistence.
Collection boundary
The API stores a sample only after an authenticated active player initiates the
visible location-dependent Explore/event flow, a fresh claim reaches
POST /api/v1/events/{eventId}/eligibility, and the server confirms that the
event exists and is currently active under the authoritative event/campaign
availability resolver. After an event is selected,
the existing Explore journey may perform bounded automatic eligibility refreshes
for meaningful reported-location changes while the page remains visible and in
active use. The watcher pauses when the page is hidden and stops on unmount.
Issue #168 adds no browser watcher, passive/background polling, tracking
endpoint, or movement-history response.
Stale, future, malformed, unauthenticated, nonexistent-event, unpublished, and inactive-event requests do not create evidence. A valid claim can be stored for a reachable, unreachable, or accuracy-ambiguous result because all three arose from the same legitimate active eligibility operation.
Stored precise evidence
Migration 0031_create_movement_evidence.sql creates
public.movement_evidence with:
- a random immutable record ID;
- the server-resolved active player UUID;
- the authoritative event UUID;
- claimed latitude and longitude;
- reported accuracy in metres;
- device acquisition time;
- server receipt and immutable creation time; and
- an explicit expiry time.
Foreign keys, coordinate/accuracy bounds, finite timestamp checks, and indexes
for the latest per-player sample and expiry cleanup reinforce the API checks. A
unique (player_id, event_id, acquired_at) context prevents duplicate retries.
The service locks the active player row before reading every context attached to the latest player-global acquisition time, so concurrent requests and multiple API instances serialize against one physical movement sequence:
- an older acquisition time is rejected as non-monotonic;
- an equal acquisition time with different coordinates or accuracy is rejected as inconsistent evidence;
- an identical equal-time sample already stored for the event succeeds idempotently without another row;
- an identical equal-time sample for another valid event creates one additional immutable context row; and
- a newer acquisition time creates the next physical sample normally.
This preserves player-global ordering for issue #169 without forcing a new
geolocation acquisition merely because the player selects another event.
The table is internal database evidence. No public or player-facing API reads it, and another player cannot supply or retrieve its ownership identity.
Retention and automatic cleanup
MOVEMENT_EVIDENCE_RETENTION_MS controls precise-data retention. The current
default is a provisional 24 hours (86400000 ms), chosen as a conservative,
reversible implementation value rather than a final product/privacy decision.
The team must confirm or revise it before production release based on the
minimum window required by issue #169.
The value must be a safe integer between 1000 and
8000000000000000 milliseconds. The upper bound is only a JavaScript Date
representation safeguard, not a supported business retention period. Each row
stores its calculated expires_at when collected. Changing configuration
affects newly collected evidence only; existing rows keep their recorded expiry.
The API process starts an idempotent cleanup sweep immediately and then every
MOVEMENT_EVIDENCE_CLEANUP_INTERVAL_MS (default 60000 ms). Each sweep deletes
rows whose expires_at is at or before the database operation's cutoff. It logs
only the number deleted; failures use a fixed safe message without row content.
Repeated execution is safe.
The cleanup interval must be a safe integer from 1000 through
2147483647 milliseconds. The upper value is Node's signed 32-bit timer limit;
larger delays would otherwise be reduced to an unsafe one-millisecond interval.
Production deployment must set and review both values. The worker runs per API instance, so overlapping sweeps are expected and safe. Operators should verify the migration and worker after deployment without attaching coordinates to evidence.
Logging, access, and deletion
Fastify redacts latitude, longitude, accuracy, and acquisition time if a request body is logged. The movement service and cleanup worker do not log input rows, and safe API errors contain no prior or submitted coordinates. Ordinary logs may contain a request ID and aggregate cleanup count only.
When an account enters deletion_pending, a database trigger deletes all of
that player's precise movement evidence in the same transaction. Nothing from
this table is retained against the anonymous player tombstone. Issue #168
adds no longer-lived derived decision table because the current eligibility
response and existing gameplay records are sufficient; storing another audit
record is not yet justified.
Relationship to issue #169 and limitations
Issue #169 may read recent rows through an internal server-side service to
compare event journeys. It must continue to ignore browser-calculated speed and
must not turn this table into a public history, permanent punishment, trust
score, or behavioural profile. This implementation does not detect spoofing,
collusion, impossible journeys, or poor-GPS alternatives.
The 24-hour default and production scheduling evidence remain decisions for team/operator confirmation. Passing local tests does not prove that production cleanup is running.
Verification
movement-evidence.test.tscovers runtime validation, physical-sample ordering, identical-sample reuse, idempotency, expiry representation, transaction rollback, and cleanup queries.events.integration.test.tscovers the authenticated active-operation boundary, stale/malformed/unauthorised inputs, and non-monotonic safe errors.movement-evidence-logging.test.tsverifies precise request fields are redacted from ordinary structured logs.movement-evidence-database.integration.test.tscovers real constraints, immutability, ownership/context, player-global ordering, same-event and cross-event concurrency, expiry, idempotent cleanup, and account-deletion removal.
The preceding document was generated and edited with the assistance of: Codex[GPT-5].