--- type: database title: Challenge Tables description: category, challenge, challenge_file, and solve tables — how CTF challenges and scoring are stored. tags: [database, challenge, category, solve] timestamp: 2026-07-22T16:44:54Z --- # Tables ## `category` | Column | Type | Description | |----------------|---------|------------------------------------------------------------------------------| | `id` | TEXT PK | UUID v4. | | `system_key` | TEXT | Non-null for system categories (e.g. `CRY`, `MSC`, `PWN`, `REV`, `WEB`, `HW`); unique where NOT NULL; `NULL` for user-created categories. | | `name` | TEXT | Display name (e.g. `Cryptography`). | | `abbreviation` | TEXT | Short label (e.g. `CRY`), 2–6 chars, server-uppercased. Globally unique — enforced by `uq_category_abbreviation`. | | `description` | TEXT | Optional description. | | `icon_path` | TEXT | Default icon URL (e.g. `/uploads/icons/CRY.png`). | | `created_at` | TEXT | ISO 8601 timestamp the row was created. Declared in the initial `InitSchema1700000000000` migration (defaulting to `strftime('%Y-%m-%dT%H:%M:%fZ','now')`); the `AddCategoryTimestampsAndUniqueAbbrev1700000000200` migration adds it as a no-op for older pre-existing databases. | | `updated_at` | TEXT | ISO 8601 timestamp the row was last updated (bumped on `PUT /api/v1/admin/categories/:id`). Same provenance as `created_at`. | System rows are seeded by the `UpdateSystemCategoryKeys1700000000300` migration so the canonical keys are `CRY` (Cryptography), `MSC` (Misc), `PWN` (Binary exploitation), `REV` (Reverse Engineering), `WEB` (Web), and `HW` (Hardware). A forward-only repair migration, `RepairCategorySchemaAndSystemCategories1700000000400`, runs after that one on existing databases that pre-date the canonical six keys (it adds `created_at`/`updated_at` if missing, deletes obsolete `FOR`/`OSI` system rows, rewrites name/abbreviation/description/icon metadata on canonical rows, inserts any missing canonical row, and guarantees unique indexes on `system_key` and `abbreviation`). User-created categories leave `system_key` as `NULL`. The uniqueness on `abbreviation` is enforced both by application validation (uppercased on save, duplicates rejected) and by the `uq_category_abbreviation` index created in migration `1700000000200` (re-asserted by `1700000000400`). ## `challenge` | Column | Type | Description | |-------------------|----------|-----------------------------------------------------------------------------------| | `id` | TEXT PK | UUID v4. | | `name` | TEXT | Display name. | | `description_md` | TEXT | Markdown description. | | `category_id` | TEXT | FK → `category.id`. | | `difficulty` | TEXT | `'low'` / `'med'` / `'high'`. | | `initial_points` | INTEGER | Starting point value. | | `minimum_points` | INTEGER | Floor after decay. | | `decay_solves` | INTEGER | Number of solves at which decay reaches the minimum. | | `flag` | TEXT | Submission string expected from players. | | `protocol` | TEXT | `'nc'` (netcat-style connection) or `'web'` (browser-based). | | `port` | INTEGER | TCP port when relevant (nullable). | | `ip_address` | TEXT | IP the challenge listens on (defaulted from `setting.defaultChallengeIp`). | | `created_at` | TEXT | ISO 8601 timestamp. | ## `challenge_file` | Column | Type | Description | |---------------------|---------|--------------------------------------| | `id` | TEXT PK | UUID v4. | | `challenge_id` | TEXT | FK → `challenge.id`. | | `original_filename` | TEXT | Filename from upload. | | `stored_path` | TEXT | Path on disk under `UPLOAD_DIR/challenges/`. | ## `solve` | Column | Type | Description | |------------------|---------|----------------------------------------------------------------------| | `id` | TEXT PK | UUID v4. | | `challenge_id` | TEXT | FK → `challenge.id`. | | `user_id` | TEXT | FK → `user.id`. | | `solved_at` | TEXT | ISO 8601 timestamp the solve was recorded. | | `points_awarded` | INTEGER | Points actually awarded (after decay). | | `base_points` | INTEGER | Snapshot of `challenge.initial_points` at solve time. | | `rank_bonus` | INTEGER | Extra points from rank (e.g. first-blood). | Uniqueness is enforced by `uq_solve_challenge_user` on `(challenge_id, user_id)` — a user can solve a given challenge only once. # Scoring formula `points_awarded = clamp(initial_points - decay, minimum_points, initial_points)` where `decay = max(0, base_points - minimum_points) * solvesBefore / decay_solves`, and additional `rank_bonus` may be added for early solves. (The exact scoring algorithm lives in the future solve-recording service and is consumed by `SseHubService` whenever a new solve is published.) # See also - [Database Schema Overview](/database/schema.md) - [System Endpoints](/api/system.md) (SSE scoreboard stream) - [Scoreboard Stream](/guides/scoreboard-stream.md)