category, challenge, challenge_file, and solve tables — how CTF challenges and scoring are stored.
database
challenge
category
solve
2026-07-21T14:18: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 (added by AddCategoryTimestampsAndUniqueAbbrev1700000000200).
updated_at
TEXT
ISO 8601 timestamp the row was last updated (added by the same migration; bumped on PUT /api/v1/admin/categories/:id).
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 migration drops any legacy seeded rows that no
longer match the canonical set before inserting the missing entries.
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.
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.)