88 lines
3.9 KiB
Markdown
88 lines
3.9 KiB
Markdown
---
|
|
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-22T14:50:25Z
|
|
---
|
|
|
|
# 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`).
|
|
The initial migration declares every column used by the entities
|
|
(including `category.created_at` and `category.updated_at`), so the
|
|
later `AddCategoryTimestampsAndUniqueAbbrev1700000000200` and
|
|
`UpdateSystemCategoryKeys1700000000300` migrations only patch legacy
|
|
databases that pre-date those columns and reseed the canonical system
|
|
rows.
|
|
|
|
# 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)
|