category, challenge, challenge_file, and solve tables — how CTF challenges and scoring are stored.
database
challenge
category
solve
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.
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.