Administration#

This app has no authentication. It is local-first: one container, one SQLite file, one machine. Everyone who can reach the port can do everything. That is a deliberate trade — it keeps the app simple and it is the right shape for something running on your own laptop or on a home LAN.

Do not put this on the public internet as-is

There is no login, no rate limiting and no CSRF token on the JSON API. If you need it reachable from outside your network, put it behind a reverse proxy that does the authentication (basic auth is enough) and terminate TLS there. See Installation.

Profiles#

A profile is a student. It owns assessments, attempts, solves, stars, focus items, drill sessions, plan progress and the cached scores — everything except the algorithm library, which is shared.

The first boot creates a default profile called Me with the slug me and the goal sub_20. The active profile lives in the session cookie, so switching profiles is per browser, not per machine.

Managing profiles#

From the profile chip in the header:

  • Switch — pick another profile. Takes effect immediately.
  • New — name and goal. The slug is derived from the name and de-duplicated (alex, alex-2, …).
  • Rename — changes the display name; the slug stays put so links and CLI commands keep working.
  • Goal — one of the keys under targets.splits. This selects the split targets every score is measured against, so a coach can keep a sub-30 and a sub-12 student on one machine and both see sensible colours.
  • Delete — removes the profile and everything it owns. The default profile cannot be deleted; promote another one first.

Using profiles for coaching#

One profile per student is the obvious use. Two less obvious ones:

  • A scratch profile. Handy when you want to poke at the API console or try a plan without polluting real data.
  • A "cold" profile. Some coaches keep a second profile per student with no assessments at all, to re-measure recognition from scratch every few months.

Resetting student data#

Two levels, both destructive, both irreversible without a backup.

Reset a profile keeps the profile and deletes every row it owns — assessments, attempts, solves, stars, focus, plan progress, cached scores and drill sessions.

docker compose run --rm web flask reset-profile me

There is no --force; the command is short and explicit on purpose. The same action is available from the profile page with a confirmation dialog.

Reset everything means throwing away the database. Stop the app, remove the volume, start it again:

docker compose down
docker volume rm rubiks-trainer_rt-data
docker compose up -d

The library is re-imported and a fresh default profile is created on first boot.

Backups#

Everything a student owns is in one SQLite file on the rt-data volume, at /data/rubiks-trainer.db by default.

Taking a backup#

Use SQLite's own backup command rather than copying the file, so you get a consistent snapshot even while the app is running:

docker compose exec web \
  python -c "import sqlite3,sys; \
    src=sqlite3.connect('/data/rubiks-trainer.db'); \
    dst=sqlite3.connect('/data/backup.db'); \
    src.backup(dst); dst.close(); src.close()"

docker compose cp web:/data/backup.db ./rubiks-trainer-$(date +%F).db

Restoring#

Stop the app first — restoring under a running server will corrupt the open connection's view of the file.

docker compose down
docker compose cp ./rubiks-trainer-2026-08-12.db web:/data/rubiks-trainer.db
docker compose up -d

What to keep#

File Why
/data/rubiks-trainer.db All student data. This is the one that matters
app/config/config.yml Your targets, scrambler settings and colours
.env Port, secret key, database URL

The algorithm library and the built-in plans are re-created from app/data/algorithms.json on every boot, so they do not need backing up.

If you use Postgres

Point RT_DATABASE_URL at Postgres and back it up with pg_dump like any other database. The schema is created automatically on first boot.

Seeding and updating the library#

The algorithm library is imported from app/data/algorithms.json at startup if the database is empty. To re-import after the data file changes:

docker compose run --rm web flask seed

By default this is additive and idempotent — existing cases are matched by key and updated, new ones are inserted. To wipe the library and rebuild it from the file:

docker compose run --rm web flask seed --force

Student data is keyed on the case row, so --force will orphan attempts and assessments if case keys change. Take a backup first, and prefer the plain seed unless you know keys were renamed.

Regenerating the data file from the PDFs#

The library is extracted from the algorithm sheets in pdf/:

docker compose run --rm web python scripts/extract_algorithms.py

This rewrites app/data/algorithms.json and validates every case — the setup moves followed by the primary algorithm must return the cube to the phase's target state. Cases that fail are reported and the script exits non-zero. See Contributing before you commit the result.

Upgrading#

See Installation → Upgrading. In short: pull, rebuild, restart. The schema is created with create_all() and there is no migration framework, so a release that changes a column requires a note in the changelog and, in the worst case, a rebuild from a backup.

Health checks and monitoring#

GET /healthz returns {"status": "ok", "version": "..."} and is what the Compose health check calls every 30 seconds:

curl -s http://localhost:8080/healthz

Application logs go to stdout and are visible with docker compose logs -f web. Gunicorn writes both access and error logs there.

Configuration#

Every key in config.yml and every RT_* environment variable is documented in Configuration.

Source: docs/admin/index.md