--- 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-21T14:18:00Z --- # Tables ## `category` | Column | Type | Description | |----------------|---------|------------------------------------------------------------------------------| | `id` | TEXT PK | UUID v4. | | `system_key` | TEXT | One of `crypto`, `forensics`, `pwn`, `web`, `misc`, `osint` (unique where NOT NULL) or `NULL` for user-created categories. | | `name` | TEXT | Display name (e.g. `Cryptography`). | | `abbreviation` | TEXT | Short label (e.g. `CRY`). | | `description` | TEXT | Optional description. | | `icon_path` | TEXT | Default icon URL (e.g. `/uploads/icons/crypto.svg`). | The seed migration inserts one row per `SYSTEM_CATEGORY_KEYS` value from `backend/src/config/env.schema.ts`. ## `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)