Self-hosting¶
tap ships as a Docker Compose stack: PostgreSQL, Redis, the FastAPI backend, a Celery worker and
beat scheduler for automatic polling, and an nginx-served frontend that reverse-proxies /api to
the backend. There is nothing else to install.
Prerequisites¶
- Docker Engine and the Docker Compose plugin (
docker compose version). - A place to run it with the ports
8080(frontend) and8000(backend API) free, or editdocker-compose.ymlto change them.
Quick start¶
git clone https://github.com/Peppe37/tap.git
cd tap
cp .env.example .env
Edit .env and fill in the secrets .env.example leaves blank:
# JWT signing key for access/refresh tokens
python3 -c "import secrets; print(secrets.token_urlsafe(48))"
# Fernet key used to encrypt provider credentials (e.g. an aggregator API key) at rest
python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
(No Python locally? Run either command through Docker instead:
docker run --rm python:3.12-slim python3 -c "...".)
Also change POSTGRES_PASSWORD from the placeholder. Then:
docker compose up --build
- Frontend: http://localhost:8080
- Backend API + interactive docs: http://localhost:8000/docs
The backend container runs database migrations (alembic upgrade head) and seeds the built-in
carrier/shop/provider catalogue on every startup, so a fresh instance is ready to use as soon as
the containers report healthy.
Using pre-built images¶
docker compose up --build builds the backend and frontend images locally, which works
everywhere but takes a minute on first run. As an alternative, every release publishes multi-arch
(amd64/arm64) images to GitHub Container Registry -- ghcr.io/peppe37/tap-backend and
ghcr.io/peppe37/tap-frontend -- and pushes to main also publish a rolling edge tag. Use them
instead of building by layering docker-compose.images.yml on top, which swaps build: for
image: on the backend/worker/beat/frontend services without changing anything else (Postgres,
Redis, volumes, healthchecks stay exactly as in docker-compose.yml).
You don't need to clone the whole repository for this -- just the two compose files and .env:
mkdir tap && cd tap
curl -O https://raw.githubusercontent.com/Peppe37/tap/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/Peppe37/tap/main/docker-compose.images.yml
curl -O https://raw.githubusercontent.com/Peppe37/tap/main/.env.example
cp .env.example .env # then fill in the secrets, same as the Quick start above
docker compose -f docker-compose.yml -f docker-compose.images.yml up -d
Set TAP_IMAGE_TAG in .env to pin a specific version (e.g. v0.2.0) or track edge; it
defaults to latest (the most recent stable release).
Why two images, not one¶
The backend image is already reused three ways (API, Celery worker, Celery beat -- same image,
different command), so it's not duplicated per-service. The frontend is nginx serving a static
build and reverse-proxying /api, a fundamentally different runtime than the Python API. Merging
the two into a single image would mean running two unrelated processes in one container (nginx +
uvicorn, via something like supervisord) — it would work, but it means one container whose health
check can't distinguish "the API died" from "nginx died," logs from both processes interleaved,
and no way to scale or cache the static frontend independently of the API. Two images, one
process each, is the more debuggable and more standard shape for this kind of app; that's why we
kept it even though it means one extra docker pull.
First run¶
Opening the frontend for the first time shows a "Create the administrator account" screen
instead of a login form — this is a one-time bootstrap step (the backend exposes
GET /auth/setup-status to detect that no user exists yet). Once created, every further user is
added from inside the app by an existing account; there is no public sign-up.
Guided connections¶
Carriers can be tracked through more than one method: an official carrier API, a maintained scraper, or a third-party aggregator. Methods that need a personal API key (aggregators, mainly) show up under Settings → Connections with:
- step-by-step instructions for obtaining the credential from that provider's own site,
- the exact fields to fill in,
- a "test connection" check that calls the provider before anything is saved.
Nothing is stored until the test succeeds, and credentials are encrypted at rest with the Fernet
key you generated above (TAP_CREDENTIAL_ENCRYPTION_KEY) — losing that key makes stored
credentials unrecoverable, so back it up along with the database.
Environment variables¶
All of these live in .env at the repo root, read by docker-compose.yml:
| Variable | Purpose |
|---|---|
POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB |
Credentials for the bundled PostgreSQL container. |
TAP_ENVIRONMENT |
production by default; informational, used in a few log/error messages. |
TAP_DATABASE_URL |
SQLAlchemy async URL the backend uses to reach Postgres. |
TAP_REDIS_URL |
Redis URL shared by Celery (broker/result backend) and the local rate limiter. |
TAP_JWT_SECRET_KEY |
Signs access/refresh tokens. Rotating it invalidates every existing session. |
TAP_CREDENTIAL_ENCRYPTION_KEY |
Fernet key encrypting stored provider credentials. |
TAP_CORS_ORIGINS |
JSON array of allowed origins for the API (only matters if you serve the frontend from a different origin than the default nginx proxy). |
TAP_RATE_LIMIT_PER_MINUTE |
Requests/minute allowed per caller (authenticated user, or client IP when unauthenticated) across the whole API. Default 120; raise it if a household of heavy users trips it during normal use. |
VITE_API_BASE_URL |
Base path the frontend calls; left as /api unless you split frontend/backend across origins. |
Health and monitoring¶
GET /api/health-- plain liveness: "is the process running." Always200 {"status": "ok"}if it responds at all.GET /api/health/ready-- readiness: actually checks the database and Redis connections,503if either is down. This is whatdocker-compose.yml's backend healthcheck uses (sodepends_on: condition: service_healthyfor worker/beat/frontend means "backend can really serve requests," not just "the process started"); point an external uptime checker or reverse proxy health check at this one, not the plain liveness endpoint.- Admins also get an update-available notice in the UI itself (
GET /api/system/update-status), checked against GitHub Releases and cached for 6h -- no separate setup needed.
Updating¶
git pull
docker compose up --build
Migrations run automatically on backend startup; there's no separate migration step to remember.
Backups¶
Everything that matters lives in the postgres-data named volume (packages, tracking history,
users, encrypted credentials) plus the two secrets in .env
(TAP_JWT_SECRET_KEY, TAP_CREDENTIAL_ENCRYPTION_KEY — without the encryption key, backed-up
credentials can't be decrypted even if the database is restored).
scripts/backup.sh [output-dir] dumps the database (gzip-compressed, timestamped) and prunes
dumps older than TAP_BACKUP_RETENTION_DAYS (default 14 days). Run it from a cron job or
systemd timer on the host for actual automation:
# crontab -e
0 3 * * * cd /path/to/tap && ./scripts/backup.sh >> backups/backup.log 2>&1
Back up .env alongside the dumps -- it never changes as often, but a dump without the
encryption key it contains is only half a backup. scripts/restore.sh <dump-file> restores one
(destructive: it drops and recreates the database first, asks for confirmation, and stops
backend/worker/beat for the duration).
Security¶
Every push and pull request runs, on top of linting and tests: security.yml (pip-audit and
bandit for the backend, pnpm audit for the frontend, a gitleaks secret scan across the whole
repository) and codeql.yml (static analysis for Python and JavaScript/TypeScript). Two frontend
advisories are currently accepted as not applicable rather than fixed — see the comment above
auditConfig in frontend/pnpm-workspace.yaml for the reasoning behind each one.
Dependabot opens weekly PRs for outdated pip/npm/Docker-base-image/GitHub Actions dependencies
(grouped per ecosystem, so it's one PR to review, not ten), and every published Docker image
carries SBOM and provenance attestations (visible in GHCR's package view) so you can audit what
went into a given ghcr.io/peppe37/tap-* tag.
Found an actual vulnerability? See SECURITY.md rather than opening a public issue.
Local development¶
See Contributing for running the backend and frontend outside Docker.