dumbtv
dumbtv takes the ads, screen tracking and update nags out of your Fire TV over ADB. No root, none of your apps touched, every step can be undone. Free and open source.
翻譯準備中
A modern, open-source platform for running Capture the Flag competitions — self-hosted, real-time, and batteries-included.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗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, automate the whole event, and — when it wraps — hand out certificates and generate a branded post-event report. It's multi-tenant from the ground up (run many competitions from a single install), real-time throughout (WebSockets, not polling), translatable (ships in English, French, Spanish & Polish), and deploys as a one-command production stack.
Sign-in is local (username + optional email) or an external identity provider — OIDC & OAuth 2.0 (one-click Google, Microsoft, GitHub & Discord presets, or Okta, Keycloak, Entra, or anything with a discovery document), SAML 2.0, or LDAP / Active Directory. Optional AI assistants (administrator + competitor) plug into an OpenAI-compatible provider you configure — off by default until you enable them.
The things that set Flagpost apart — every one of them built and working today:
Competitions
Challenges
host:port) for challenges that run a live serviceScoring & scoreboard
Certificates & wrap-up
Teams & participants
Communicate & collaborate
Automation, feedback & insight
Administration
/p/…, public or members-only/metrics for observability/changelog, linked from
the footerRequires Docker with Compose. The default stack is production: built images behind a Caddy reverse proxy, with Postgres, Redis, and RustFS (S3-compatible object storage).
git clone https://github.com/tbcsec/flagpost.git
cd flagpost
docker compose up --build
Open 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). (Want a demo instance that
resets to your own configured baseline on a schedule? See
docs/INTERNAL_DEMO.md.)
| 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. Also what OIDC redirect URIs are built from, so it must be exact if you configure SSO. |
JWT_SECRET | A long random value (required for multi-host; otherwise the app derives and persists one). |
POSTGRES_PASSWORD, S3_ACCESS_KEY/S3_SECRET_KEY | Real credentials — generate with openssl rand -hex 24. Compose falls back to well-known development values so a local run needs no config, but those are published defaults, not secrets. |
S3_PUBLIC_ENDPOINT | A browser-reachable object-storage host for signed attachment downloads. |
The backend refuses to start if it finds the object store's default credentials on a deployment that looks reachable —
PUBLIC_ORIGINnaming a non-local host, orS3_PUBLIC_ENDPOINTbeing set. Browsers fetch attachments straight from the S3 API, so it has to be reachable, and default credentials there mean world read/write on every challenge attachment — including unreleased ones — outside RBAC entirely. To rotate the pair, changeS3_ACCESS_KEY/S3_SECRET_KEYand rundocker compose up -d --force-recreate rustfs backend: RustFS reads the pair from its environment on every start, so that is the rotation — the old pair is rejected immediately and there is nothing to do inside the server.Upgrading from v1.6 or earlier: the object store is now RustFS (ADR-0041 — MinIO was withdrawn from distribution) and the variables were renamed as a hard break:
MINIO_ROOT_USER→S3_ACCESS_KEY,MINIO_ROOT_PASSWORD→S3_SECRET_KEY,MINIO_PUBLIC_ENDPOINT→S3_PUBLIC_ENDPOINT,MINIO_ENDPOINT→S3_ENDPOINT,MINIO_REGION→S3_REGION,MINIO_IAM_AUTH→S3_IAM_AUTH. The backend refuses to start while anyMINIO_*variable is still set and prints this mapping, so a stale.envfails safe instead of silently falling back to default credentials. Do the upgrade in this order:
- With the old stack still running, copy the objects out:
rclone syncfrom the oldminio-dataserver to a scratch location (or directly to the new server once it's up) — they are not migrated automatically.docker compose down --remove-orphans— the oldminiocontainer is no longer in the compose file, so a plainup -dwould leave it running and holding port 9000, andrustfswould fail to bind.- Rename the variables in
.env, thendocker compose up -d.- Sync the objects into the new
rustfs-dataserver, then remove the old volume (docker volume rm <project>_minio-data).
The backend runs as a single process by default (in-process WebSocket
broadcast, no Redis required). For larger events it scales out to multiple
workers — set WEB_CONCURRENCY>1 and the real-time layer switches to a
Redis-backed cross-worker relay (a startup guard refuses to boot multi-worker
without Redis) — and beyond one box to multiple instances behind a load
balancer (the AWS ECS/Fargate + ALB + S3 + RDS + ElastiCache topology is a
supported target). 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 --ws-max-size 2097152 (no --reload) behind your own
TLS-terminating proxy.
Every release tag publishes pinned, reproducible images to GHCR — pick a tag from Releases:
ghcr.io/tbcsec/flagpost-backend:vX.Y.Z (also :latest)
ghcr.io/tbcsec/flagpost-frontend:vX.Y.Z (also :latest)
The release frontend is built in same-origin mode — API calls and
WebSockets resolve against whatever origin serves the page — so one image works
behind any single-origin proxy with no baked-in domain and no PUBLIC_ORIGIN
rebuild. Point the compose frontend/backend services at these images (a
two-line override) to upgrade by tag instead of rebuilding from source.
A release image reports its exact tag as the running version; a build from
source reports the release it's based on with an -src suffix (e.g.
1.4.0-src), since main starts accumulating the next version the moment a tag
is cut.
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/docs
Or run each side directly (backend needs Python 3.12+ and a venv; frontend needs Node 20+ — the shipped images run 3.14 and 26, which is what CI tests):
# 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 --ws-max-size 2097152
# Frontend
cd frontend && npm install && npm run dev
Run the checks CI runs before opening a PR:
cd backend && .venv/bin/pytest # SQLite-backed, no infra needed
cd frontend && npx tsc --noEmit && npx eslint .
cd frontend && npm run test # vitest
cd frontend && npm run build # catches prerender-only failures
CI also runs alembic upgrade head against a real PostgreSQL, because the test
suite builds its schema from the models rather than by running migrations
(ADR-0006) — so a broken migration only shows up there. Run the production stack
once before shipping one.
Backend — Python · FastAPI · SQLAlchemy 2 (async) · Alembic · PostgreSQL · Redis · RustFS/S3 · JWT + argon2 + OIDC/OAuth2/SAML/LDAP · WeasyPrint + Pillow (reports & certificates) · a first-class async event bus. Frontend — TypeScript · Next.js 16 (App Router) · React 19 · TanStack Query · Zustand · Tailwind v4 · TipTap + Y.js (CRDT) · next-intl (i18n). Realtime — WebSockets throughout. Deploy — Docker Compose + Caddy, or multi-instance behind a load balancer (Fargate/ALB).
| 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.
Your competitions, users and submissions never leave your infrastructure. The only thing Flagpost sends out is a once-daily update check that carries your version number and nothing else — no identifier, no hostname, no user data. The count of those requests is how the project gauges how many deployments are live.
Turn it off in Admin → Site settings, or set UPDATE_CHECK_URL="" to make sure
the call is never attempted at all. Full detail in PRIVACY.md.
Copyright © 2026 Tom Collier.
Flagpost is licensed under the Apache License 2.0 (from v1.5.1 — earlier releases were published under AGPL-3.0 and remain available on those terms). You're free to use, modify, self-host, and build on it, commercially or otherwise, with the license's explicit patent grant. The name "Flagpost" and the project marks are not part of the grant (License §6). The built-in "Powered by Flagpost" footer links every page back to this repository.
docker compose up brings up the whole stack
behind a Caddy reverse proxy on a single origin — with automatic HTTPS when you
point it at a domain.