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