Learn PostgreSQL by escaping a database prison.
EscapeQL is a Interactive, story-driven SQL escape room where the database is the Game. Players take on the role of a "prisoner" and must solve increasingly complex PostgreSQL puzzles accross five themed rooms to escape.
Built as a teaching tool to make learning advanced database concepts fun, hands-on, and memorable.
You wake up inside the escape_room database with almost no privileges.
Your only tools are web-based SQL terminal and your knowledge of PostgreSQL.
Rooms & Concepts Taught:
| Room | Theme | Core Concepts taught |
|---|---|---|
| Loby | NULL Puzzle | Null Handeling, column comments, metadata queries |
| Corridor | Broken View | Views, pg_views, relational algebra, base tables |
| Vault | Normalizatoin | 2NF violation, functional dependencies, transactions |
| Server Room | Trigger Debugging | Triggers, pg_trigger, debugging, concurrency (SKIP_LOCKED) |
| Escape | Encryption | pgcrypto, pgp_sym_decrypt, key assembly |
- Fully Functional web frontend with SQL terminal
- Real PostgreSQL backend with multiple schemas acting as "rooms"
- Role-based security (
warden,prisoner) - Secure answer validation using hashed passwords
- Progress tracking, fragments collections, and leaderboard
- Safe query executor (blocks dangerous commands)
- Responsive Pixelated/Terminal/Game aesthetic
- Complete reset script for easy replay
- Database: PostgreSQL 16
- Backend: Node.js + Express + pg
- Frontend: Vanilla HTML, CSS, JavaScript
- Authentication: Session-based
- Security: Row-Level Security (RLS),
SECURITY DEFINERfunctions, parameterized queries
Follow these steps to run the full stack locally (PostgreSQL + backend + frontend):
-
Install prerequisites:
- PostgreSQL 16 (or compatible)
- Node.js 18+ and npm
-
Create the database and run the SQL setup scripts (adjust
postgresuser as needed):
# create database (run as a postgres superuser)
createdb escapeql
# run setup scripts in order
psql -d escapeql -f database/00_setup.sql
psql -d escapeql -f database/01_warden_schema.sql
psql -d escapeql -f database/02_lobby.sql
psql -d escapeql -f database/03_corridor.sql
psql -d escapeql -f database/04_vault.sql
psql -d escapeql -f database/05_server_room.sql
psql -d escapeql -f database/06_escape.sql
psql -d escapeql -f database/07_permissions.sql- Configure backend environment:
- Copy
.env.example(if present) or create a.envfile insidebackend/with at least these values:
- Copy
DATABASE_URL=postgres://<dbuser>:<dbpassword>@localhost:5432/escapeql
SESSION_SECRET=some_long_random_string
PORT=3001- Install and run the backend:
cd backend
npm install
# start server (serves static frontend and API)
npm start
# or during development
npm run dev- Open the game in your browser:
- Navigate to http://localhost:3001/ — the backend serves the frontend and the API.
Notes and troubleshooting:
- If you open the frontend files directly via
file://, cookies/sessions may not work. Run the backend so session cookies are properly set (http://localhost:3001). - If the API requests fail due to CORS or credentials, ensure
PORTandDATABASE_URLmatch and the backend is running. - To reset the game state, use
database/reset.sqlor the backend reset endpoints (warden UI) depending on your setup.
This section provides a step-by-step guide for the prisoner role to solve each room. It assumes the prisoner account has restricted privileges (mostly SELECT on specific tables/views) and that destructive commands (DDL/DML like DROP, ALTER, or UPDATE) are blocked by the game executor. Use only SELECT, LIMIT, ORDER BY, JOIN, simple string functions and WHERE filters. Examples below are written to be copy/pasted into the in-game SQL terminal.
General tips for the prisoner role
- Use
information_schemato discover accessible tables and columns:SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'; - Inspect column names:
SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'doors'; - Use
ILIKEfor case-insensitive matching and%wildcards to search text:WHERE name ILIKE '%corridor%'. - Use
LIMIT 1when you only need a single value. - If a column returns
NULL, tryIS NOT NULLorCOALESCE(col, '<none>'). - If views are present and look empty, fetch the view definition to see underlying tables:
SELECT view_definition FROM information_schema.views WHERE table_name = 'my_view';
Room: Lobby (Null handling & metadata)
- Goal: Find the corridor keycode stored in the
doorstable or related metadata. - Restrictions: You may be able to
SELECTfromdoorsbut some columns can beNULLor masked.
Steps & Example queries:
- List tables you can access:
SELECT table_name FROM information_schema.tables WHERE table_schema='public' ORDER BY table_name;- Inspect columns for
doors(replace table name if different):
SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'doors';- Query for the corridor code (common pattern):
SELECT code FROM doors WHERE name ILIKE '%corridor%' AND code IS NOT NULL LIMIT 1;- If result is NULL or split across rows, try searching comments or metadata columns:
SELECT * FROM doors WHERE description ILIKE '%corridor%' OR notes ILIKE '%corridor%';Room: Corridor (Views, filtering, fragment discovery)
- Goal: Identify encrypted fragments in the
keys(or similar) table and extractfragment_codevalues. - Restrictions: The prisoner may only have access to a view rather than the underlying table; use
information_schema.viewsto inspect view SQL.
Steps & Example queries:
- See if a
keystable or view exists:
SELECT table_name, table_type FROM information_schema.tables WHERE table_name ILIKE '%key%' OR table_name ILIKE '%keys%';- If
keysis a view and empty, inspect its definition:
SELECT view_definition FROM information_schema.views WHERE table_name = 'keys';- Query the fragments you can see (typical):
SELECT fragment_code, status, created_at FROM keys WHERE status = 'ENCRYPTED' ORDER BY created_at DESC LIMIT 20;- If fragment codes are split or encoded, try string functions and casts:
-- If column is bytea
SELECT encode(fragment_code::bytea, 'hex') FROM keys WHERE status='ENCRYPTED' LIMIT 5;Room: Vault (Normalization, joins, and secret lookup)
- Goal: Locate the admin password fragment stored across normalized tables (e.g.,
employees,credentials). - Restrictions: Sensitive fields may be split across tables and require
JOINoperations; prisoner should be allowed to performSELECTwithJOIN.
Steps & Example queries:
- Find likely tables:
SELECT table_name FROM information_schema.tables WHERE table_name ILIKE '%employee%' OR table_name ILIKE '%cred%';- Inspect columns and then JOIN to combine fragments:
SELECT e.username, c.secret_fragment, e.clearance_level
FROM employees e
JOIN credentials c ON c.employee_id = e.id
WHERE e.clearance_level >= 5
ORDER BY e.clearance_level DESC LIMIT 10;- If the secret is split into parts across rows, aggregate them in order:
SELECT string_agg(part, '' ORDER BY part_index) AS full_fragment
FROM employee_fragments
WHERE employee_id = (SELECT id FROM employees WHERE username ILIKE '%admin%' LIMIT 1);Room: Server Room (Logs, ordering, and JSON metadata)
- Goal: Extract override PIN or code from
system_logsor JSON metadata fields. - Restrictions: Recent logs might be protected by RLS; use
ORDER BY timestamp DESCto prioritize recent error entries.
Steps & Example queries:
- Look for log tables:
SELECT table_name FROM information_schema.tables WHERE table_name ILIKE '%log%' OR table_name ILIKE '%system%';- Search messages for keywords (PIN, OVERRIDE, ERROR):
SELECT timestamp, message, metadata FROM system_logs WHERE message ILIKE '%pin%' OR message ILIKE '%override%' ORDER BY timestamp DESC LIMIT 20;- If metadata is JSON and the PIN is a key inside it:
SELECT metadata->>'override_pin' AS pin, timestamp FROM system_logs WHERE metadata ? 'override_pin' ORDER BY timestamp DESC LIMIT 5;Room: Escape (Assembling fragments & final key)
- Goal: Combine all collected fragments in the correct order to obtain the final decryption key.
- Restrictions: You may only have
SELECTaccess to thefragmentsorkeys_collectedtable; use ordering metadata to assemble.
Steps & Example queries:
- List fragments you have collected (or rows marked as
collected_by = current_prisoner):
SELECT fragment_code, fragment_index FROM fragments WHERE collected_by = 'prisoner_username' ORDER BY fragment_index;- Aggregate into a single key string:
SELECT string_agg(fragment_code, '' ORDER BY fragment_index) AS final_key FROM fragments WHERE collected_by = 'prisoner_username';- If fragments are encoded, decode or cast as needed before concatenation.
Submitting answers in the UI
- Use the in-game
ANSWER_SUBMISSIONbox (right under the terminal) to submit single answers for room unlocks. - For the final escape key use the
FINAL_DECRYPTION_KEYinput in the escape panel.
If any query returns permission errors
- The
prisonerrole intentionally lacks privileges on some tables. When you seepermission denied, try these approaches:- Query views instead of base tables (views are often given read access).
- Inspect
information_schema.viewsto find alternative object names. - Search for the same data in other tables (e.g.,
meta,notes,config).
If you'd like, I can now:
- Add these walkthroughs as a standalone
docs/PRISONER_GUIDE.mdfile, or - Expand each room with exact table/column names from your database files for a fully concrete solution.
escapeql/
├── database/ # All SQL setup files
│ ├── 00_setup.sql
│ ├── 01_warden_schema.sql
│ ├── 02_lobby.sql
│ ├── 03_corridor.sql
│ ├── 04_vault.sql
│ ├── 05_server_room.sql
│ ├── 06_escape.sql
│ ├── 07_permissions.sql
│ └── reset.sql
├── backend/
│ ├── server.js
│ ├── db.js
│ ├── routes/
│ └── package.json
├── frontend/
│ ├── index.html
│ ├── game.html
│ ├── css/
│ └── js/
└── README.mdThis project is excellent for teaching:
- Relational Database Design
- SQL Query writing & Optimization
- Database Security (Roles, Privileges, RLS)
- Transactions & Concurrency
- Triggers & Stored Procedures
- PostgreSQL System Catalogs(
pg_views,pg_triggers, etc.) - Normalization & Functional Dependencies
- Encryption with
pgcrypto
- Multiplayer support with real-time updates
- Docker Compose setup for easier deployment
- Admin dashboard for instructors
- Difficulty levels / hints system
- Score based on time and attempts
MIT License
Copyright (c) 2026 Misbah Ullah
Feel Free to Fork, modify, and use it in your classes or personal Learning
- Built as a fun way to teach advanced PostgreSql concepts
- Inspired by classic escape room games and SQL learning platforms
Made with ❤️ for database learners everywhere
Start Escaping
If you open the game directly from the filesystem, use the backend server at http://localhost:3001/ so registration and sessions work correctly.