--- type: database title: Database Schema Overview description: SQLite (better-sqlite3) schema for HIPCTF: tables, relationships, indexes, and WAL journal mode. tags: [database, sqlite, typeorm, schema] timestamp: 2026-07-21T14:18:00Z --- # Overview HIPCTF uses **SQLite via better-sqlite3** with TypeORM migrations. The database is a single file (`DATABASE_PATH`, default `/data/hipctf/db.sqlite`) running in **WAL journal mode** for better concurrent read/write performance. The schema is created by a single migration (`backend/src/database/migrations/1700000000000-InitSchema.ts`) and seeded by a second migration (`backend/src/database/migrations/1700000000100-SeedSystemData.ts`). # Tables | Table | Purpose | |-------------------|-------------------------------------------------------------| | `user` | Authenticated users with role + status. | | `setting` | Key/value site configuration. | | `category` | Challenge categories (system + user-defined). | | `challenge` | CTF challenges with scoring + protocol metadata. | | `challenge_file` | Files attached to challenges. | | `solve` | User-solved challenges (scoring ledger). | | `refresh_token` | Rotating refresh tokens (hash + revocation). | | `blog_post` | Markdown blog posts in draft/published states. | # Relationships ``` user ───< solve >─── challenge │ └─── challenge_file >── challenge ──> category user ───< refresh_token ``` * `challenge.category_id` → `category.id` (`ON DELETE RESTRICT`). * `challenge_file.challenge_id` → `challenge.id` (`ON DELETE RESTRICT`). * `solve.challenge_id` → `challenge.id` (`ON DELETE RESTRICT`). * `solve.user_id` → `user.id` (`ON DELETE RESTRICT`). # Indexes | Table | Index | |------------------|----------------------------------------------------| | `category` | `uq_category_system_key` (unique on `system_key` where NOT NULL) | | `challenge` | `idx_challenge_category` | | `challenge_file` | `idx_challenge_file_challenge` | | `solve` | `uq_solve_challenge_user` (unique), `idx_solve_user`, `idx_solve_challenge` | | `refresh_token` | `idx_refresh_token_user`, unique on `token_hash` | | `blog_post` | `idx_blog_status_published` (`status`, `published_at`) | # PRAGMAs | PRAGMA | Where set | Effect | |--------------------|----------------------------------------------------|---------------------------------| | `foreign_keys = ON`| Migration `InitSchema1700000000000` | Enforce FK constraints. | | `journal_mode = WAL`| Migration + `DatabaseInitService.ensureWalMode()` on every startup | Persisted in DB header; allows concurrent readers. | # Bootstrap behavior On every process start, `DatabaseInitService.init()`: 1. Initializes the TypeORM `DataSource` (idempotent). 2. Calls `ensureWalMode()` — idempotent; if WAL is already set it only logs. 3. Runs pending migrations if any (or if the `category` table is missing). 4. Logs the seed row counts (categories, settings) via `verifySeed()`. # See also - [User Table](/database/users.md) - [Challenge Tables](/database/challenges.md) - [Auth and Settings Tables](/database/auth-settings.md) - [Blog Post Table](/database/blog-posts.md)