--- type: database title: Seed Sample Challenges Migration (1700000000600) description: Forward-only migration that inserts a representative sample set of challenges on a fresh database so the /challenges board renders cards without manual admin action. tags: [database, migration, challenge, seed, sample] timestamp: 2026-07-23T02:23:34Z --- # Purpose A brand-new HIPCTF database (default path [`./data/db.sqlite`](/database/schema.md)) has no `challenge` rows until an admin imports or creates them. The `SeedSampleChallenges1700000000600` migration bridges that gap by inserting a small, representative sample set so the authenticated `/challenges` board (see [Challenges Board](/guides/challenges-board.md)) already has cards on the very first visit and the public landing can demonstrate the platform with real content. The migration runs at the end of the `MIGRATIONS` array in [`DatabaseModule`](/architecture/backend-modules.md) so every other category / challenge schema migration has already landed and the `category` rows it joins against are present. # File `backend/src/database/migrations/1700000000600-SeedSampleChallenges.ts` Registered last in `backend/src/database/database.module.ts` so it runs after `UpgradeChallengeAdminSchema1700000000500`. # Sample set Eight challenges, one or more per canonical system category. Source of truth is the typed `SAMPLE_CHALLENGES` array exported by the migration: | Name | `system_key` | Difficulty | `initial_points` | `minimum_points` | `decay_solves` | Flag | |-------------------|--------------|------------|------------------|------------------|----------------|--------------| | Alpha Cipher | `CRY` | `LOW` | 200 | 100 | 5 | `flag{alpha}` | | Beta Cipher | `CRY` | `MEDIUM` | 300 | 100 | 10 | `flag{beta}` | | Zeta Key | `CRY` | `HIGH` | 500 | 100 | 10 | `flag{zeta}` | | Web Welcome | `WEB` | `LOW` | 100 | 50 | 10 | `flag{web1}` | | Mobile Mayhem | `PWN` | `MEDIUM` | 300 | 100 | 10 | `flag{mob1}` | | Hardware Hello | `HW` | `LOW` | 100 | 50 | 10 | `flag{hw1}` | | Markdown Mystery | `MSC` | `LOW` | 100 | 50 | 10 | `flag{md}` | | Reverse Ranger | `REV` | `MEDIUM` | 300 | 100 | 10 | `flag{rev1}` | All rows are inserted with `protocol='WEB'`, `port=NULL`, `ip_address=''`, and `enabled=1` so they appear on the board the moment the event window opens (see [Event Window](/guides/event-window.md)). Categories are resolved by `system_key` rather than UUID so the seed works regardless of the UUIDs the category migration assigned at install time. Sample flags follow the `flag{}` convention and are exposed only inside admin views — the public challenge DTO continues to strip them (see [Challenges Endpoints](/api/challenges.md)). # What the `up()` does 1. **Idempotency guard** — `SELECT COUNT(*) FROM "challenge"`. If the table already contains rows the migration returns immediately; an admin that imported a custom challenge set first is never overwritten. 2. **System-category lookup** — `SELECT id, system_key FROM "category" WHERE system_key IS NOT NULL` and builds an in-memory `Map`. If fewer than six canonical keys are present (the seed cannot bind) the migration aborts as a no-op rather than inserting challenges into the wrong category. 3. **Insert sample rows** — for each entry in `SAMPLE_CHALLENGES`, an `INSERT INTO "challenge"` with a fresh UUID v4, ISO 8601 `created_at` / `updated_at` via `strftime('%Y-%m-%dT%H:%M:%fZ','now')`, and `enabled=1`. Categories without a matching `system_key` are silently skipped (defensive — only happens if the canonical six are missing). # Idempotency * Running the migration twice (or restarting after it has been recorded) leaves the same eight rows. The early-return count guard skips the second run. * Re-running the migration after a custom `challenge` row exists also no-ops, so manually-imported content from the [Admin — Challenges Full-Replace Import](/guides/admin-challenges-import.md) flow is preserved. # `down()` Scoped to the seeded set: `DELETE FROM "challenge" WHERE "name" IN (...)` using the eight fixed names from `SAMPLE_CHALLENGES`. It deliberately matches **only** the sample names so unrelated admin rows are not removed. # Tests | File | What it asserts | |---|---| | `tests/backend/migrations.spec.ts` — *"seeds sample challenges on a fresh database (Job 906)"* | After all migrations run on an in-memory SQLite DB, the eight expected sample names are present. | | `tests/backend/migrations.spec.ts` — *"seeded sample challenges are enabled, schema-compatible, and point at the right categories (Job 906)"* | Every seeded row has `enabled=true`, a valid `difficulty`, `initial_points >= minimum_points`, a non-empty `flag`, and a `category_id` that resolves to one of the six canonical system categories. | | `tests/backend/migrations.spec.ts` — *"SeedSampleChallenges down() scope (Job 906)"* | `down()` removes only rows whose name is in the seed list and leaves manually-inserted rows of similar names (e.g. `Other`) intact. | | `tests/backend/challenges-board.spec.ts` | Board-listing test now allows extra seeded cards inside the CRY column (`Alpha Cipher`, `Beta Cipher`, `Zeta Key`) while still asserting that the hand-inserted `alphacipher` LOW and `Zeta-key` HIGH rows occupy the bookended slots and that the disabled `hidden` row is excluded. | # Operational notes * On a fresh database the seed fires before the first app boot, so [`GET /api/v1/challenges/board`](/api/challenges.md) returns the sample columns immediately and the player-facing [Challenges Board](/guides/challenges-board.md) shows eight cards. An admin deleting every challenge is the only way to reach an empty board. * To confirm on the command line after `npm run setup`: ``` sqlite3 ./data/db.sqlite 'SELECT COUNT(*) FROM challenge;' ``` Expected count: `8`. Re-running setup on a populated DB leaves the count unchanged (idempotent guard). * Played flags are usable for end-to-end demos: e.g. submitting `flag{alpha}` against the Alpha Cipher card immediately awards points and broadcasts the solve on the [Scoreboard Stream](/guides/scoreboard-stream.md). # See also * [Database Schema Overview](/database/schema.md) — full migration list and bootstrap behavior. * [Challenge Tables](/database/challenges.md) — `challenge` schema, indexes, and provenance. * [Challenges Endpoints](/api/challenges.md) — board query and flag submission. * [Challenges Board](/guides/challenges-board.md) — user-facing guide. * [Admin — Challenges](/guides/admin-challenges.md) — how an admin replaces or extends the sample set. * [Category Repair Migration](/database/category-repair-migration.md) — earlier canonical-six migration this seed depends on.