Everything needed to run, deploy and maintain the Service.
| Piece | Where |
|---|---|
| Frontend (React SPA) | Vercel — app-metadata.hardbanrecordslab.online |
| Backend (FastAPI) | VPS 84.247.162.167, Docker container metadata-backend, host 127.0.0.1:8888 → container 7860 |
| Public API | metadata.hardbanrecordslab.online/api (Nginx → the container) |
| Database | PostgreSQL metadata_engine in the shared hbrl-postgres container (external Docker network hbrl-db); DATABASE_URL lives only in the VPS .env |
| Repo path on VPS | /srv/hbrl/Metadata-Engine |
Vercel's frontend/vercel.json proxies /api/* to the VPS and rewrites
everything else to index.html.
The container also lives among other services on that box (WordPress, AzuraCast, a CMLP platform, Vault, Postgres instances). This app uses the
metadata_enginedatabase of the sharedhbrl-postgresinstance (migrated from SQLite on 2026-09-17; the olddata/music_metadata.dbis a stale leftover). Production runs fromdocker-compose.yml. The formervps.docker-compose.ymlpinnedDATABASE_URLto SQLite, which switched production to an empty database on 2026-09-21 — it is now only a deprecated alias ofdocker-compose.yml.
.github/workflows/deploy.yml — triggered by push to main:
appleboy/ssh-action@v1.2.2SSHes to the VPS (VPS_HOST/VPS_USER/VPS_PORT/VPS_SSH_KEYsecrets).- Guard: refuses to run unless the VPS
.envhas aDATABASE_URLpointing athbrl-postgres:5432/metadata_engine. - Backs up the current
.env, then rewrites it from GitHub repo Secrets (heredoc — keep its 12-space indentation or the workflow fails to parse).DATABASE_URLis not taken from the secrets: the value from the old.envis carried over and re-checked. git fetch+git checkout main+git merge --ff-only origin/main(fails instead of mixing commits if the VPS checkout diverged).- Tags the running image as
music-metadata-engine:previous, thenexport BUILDKIT_PROGRESS=plain(the old TTY progress renderer broke the SSH action) anddocker compose -f docker-compose.yml up -d --build(command_timeout: 30m; nodown— a failed build leaves the old container up). - Verify: waits up to 5 min for
healthyand checks that the container'sDATABASE_URLis stillhbrl-postgres/metadata_engine; otherwise it restoresmusic-metadata-engine:previousand fails the run.
Check runs at GitHub → Actions → "Deploy to VPS". "Re-run jobs" replays the old workflow file — to pick up a workflow change, push a fresh commit.
Manual deploy (when the pipeline is down):
ssh root@84.247.162.167
cd /srv/hbrl/Metadata-Engine
git fetch origin main && git checkout main && git merge --ff-only origin/main
docker compose -f docker-compose.yml up -d --buildFull annotated list: backend/.env.example. Summary:
| Variable | Used by | Notes |
|---|---|---|
SECRET_KEY |
security.py |
JWT signing — app won't start without it. openssl rand -hex 32. Also accepts JWT_SECRET. |
DATABASE_URL |
db.py |
Prod: postgresql://…@hbrl-postgres:5432/metadata_engine, set only in the VPS .env (not from GitHub Secrets). Unset → local SQLite. |
CORS_ORIGINS |
config.py |
Explicit origin list. |
GROQ_API_KEY |
ensemble, Whisper | Required for analysis. |
GEMINI_API_KEY |
ensemble, proxy | Required for analysis. |
OPENROUTER_API_KEY |
ensemble (3rd vote) | Free-tier daily limit is 50/day, or 1000/day once the account has ever bought ≥$10 credit (GET /api/v1/key → is_free_tier). |
OPENROUTER_ALLOW_PAID |
ensemble | Repo Variable, not a secret. true → cheap paid fallback when all free models fail. |
PINATA_JWT, PINATA_GATEWAY |
certificates / IPFS | |
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_PACK_* |
billing | see Stripe setup below |
SPOTIFY_*, LASTFM_API_KEY, DISCOGS_API_KEY, DISCOGS_SECRETS, ACOUSTID_API, ACOUSTID_API_TOKEN, ACR_* |
enrichment / identification | optional |
SENTRY_DSN / SENTRY_ENV / SENTRY_TRACES_SAMPLE_RATE |
main.py |
error monitoring; no-op if SENTRY_DSN unset. Free tier at sentry.io is plenty. |
APP_BASE_URL |
auth.py |
public frontend URL used in email links (default is the canonical app domain) |
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_FROM / SMTP_STARTTLS |
utils/email.py |
transactional email (password reset, email verification). If SMTP_HOST is unset, emails are logged instead of sent AND email verification is skipped (new accounts are created verified). Set these in prod to turn verification on. |
ADMIN_BOOTSTRAP_EMAIL / ADMIN_BOOTSTRAP_PASSWORD |
startup.py |
optional; creates a superuser on first boot if that email doesn't exist |
Currently read by nothing / inert code — safe to omit:
MISTRAL_API_KEY, DEEPSEEK_API_KEY, XAI_API_KEY (disabled ensemble
adapters), API_CLIENT_ID, API_CLIENT_SECRET (no code references),
ANTHROPIC_API_KEY (the /ai_proxy Claude branch exists but no key is
provisioned).
To add or rotate a secret: GitHub → repo → Settings → Secrets and variables
→ Actions, then push (or re-run the last deploy). Bulk-import from a dotenv
file with gh secret set -f <file>.
Admin = is_superuser = true on the users row (plus the allow-list in
backend/app/admin_config.py, currently just
hardbanrecordslab.pl@gmail.com). Superusers bypass the credit quota.
Reset a password (no way to recover the hash):
ssh -t root@84.247.162.167 \
"docker exec -it metadata-backend python reset_pwd.py <email>"reset_pwd.py prompts (hidden). To also force is_superuser / create a
missing account, run a short inline script via docker exec -i metadata-backend python using app.db + app.security.get_password_hash.
GET https://metadata.hardbanrecordslab.online/api/health/→200GET /api/billing/packs→ the four packsPOST /api/tools/fingerprint(with a file) → a fingerprintPOST /api/analysis/transcribe(with a file) → needsGROQ_API_KEY- Container:
docker ps --filter name=metadata-backend→Up … (healthy)
- Dashboard → Products → create 4 products, each with a one-time price ($9 / $35 / $89 / $199).
- Copy each Price ID (
price_…) into the matchingSTRIPE_PRICE_PACK_*secret. - Developers → Webhooks → add endpoint
https://metadata.hardbanrecordslab.online/api/billing/webhook/stripe, eventcheckout.session.completed; copy the Signing secret intoSTRIPE_WEBHOOK_SECRET. - Developers → API keys → Secret key into
STRIPE_SECRET_KEY. - Local test:
stripe listen --forward-to localhost:8888/api/billing/webhook/stripe, use the CLI'swhsec_….
Idempotency: the webhook keys on credit_purchases.stripe_session_id.
State = the metadata_engine database in hbrl-postgres + /srv/hbrl/Metadata-Engine/data/
(uploaded certificates, stale SQLite leftover).
Automated: .github/workflows/backup.yml runs daily at 03:00 UTC (and on
manual dispatch). It runs pg_dump -Fc metadata_engine inside hbrl-postgres,
verifies the dump (size + pg_restore --list), stores it as
data/backups/mme-pg-<stamp>.dump on the VPS (mode 600, keeps the last 14 of
these, latest-pg.dump points at the newest). It is not uploaded to GitHub:
the repo is public and the dump holds user data. There is no off-site copy yet
(restic — handbook decision D-1). Before 2026-09-21 this job snapshotted the
retired SQLite file with a sqlite3 CLI that is not installed on the VPS, so it
failed daily and no backup existed.
Restore (try it on a scratch database first if unsure):
cd /srv/hbrl/Metadata-Engine
docker compose -f docker-compose.yml stop backend
docker exec -i hbrl-postgres pg_restore -U hbrl_admin -d metadata_engine --clean --if-exists \
< data/backups/latest-pg.dump
docker compose -f docker-compose.yml start backendThe deploy also drops timestamped .env.backup.* files in the repo dir — prune
them occasionally.
torchis pinned to2.13.0+cpuinbackend/requirements.txt(CPU wheel index) — do not let it drift back to the CUDA build (~2 GB, times the deploy out).- Frontend main bundle is >1 MB; a code-split pass is a future improvement.