Self-hosted management app for an art class — recurring weekly batches, ad-hoc drop-ins, and one-on-one private lessons (billed per session).
- Backend: FastAPI + PostgreSQL, JWT auth, role-based access. Seeds the first admin on boot.
- Frontend: React + Vite + Tailwind.
- Deploy: Docker Compose — postgres + a single app image (FastAPI serves the
built UI and the API together on one port; API lives under
/api).
| Role | Access |
|---|---|
| admin | Everything, including user management |
| staff | Everything except user accounts |
| parent | Read-only portal — only their linked children's sessions and attendance |
cp .env.example .env # then edit secrets
docker compose up --build- App (UI + API): http://localhost:8000
- API docs: http://localhost:8000/docs
- Log in with
ADMIN_EMAIL/ADMIN_PASSWORDfrom.env.
# Backend
cd backend
python -m venv .venv && . .venv/Scripts/activate # or source .venv/bin/activate
pip install -r requirements.txt
export DATABASE_URL="sqlite:///./dev.db" # or point at Postgres
uvicorn app.main:app --reload
# Frontend (separate terminal) — proxies /api -> http://localhost:8000
cd frontend
npm install
npm run devTrueNAS SCALE runs Docker workloads. Two common paths:
- Apps → Discover Apps → Custom App (or the "Install via YAML" option).
- Paste the contents of
docker-compose.yml. - Add the environment variables from
.env.example(set real secrets forPOSTGRES_PASSWORD,JWT_SECRET,ADMIN_PASSWORD). - Map a dataset to the
db_datavolume so the database survives upgrades (e.g./mnt/tank/apps/studio/db). - Deploy. Access the app (UI + API) on the host's IP at port
8000.
A single image is published to GHCR by the GitHub Action on every push to main:
ghcr.io/<owner>/<repo>:latest
Use the ready-made docker-compose.hub.yml (pulls it instead of building):
cp .env.example .env # then edit secrets
docker compose -f docker-compose.hub.yml up -dThe GitHub Action rebuilds and republishes :latest (and a :sha-… tag) on
every push to main — no manual push step needed.
- First boot auto-creates the admin from
ADMIN_EMAIL/ADMIN_PASSWORD. This only happens when there are zero users — change the password after first login. - Backups: snapshot the dataset backing
db_data. - HTTPS: put TrueNAS's built-in reverse proxy (or Traefik/nginx) in front of port 8000 for TLS.
Pluggable providers, all optional:
- Email (SMTP): works once
SMTP_HOSTetc. are set. A receipt is auto-emailed on payment when the student's guardian email is on file. - SMS (Twilio) / WhatsApp (Meta Business API): activate when their creds are
set in
.env; otherwise attempts are logged asdisabled(no error).
Fresh installs build the schema on startup via Base.metadata.create_all — zero
config, nothing to run. Alembic is wired in backend/ for schema changes:
cd backend
alembic revision --autogenerate -m "describe change" # after editing models
alembic upgrade head # apply (uses DATABASE_URL)The 0001_baseline revision recreates the current schema from the models, so it's
identical to create_all and works on both Postgres and SQLite. On an existing DB
created by create_all (no alembic_version table), run alembic stamp head once
before applying future migrations.
Set APP_ENV=prod in .env for real deployments: the app then refuses to boot
if JWT_SECRET or ADMIN_PASSWORD are still the insecure defaults. In split
deployments (UI and API on different origins) set CORS_ORIGINS to the UI origin;
the default single-image deploy serves both same-origin and needs nothing.