Skip to content

migrations ​

Postgres schema migrations (sqlx), applied in numeric order. Located at backend/migrations/ (001..059).

059 добавляет admin_sessions: UUID сессии, account, SHA256 токена, сроки и отзыв. Исходные JWT не сохраняются; старые cookies без записи требуют входа заново. Таблица и индексы создаются через IF NOT EXISTS, FK удаляет записи вместе с account. Откат базы выполняется восстановлением резервной копии; запуск старого образа не отменяет изменение схемы и гарантию отзыва сессий.

Convention ​

  • One file per change: NNN_short_name.sql, append-only (never edit a shipped migration; add a new one).
  • Data-driven templates ship as migrations (game_templates rows + gameplay specs), so adding a typical game is data-only (D2).
  • Test/demo fixture data (accounts, projects, API keys used only by CI/local smoke scripts) must not ship in new migrations (Task 14 step 4/14.6, DATA-OPS-09). Migrations 025/027/032 predate this rule and already shipped proj_demo/proj_promo_smoke and two plaintext-committed API keys this way — those are not rewritten (already applied everywhere, including production); migration 038 revokes both keys unconditionally instead. scripts/e2e-stack-smoke.sh/e2e-release-gate.sh mint their own fresh, never-committed keys for those same fixture rows at run time rather than depending on the committed ones staying usable. Any new fixture a future test scenario needs belongs in a setup script that runs outside production, never in a schema migration that applies to every environment the migration ever runs in, production included.
  • Migrations are additive/expand-first — never plan a migration and a destructive down together (Task 14 step 4/14.8, OPS-13): the migration step runs before the new service image is rolled out (swarm-start.sh's existing docker stack deploy sequencing already applies the stack's Postgres/Dragonfly service before the app services), rollback compatibility requires both an expand-first schema and an old image built with db::postgres::migrator().set_ignore_missing(true). The current image supports this; a historical binary without that setting can reject newer applied migrations with VersionMissing. A column/table a rollback's old code still reads must not be dropped or renamed in the same deploy that stops writing it — drop it only in a later migration, once nothing still deployable depends on it. sqlx migrate here has no down scripts at all (append-only, forward-only) by design, consistent with this rule.
  • The previous image must start on the newer schema (РФ-10 of the block Г slice plan, Task 14.8): the embedded migrator (db::postgres::migrator()) sets ignore_missing, so versions applied by a newer image and absent from the older binary do not abort startup with VersionMissing. Editing an already applied migration still fails with VersionMismatch (checksum). Regression: tests/platform_14_migrator_rollback.rs.

Map ​

RangeTheme
001-004Init, outbox, demo project, progress/events
005-010Campaigns/rewards, quiz/scratch, economy/score, billing/onboarding, Stripe, memory/wheel
011-015Account roles (D7), plan limits, webhook inbox, project template config, fraud_config (D3)
016-022GameplaySpec + per-game data specs (clicker, scratch, quiz, wheel, memory, match3)
023Campaign schedule
024progress_snapshots + hot restore (D6)
025tap_demo_v1 fixture (data-only game proof)
026GameplaySpec DSL v2 (declarative scratch/wheel/quiz/memory)
027Promo smoke fixture
028-033Project webhook secrets, idempotency, exposure, score evidence and fixture fixes
034Admin two-factor authentication
035Local demo origin fix
036game_runs + game_run_commands и строка шаблона match3_v1 (engine game_run)
037admin_two_factor.last_used_step — single-use TOTP steps
038Revoke the two known fixture API keys (DATA-OPS-09)
039reward_claims.client_idempotency_key + single_per_user and its partial unique index (Task 4)
040account_usage_daily.anonymous_mau — observational split of anonymous vs. plan-relevant MAU (Task 6)
041reward_claims risk/hold columns + player_fraud_status table — value gate (Task 7)
042match3_v1.allowed_modes extended to ["free_play", "promo_rewards"] — win reward through the value gate (Task 8)
043fraud_flags weight/origin/rule/run/address-hash/request-id/dedup/resolution columns + fraud_flags_player_recent_idx — review and flag hygiene (Task 9)
044player_resource_balances.balance gets CHECK (balance >= 0) — economy balance guard (Task 10)
045player_erasure_jobs — durable queue for the hot-storage half of a player erasure (Task 11)
046job_heartbeats — Postgres-backed job liveness, replacing process-local counters the API and worker (separate services) never shared (Task 13)
047events (project_id, event_type, created_at) + events (template_id, event_type, created_at) for the dashboard's actual query shape; drops job_heartbeats_job_name_idx, a redundant leading-prefix duplicate of the table's own PRIMARY KEY (job_name, instance_id) index (Task 15)
048projects.webhook_secret_rotated_at — окно приёма прежнего секрета входящих webhook после ротации (задача 18)
049projects.experiment_key — соль A/B-распределения по проекту (задача 19)
050Настройки игрового направления в projects (play_mechanic, lobby_wheel_enabled, entry_mode, timezone, timezone_next, timezone_switch_at, lobby_config, play_config_revision), аварийный выключатель game_templates.selectable; заполнение play_mechanic='match3_v1'; скрыты memory_pairs_v1, wheel_v1, match3_lite_v1 (блок Г, G1.1)
051player_level_results (истина «уровень пройден») и player_level_progress (сводка с cycle_wins); перенос побед level-001 с completion_source='backfill' (G1.1)
052Колонки game_runs для уровней и механик (level_number, track_version, initial_seed, is_replay, end_reason, expires_at, entry_mode, start_fingerprint), индексы game_runs_active_expiry_idx и game_runs_funnel_idx; срок старых активных партий Match3 (G1.1)
053reward_claims.run_id/mechanic/level_number без внешнего ключа, индекс (project_id, run_id); trigger_params.mechanic='match3_v1' у существующих кампаний game_won (G1.1)
054Скрытые memory_v1 и parcel_pilot_v1 с engine='game_run', только free_play; исторический memory_pairs_v1 сохраняется (G4.1)
055wheel_configs: черновики и неизменяемые опубликованные версии R1; wheel_spins: результат, бесплатный слот проектных суток и ссылка на принадлежащий игроку GameRun. Индексы защищают повтор spin_id и бесплатную попытку; удаление GameRun удаляет его вращение (G6.2). HTTP и клиент включаются отдельными этапами.
056Зарезервирована для альтернативы РП-29 с переносом level-001 на позицию 4. Выбрана позиция 1, файла и переноса нет
057Включение memory_v1.selectable=true после проверки ожидаемого шаблона game_run/free_play; другие шаблоны, режимы и проекты не меняются. Готовность фактического клиента по-прежнему проверяет API (G4.4)
058Nullable-колонка game_runs.cycle_round сохраняет круг текущей партии Match3. Исторические строки остаются с NULL; CHECK разрешает ненулевой круг только от 2, при is_replay=true и заданных положительных track_version и level_number (G3.3/G3.4)

С 050 текст каждой миграции проверяет backend/crates/api/tests/migration_rules.rs: файл только добавляет (IF NOT EXISTS, INSERT … ON CONFLICT, пара DROP CONSTRAINT IF EXISTS + ADD CONSTRAINT) и может быть выполнен повторно. API и worker применяют миграции при старте; образ, собранный после РФ-10, стартует и на базе с более новыми миграциями (ignore_missing, см. выше), а образ, собранный до РФ-10, на базе с 050–053 не стартует (VersionMissing). Исправление ошибки в применённой миграции — только новой миграцией (изменённый файл даёт VersionMismatch).

sqlx учитывает номер и контрольную сумму миграции, поэтому штатный старт не применяет уже учтённый файл повторно. Повторяемость SQL 058 обеспечивают ADD COLUMN IF NOT EXISTS и DROP CONSTRAINT IF EXISTS перед ADD CONSTRAINT. Колонка добавляется без обязательного значения и без обратного заполнения: текущий прогресс игрока не позволяет достоверно определить круг старой партии. API пропускает level.cycle_round, если в строке SQL NULL; поле progression.cycle_round относится к следующему уровню и не заменяет сохранённое значение текущей партии.

Старый путь data-driven игр ​

  1. New migration: INSERT into game_templates (manifest + gameplay spec, engine = data).
  2. Add widget/templates/<id>/bundle.js with a mount() entry.
  3. No Rust changes for typical mechanics; the registry picks it up via AdapterRegistry::from_manifests().

Этот рецепт описывает только существующие legacy-шаблоны. Новую полноценную игру по нему добавлять нельзя. See SpecEngine for the historical spec contract.

Путь игры нового поколения ​

  1. Миграция добавляет строку game_templates с engine = 'game_run' (каталог для админки: название, allowed_modes, selectable).
  2. Rust-модуль механики регистрируется в MechanicRegistry по паре game_id + game_version с дескриптором (kind, authority, client_entry_key, limits) и дорожкой уровней (см. adapters).
  3. Клиент живёт в games/<game_id>/ и собирается в общий выпуск shell/<release>; сборка пишет mechanics.json, сервер находит вход механики по client_entry_key. Ассеты отдаются с домена сервиса.

036_game_runs.sql заводит обе таблицы прохождения. У game_runs есть частичный уникальный индекс (project_id, external_user_id, game_id) для статуса active: у игрока не может быть двух активных партий одной игры. Ограничения CHECK на размер JSON (8 KiB на тело команды, по 128 KiB на состояние и вид клиента, 256 KiB на сохранённый ответ) отсекают раздувание строки до расчёта хода.

Internal & integration documentation