7.4 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | ||||
|---|---|---|---|---|---|---|---|---|
| database | Challenge Tables | category, challenge, challenge_file, and solve tables — how CTF challenges and scoring are stored. |
|
2026-07-22T20:30:00Z |
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. |
updated_at |
TEXT | ISO 8601 timestamp the row was last updated. |
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). The forward-only
RepairCategorySchemaAndSystemCategories1700000000400 migration
repairs legacy schemas on existing databases.
challenge
| Column | Type | Description |
|---|---|---|
id |
TEXT PK | UUID v4. |
name |
TEXT | Display name. Case-insensitive uniqueness enforced by uq_challenge_name_lower. |
description_md |
TEXT | Markdown description. |
category_id |
TEXT | FK → category.id (ON DELETE RESTRICT). |
difficulty |
TEXT | 'LOW' / 'MEDIUM' / 'HIGH'. |
initial_points |
INTEGER | Starting point value (1–100000). |
minimum_points |
INTEGER | Floor after decay (1–100000, must be ≤ initial_points). |
decay_solves |
INTEGER | Solves at which decay reaches the minimum (1–10000). |
flag |
TEXT | Submission string expected from players. Never returned in non-admin DTOs. |
protocol |
TEXT | 'NC' (netcat-style connection) or 'WEB' (browser-based). |
port |
INTEGER | TCP port (nullable for WEB, required 1–65535 for NC). |
ip_address |
TEXT | IP the challenge listens on (defaults to setting.defaultChallengeIp). |
enabled |
INTEGER | Boolean — 1 (default) or 0. |
created_at |
TEXT | ISO 8601 timestamp the row was created. |
updated_at |
TEXT | ISO 8601 timestamp the row was last updated (bumped on every PUT). |
The UpgradeChallengeAdminSchema1700000000500 migration rebuilt
the challenge table atomically (preserving legacy data), introduced
canonical enums, the enabled flag, updated_at, the
LOWER(name) unique index, and tighter scoring defaults. The
category FK stays at ON DELETE RESTRICT so the
CATEGORY_HAS_CHALLENGES business rule is preserved.
The SeedSampleChallenges1700000000600 migration inserts a small
representative set of sample challenges (one or more per canonical
system category) on a fresh database so the /challenges board has
cards immediately after bootstrap. It is idempotent: if the
challenge table already contains rows (e.g. an admin imported a
custom challenge set), it does nothing. Sample flags follow the
flag{<token>} convention and are exposed only inside admin views.
challenge_file
| Column | Type | Description |
|---|---|---|
id |
TEXT PK | UUID v4. |
challenge_id |
TEXT | FK → challenge.id (ON DELETE CASCADE). |
original_filename |
TEXT | Filename from upload. |
stored_filename |
TEXT | UUID-based filename on disk under UPLOAD_DIR/challenges/; unique. |
mime_type |
TEXT | Source MIME type (defaults to application/octet-stream). |
size_bytes |
INTEGER | Decoded file size in bytes. |
created_at |
TEXT | ISO 8601 timestamp. |
The upgrade migration added stored_filename, mime_type,
size_bytes, and created_at. The challenge → file FK was changed
from RESTRICT to CASCADE so deleting a challenge in one DB
transaction removes all its ChallengeFile rows in lockstep.
solve
| Column | Type | Description |
|---|---|---|
id |
TEXT PK | UUID v4. |
challenge_id |
TEXT | FK → challenge.id (ON DELETE CASCADE). |
user_id |
TEXT | FK → user.id (ON DELETE RESTRICT). |
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). |
is_first |
INTEGER | Boolean — first-blood flag. |
is_second |
INTEGER | Boolean — second-blood flag. |
is_third |
INTEGER | Boolean — third-blood flag. |
uq_solve_challenge_user on (challenge_id, user_id) is retained;
deleting a challenge now cascades to its solve rows, while deleting
a user still requires explicit handling (user_id remains
RESTRICT).
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.
See also
- Database Schema Overview
- System Endpoints (SSE scoreboard stream)
- Scoreboard Stream
- Admin — Challenges