References sites table updated so we can filter by site later, added timestamp to tunes so they can be sorted in certain contexts, enforced uniqueness on some tables that were causing issues |
||
|---|---|---|
| ain | ||
| app | ||
| docker | ||
| migrations | ||
| nixos | ||
| tests | ||
| .gitignore | ||
| alembic.ini | ||
| env.example | ||
| Makefile | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
| wsgi.py | ||
repertory-api
Flask + SQLAlchemy REST API for the Repertory tune-tracking database.
Stack
- Flask 3 – HTTP API
- SQLAlchemy 2 / Flask-SQLAlchemy – ORM
- PostgreSQL – database (
tunes) - Alembic – migrations
- uv – dependency management
- Docker Compose – local development
- NixOS systemd module – production deployment
Local development
First time setup
make local
This will:
- Copy
env.example→.env(skipped if.envalready exists) - Start Postgres via Docker Compose
- Wait for Postgres to be ready
- Run Alembic migrations
- Start the API container
At the end it prints the API URL and your local API key:
✅ Local environment is ready.
API: http://localhost:5000
API key: dev-secret-key-change-me
The API key is set in .env as API_KEY. Change it there if you want — it only affects your local dev environment. Production keys are managed via SOPS (see below).
Subsequent runs
make local # idempotent — safe to re-run, skips what's already done
make stop # stop containers (data preserved)
make reset # stop containers AND wipe the database volume (fresh slate)
Other useful commands
make test # run the test suite (SQLite in-memory, no Docker required)
make migrate # run pending Alembic migrations against the local DB
make logs # tail Docker Compose logs
make shell # open a psql shell in the running Postgres container
Run make or make help to see all available targets.
Adding a schema migration
After changing a model in app/models.py:
# With the local DB running:
uv run alembic revision --autogenerate -m "describe your change"
make migrate
Commit both the model change and the new file in migrations/versions/.
API Endpoints
Health
| Method | Path | Description |
|---|---|---|
| GET | /health |
Health check |
Tunes
| Method | Path | Description |
|---|---|---|
| GET | /tunes/ |
List all tunes (supports query filters — see below) |
| GET | /tunes/<id> |
Get a single tune with all sub-records |
| POST | /tunes/ |
Create a tune (optionally with nested banjo/fiddle/etc.) |
| PATCH | /tunes/<id> |
Update tune core fields |
| DELETE | /tunes/<id> |
Delete a tune (cascades to all sub-records) |
GET /tunes/ query params (all optional, combinable):
| Param | Example | Notes |
|---|---|---|
search |
?search=cluck |
Substring match on name |
key |
?key=D |
Exact key match (case-insensitive) |
instrument |
?instrument=fiddle |
Filter by instrument |
tuning |
?tuning=cross |
Substring match on tuning |
callable |
?callable=true |
Boolean |
review |
?review=false |
Boolean |
to_learn |
?to_learn=true |
Boolean |
difficulty |
?difficulty=easy |
"easy" or "hard" |
instrument must be set for tuning, callable, review, to_learn, and difficulty filters to apply.
Instrument sub-records (banjo / fiddle)
| Method | Path | Description |
|---|---|---|
| PUT | /tunes/<id>/banjo |
Create or fully replace banjo entry |
| PUT | /tunes/<id>/fiddle |
Create or fully replace fiddle entry |
| PATCH | /tunes/<id>/banjo |
Partially update banjo entry |
| PATCH | /tunes/<id>/fiddle |
Partially update fiddle entry |
| DELETE | /tunes/<id>/banjo |
Remove banjo entry |
| DELETE | /tunes/<id>/fiddle |
Remove fiddle entry |
Instrument fields: learned_from, date_learned (ISO date string), tuning, callable (bool), review (bool), to_learn (bool), difficulty ("easy"/"hard").
Notes
| Method | Path | Description |
|---|---|---|
| GET | /tunes/<id>/notes |
List notes |
| POST | /tunes/<id>/notes |
Add a note |
| PATCH | /tunes/<id>/notes/<note_id> |
Update a note |
| DELETE | /tunes/<id>/notes/<note_id> |
Delete a note |
References
| Method | Path | Description |
|---|---|---|
| GET | /tunes/<id>/references |
List references |
| POST | /tunes/<id>/references |
Add a reference |
| PATCH | /tunes/<id>/references/<ref_id> |
Update a reference |
| DELETE | /tunes/<id>/references/<ref_id> |
Delete a reference |
| POST | /tunes/<id>/references/<ref_id>/musicians |
Add a musician tag |
| DELETE | /tunes/<id>/references/<ref_id>/musicians/<name> |
Remove a musician |
Production deployment (NixOS + SOPS + age)
The NixOS module in nixos/module.nix fetches the application source directly from your Forgejo instance at a pinned tag, builds the Python environment, and manages the service under systemd — no manual file copying required.
1. Add secrets to your SOPS file
repertory-api-key: "your-api-key-here"
repertory-db-url: "postgresql://user:pass@host:5432/tunes"
2. Wire up the module in configuration.nix
imports = [ (fetchTarball {
url = "https://git.yourdomain.com/yourusername/repertory-api/archive/v1.0.0.tar.gz";
sha256 = "0000000000000000000000000000000000000000000000000000";
} + "/nixos/module.nix") ];
sops.secrets."repertory-api-key" = {};
sops.secrets."repertory-db-url" = {};
services.repertory-api = {
enable = true;
domain = "git.yourdomain.com";
owner = "yourusername";
version = "v1.0.0";
rev = "abc123..."; # git commit SHA for the tag
sha256 = "sha256-..."; # see below
};
3. Deploy
nixos-rebuild switch
On start, the service automatically runs alembic upgrade head before Flask comes up — so migrations are applied on every deploy with no manual step.
Updating to a new version
- Push a new tag to Forgejo (e.g.
v1.1.0) - Get the new sha256:
nix-prefetch-url --unpack \ https://git.yourdomain.com/yourusername/repertory-api/archive/v1.1.0.tar.gz - Update
version,rev, andsha256inconfiguration.nix nixos-rebuild switch— Nix fetches the new source, rebuilds the Python env, restarts the service, and runs any new migrations
How secrets are handled
The preStart script reads the SOPS-decrypted secret files (written to /run/secrets/ by sops-nix) and assembles them into a mode-600 env file at /run/repertory-api/env on tmpfs. The Flask process receives API_KEY and DATABASE_URL from that file. Nothing is written to disk in plaintext.
Database schema
musicians (id, name)
sources (id, name)
tunings (id, name)
instruments (id, name)
tunes (id, name, key, modal, source_id → sources.id)
├── tune_by_instrument (id, tune_id, instrument_id, learned_from_id, tuning_id, date_learned, callable, review, to_learn, difficulty)
│ └── tune_by_instrument_notes (id, tune_by_instrument_id, note)
├── tune_notes (id, tune_id, note)
└── references (id, tune_id, link, site)
└── reference_musicians (reference_id, musician_id) ← join table
To add a new instrument (e.g. harmonica): add a model in app/models.py mirroring Banjo/Fiddle, register the name in INSTRUMENT_MODELS in app/routes_instruments.py, then uv run alembic revision --autogenerate -m "add harmonica" and make migrate.