Skip to main content

Player progression persistence

Migration 0026_create_player_progression.sql adds the authoritative data foundation for points, achievements, Johannesburg activity days, streaks, and all-time/monthly standings. The browser cannot insert point values, unlocks, activity days, totals, or ranks.

Points are permanent and non-spendable. This migration and its persistence adapter neither read nor write coin wallets or coin-ledger entries. Applied migrations that introduced the legacy coin schema remain immutable; issue #193 owns its forward-only retirement and the removal of active coin response fields.

Relations

RelationResponsibility
progression_point_rulesVersioned challenge-score and CPU-result values; only activation may change on an existing version
player_point_awardsImmutable ledger decision for exactly one authoritative event completion or completed CPU match
player_activity_daysOne Johannesburg calendar row per player/day, with first/last award evidence and completion count
player_progression_stateIndexed all-time total and streak ending at the latest persisted activity day
player_monthly_progressionIndexed calendar-month total and completion count
achievement_definitionsData-driven metric, threshold, accessible display name, and description
player_achievement_unlocksImmutable unique player/achievement unlock linked to the award that first satisfied the persisted rule

Composite foreign keys ensure an award source belongs to the same player. Each event-completion and CPU-match source has a unique partial index, so even different workers cannot create two awards. Insert validation checks the source status, player, result/score, completion timestamp, point rule, and awarded value before accepting a ledger row. Ledger and unlock update/delete triggers then keep the audit history immutable.

Transaction interface

createProgressionPersistence() exposes two internal operations:

  • recordChallengeCompletion(client, completionId)
  • recordCpuMatch(client, matchId)

The caller supplies its existing transaction client. Each operation invokes a private PostgreSQL function that loads the authoritative source, takes a transaction-scoped per-player advisory lock, and returns either the newly inserted award or the existing result on replay. A new award updates activity, monthly and all-time totals, recomputes streak islands, and inserts newly met achievements with conflict-safe uniqueness.

An unknown or unfinished source raises an error. When issue #172 wires this adapter into challenge and match completion, that failure must propagate so the owning completion transaction rolls back. This issue deliberately does not add placeholder, asynchronous, browser-authoritative, or HTTP behavior.

Asynchronous PvP extension gate

Migration 0026 cannot represent an asynchronous PvP award: its checked source shape and adapter accept only event completions and CPU matches. Issue #179 must use a new forward migration rather than editing 0026. The extension must:

  • add a dedicated asynchronous_pvp source kind linked by foreign key to the completed match/participant result persisted by #179;
  • enforce exactly one award decision for each (matchId, participantId) pair, including an approved zero-point result;
  • validate the authoritative outcome, completion reason, participant, and completion timestamp before accepting the award;
  • expose one transaction-aware recordAsynchronousPvpCompletion(client, matchId) adapter operation that records both participant decisions inside the terminal match transaction, acquiring per-player locks in canonical player-ID order; and
  • reuse the per-player serialization, immutable ledger, activity, aggregate, streak, and achievement machinery only as approved by the progression rule gate.

Issue #180 depends on this #179 schema/adapter extension and the Product Lead rule decision. It must not overload cpu_match, write one participant outside the terminal transaction, or invent placeholder values while either gate is unresolved.

Time and standings

activity_day is the authoritative completion timestamp converted with Africa/Johannesburg; activity_month is the first date of that local calendar month. Persisting the individual days means late or out-of-order processing can recompute streaks correctly.

player_progression_all_time_standings and player_progression_monthly_standings(month) apply competition ranking. Their queries include every active player through a left join, so an account with no progression row receives zero points. Indexes lead on all-time points or the month and monthly points. Consumers must order tied rows by Player Name and then internal UUID for deterministic pagination.

The persisted streak_at_last_activity is the run ending at the latest activity date. Read models turn it into an effective current streak of zero after a complete Johannesburg day is missed; no scheduled midnight update is required.

Backfill and rule changes

The migration processes retained event completions and completed CPU matches in timestamp/source/ID order through the same private functions used for new awards. This reconstructs historical totals, months, activity, streaks, and achievement unlocks for active and anonymised deleted players.

Point-rule changes are prospective. Add a new inactive version, deactivate the old version and activate the new version in one transaction. Existing awards retain their original version and value. Applied migration history and ledger rows must never be edited to rebalance points.

Privacy and retention

Progression stores no email, Auth0 subject, or location. Account deletion keeps the immutable award, activity, totals, and unlock history against the anonymous player UUID because their challenge and completed-match sources are already retained. Live all-time and monthly standings filter to active accounts, so a deleted tombstone is not publicly ranked.

Verification

apps/api/tests/progression-persistence.integration.test.ts creates a disposable database at the pre-0026 schema, inserts retained history, applies the migration, and verifies backfill, zero-point records, Johannesburg boundaries, out-of-order streaks, duplicate and concurrent writes, unique unlocks, prospective versions, competition ties, zero-point standings, source validation, immutability, and deleted-player filtering. The explicit database runner registers this file so npm run test:database --workspace @wits-world/api executes the suite.

The preceding document was generated and edited with the assistance of: Codex-CLI[gpt-5.6-sol medium] and Codex[GPT-5].