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_templatesrows + 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_smokeand 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.shmint 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
downtogether (Task 14 step 4/14.8, OPS-13): the migration step runs before the new service image is rolled out (swarm-start.sh's existingdocker stack deploysequencing 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 withdb::postgres::migrator().set_ignore_missing(true). The current image supports this; a historical binary without that setting can reject newer applied migrations withVersionMissing. 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 migratehere has nodownscripts 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()) setsignore_missing, so versions applied by a newer image and absent from the older binary do not abort startup withVersionMissing. Editing an already applied migration still fails withVersionMismatch(checksum). Regression:tests/platform_14_migrator_rollback.rs.
Map
| Range | Theme |
|---|---|
| 001-004 | Init, outbox, demo project, progress/events |
| 005-010 | Campaigns/rewards, quiz/scratch, economy/score, billing/onboarding, Stripe, memory/wheel |
| 011-015 | Account roles (D7), plan limits, webhook inbox, project template config, fraud_config (D3) |
| 016-022 | GameplaySpec + per-game data specs (clicker, scratch, quiz, wheel, memory, match3) |
| 023 | Campaign schedule |
| 024 | progress_snapshots + hot restore (D6) |
| 025 | tap_demo_v1 fixture (data-only game proof) |
| 026 | GameplaySpec DSL v2 (declarative scratch/wheel/quiz/memory) |
| 027 | Promo smoke fixture |
| 028-033 | Project webhook secrets, idempotency, exposure, score evidence and fixture fixes |
| 034 | Admin two-factor authentication |
| 035 | Local demo origin fix |
| 036 | game_runs + game_run_commands и строка шаблона match3_v1 (engine game_run) |
| 037 | admin_two_factor.last_used_step — single-use TOTP steps |
| 038 | Revoke the two known fixture API keys (DATA-OPS-09) |
| 039 | reward_claims.client_idempotency_key + single_per_user and its partial unique index (Task 4) |
| 040 | account_usage_daily.anonymous_mau — observational split of anonymous vs. plan-relevant MAU (Task 6) |
| 041 | reward_claims risk/hold columns + player_fraud_status table — value gate (Task 7) |
| 042 | match3_v1.allowed_modes extended to ["free_play", "promo_rewards"] — win reward through the value gate (Task 8) |
| 043 | fraud_flags weight/origin/rule/run/address-hash/request-id/dedup/resolution columns + fraud_flags_player_recent_idx — review and flag hygiene (Task 9) |
| 044 | player_resource_balances.balance gets CHECK (balance >= 0) — economy balance guard (Task 10) |
| 045 | player_erasure_jobs — durable queue for the hot-storage half of a player erasure (Task 11) |
| 046 | job_heartbeats — Postgres-backed job liveness, replacing process-local counters the API and worker (separate services) never shared (Task 13) |
| 047 | events (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) |
| 048 | projects.webhook_secret_rotated_at — окно приёма прежнего секрета входящих webhook после ротации (задача 18) |
| 049 | projects.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) |
| 051 | player_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) |
| 053 | reward_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) |
| 055 | wheel_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) |
| 058 | Nullable-колонка 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 игр
- New migration:
INSERTintogame_templates(manifest + gameplay spec,engine = data). - Add
widget/templates/<id>/bundle.jswith amount()entry. - No Rust changes for typical mechanics; the registry picks it up via
AdapterRegistry::from_manifests().
Этот рецепт описывает только существующие legacy-шаблоны. Новую полноценную игру по нему добавлять нельзя. See SpecEngine for the historical spec contract.
Путь игры нового поколения
- Миграция добавляет строку
game_templatesсengine = 'game_run'(каталог для админки: название,allowed_modes,selectable). - Rust-модуль механики регистрируется в
MechanicRegistryпо пареgame_id + game_versionс дескриптором (kind,authority,client_entry_key,limits) и дорожкой уровней (см. adapters). - Клиент живёт в
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 на сохранённый ответ) отсекают раздувание строки до расчёта хода.