A modern, open-source platform for running Capture the Flag competitions —
self-hosted, real-time, and batteries-included.
Highlights · Features · Quick start · Deploy · Docs
Flagpost is a complete competition platform for CTF organisers: publish challenges, score solves the moment they land, support competitors, and automate the whole event — all from one self-hostable app. It's multi-tenant from the ground up (run many competitions from a single install), real-time throughout (WebSockets, not polling), and ships as a one-command production stack.
Password auth only for now; SSO/LDAP and an AI assistant are on the roadmap but deliberately not built yet.
The things that set Flagpost apart — every one of them built and working today:
- ⚡ Real-time everything. The scoreboard, "who's viewing this challenge" presence, notifications, and support-ticket threads all update live over WebSockets. No refreshing, no polling.
- 🤖 A visual automation engine. A no-code When → If → Then rule builder: on any event, run actions — notify, call a (SSRF-hardened) webhook, send email, release a hint, unlock a bonus challenge, open a survey, adjust scores, grant an award, freeze the board, or post an announcement. Includes time-based triggers like "an hour before the end, open the feedback survey."
- 📝 Live collaborative notes. True CRDT (Y.js) co-editing: a shared scratchpad per team on each challenge, and private staff notes on each ticket — everyone types at once, conflict-free.
- 🛡️ Permissions as data. RBAC that isn't hard-coded: a visual role editor lets you clone the built-ins and craft custom roles with granular, per-competition or site-wide scope.
- 🧩 A genuinely deep challenge model. Static, regex, and multiple-choice flags; dynamic (decay) scoring; prerequisite unlock chains; scheduled/waved release; tags & difficulty; and per-competition guess caps.
- 🏆 A scoreboard done right. Live standings with first-blood, parallel brackets/divisions, a freeze for the final stretch, a public spectator board, and a CTFtime feed so rated events just work.
- 🔁 CTFd-compatible & fully portable. Bulk challenge import/export in the ctfcli YAML format, plus a one-click, full-fidelity platform backup (export/import any section of your install).
- 🔒 Secure by default. argon2 hashing, a per-install auto-derived JWT secret (no shipped credentials — a first-run setup wizard creates your owner account), SSRF-hardened webhooks, ReDoS-contained regex flags, and timing-safe auth.
- 🚀 One command to production.
docker compose upbrings up the whole stack behind a Caddy reverse proxy on a single origin — with automatic HTTPS when you point it at a domain.
Note: these are placeholders. Real captures are on the way — the frames below mark exactly which screens go where (
docs/assets/screenshots/).
|
Competitions
Challenges
Scoring & scoreboard
|
Teams & participants
Communicate & collaborate
Automation, feedback & insight
Administration
|
Requires Docker with Compose. The default stack is production: built images behind a Caddy reverse proxy, with Postgres, Redis, and MinIO.
git clone https://github.com/tbcsec/flagpost.git
cd flagpost
docker compose up --buildOpen http://localhost:8080 and complete the one-time setup wizard to create your owner account. That's it — the app, its API, and its live WebSocket updates are all served same-origin through Caddy, so there's nothing else to configure for a local run.
A fresh install ships with no administrator and no default password. It's unconfigured until you complete the setup wizard, which creates the owner account and initial branding.
The default compose is the production stack, so going live is mostly
configuration in .env (copy .env.example):
| Variable | What it does |
|---|---|
SITE_ADDRESS |
Your domain, e.g. ctf.example.com. Caddy obtains & renews TLS automatically. Map ports 80 + 443. |
PUBLIC_ORIGIN |
The browser-facing origin, e.g. https://ctf.example.com. Baked into the frontend at build time — set it before docker compose build. |
JWT_SECRET |
A long random value (required for multi-host; otherwise the app derives and persists one). |
POSTGRES_PASSWORD, MINIO_ROOT_USER/PASSWORD |
Real credentials. |
MINIO_PUBLIC_ENDPOINT |
A browser-reachable MinIO host for signed attachment downloads. |
The backend runs as a single process by design (the WebSocket layer is
in-process). To run without Docker: build & serve the frontend with
npm run build && npm run start, and run the backend with alembic upgrade head
then uvicorn main:app (no --reload) behind your own TLS-terminating proxy.
The dev stack mounts source and runs hot-reloading dev servers:
docker compose -f docker-compose.dev.yml up --build
# frontend → http://localhost:3000 · backend → http://localhost:8000/docsOr run each side directly (backend needs Python 3.12 + a venv; frontend needs Node 20+):
# Backend
cd backend && python3 -m venv .venv
.venv/bin/pip install -r requirements-dev.txt
.venv/bin/alembic upgrade head # against a reachable Postgres
.venv/bin/uvicorn main:app --reload
# Frontend
cd frontend && npm install && npm run devRun the checks CI runs before opening a PR:
cd backend && .venv/bin/pytest # SQLite-backed, no infra needed
cd frontend && npm run test # vitest
cd frontend && npx tsc --noEmit && npx eslint .Backend — Python · FastAPI · SQLAlchemy 2 (async) · Alembic · PostgreSQL · Redis · MinIO/S3 · JWT + argon2 · a first-class async event bus. Frontend — TypeScript · Next.js 15 (App Router) · React 19 · TanStack Query · Zustand · Tailwind v4 · TipTap + Y.js (CRDT). Realtime — WebSockets throughout. Deploy — Docker Compose + Caddy.
| Doc | What's in it |
|---|---|
docs/VISION.md |
What Flagpost is and why it exists |
docs/ARCHITECTURE.md |
The binding technical design |
docs/ROADMAP.md |
Build order and what's next |
docs/adr/ |
Architecture Decision Records — why things are the way they are |
Contributions are welcome! Start with CONTRIBUTING.md for
setup, conventions, and the PR flow, and please follow the
Code of Conduct.
Found a security issue? Don't open a public issue — see
SECURITY.md for private disclosure.
Copyright © 2026 Tom Collier.
Flagpost is licensed under the GNU Affero General Public License v3.0. You're free to use, modify, and self-host it; if you run a modified version as a network service, the AGPL's §13 requires you to offer your users its source. The built-in "Powered by Flagpost" footer links every page to this repository, which is how Flagpost surfaces its source to remote users.