A Scala 3 backend template. Wine auctions are the showcase domain.
If you're using this as a template, run the rename script first — it swaps madrileno for your project name + package and removes the auction demo:
./scripts/init-project.scala <project-name>See docs/scripts.md for what it does and what else lives under scripts/.
You'll need:
- JDK 21 (Temurin recommended)
- sbt 1.12+ (
sbt --versionto check) - Docker with
docker compose
docker compose up -dThat brings up three services on non-standard host ports so they don't clash with anything you already have running:
| Service | Image | Host port(s) | Login |
|---|---|---|---|
| Postgres | postgres:latest |
55432 → 5432 |
postgres / postgres |
| Mailpit | axllent/mailpit:latest |
51025 (SMTP), 58025 (UI) |
— |
| OpenObserve | public.ecr.aws/zinclabs/openobserve:latest |
55080 (UI + OTLP HTTP) |
root@example.com / Complexpass#123 |
State persists across restarts in named volumes. To wipe and start clean:
docker compose down -v
docker compose up -dcp .env.sample .envThe sample is wired against the docker-compose ports above — including a working Authorization header for OpenObserve so OTLP traces show up in the UI immediately.
JWT_SECRET is fine as-is for local dev — change it when you don't want strangers to be able to forge tokens. External auth providers (FIREBASE_PROJECT_ID, OIDC_*) ship empty; the app boots fine without them and the dev login (POST /v1/auth/dev) is enabled by default via DEV_AUTH_ENABLED=true.
Two ways:
sbt "runMain madrileno.main.MigrateMain" # recommended
sbt flywayMigraterunMain madrileno.main.MigrateMain is the app's own IOApp — same as bin/migrate-main in the Docker image — so it reads application.conf with .env injected by sbt-dotenv. Works out of the box on the default .env.
sbt flywayMigrate is the sbt-flyway plugin task. It evaluates sys.env at build-load time — before sbt-dotenv injects .env — so it only works if your shell already has PG_HOST / PG_PORT / PG_DATABASE / PG_USER / PG_PASSWORD exported. The other plugin tasks (flywayInfo / flywayValidate / flywayClean) have the same constraint but are handy for inspection regardless.
Run a migration every time you add one under src/main/resources/db/migration/.
The recommended workflow is one long-lived sbt session in interactive mode:
sbtInside the sbt shell:
> ~reStart
~reStart watches sources and restarts the app on every save (compile-on-save). Plain reStart runs it once. The app comes up on http://localhost:9000 (override with PORT in .env).
Quick smoke test from another terminal:
curl http://localhost:9000/v1/health-check- App — http://localhost:9000/v1/health-check
- Mailpit UI (sent mail lands here in dev) — http://localhost:58025
- OpenObserve UI (traces, metrics, logs) — http://localhost:55080 → Login → Traces tab. The first OTLP frame from the app creates the streams.
- OpenAPI / Swagger UI — http://localhost:9000/swagger (dev only — gated on
app.environment=dev). The spec is generated by Baklava as part ofsbt test; if/swaggershows nothing, runsbt testonce first.
- sbt caches
.envat JVM startup. If you change.env, exit sbt and start it again. Same goes for the long-lived sbt server (~/.sbt/1.0/server/...). docker compose downkeeps volumes;docker compose down -vwipes them. Wipe when you want a clean PG, change OpenObserve credentials, or cycle a corrupted state.- Tests don't use the docker-compose stack. Testcontainers spins up its own. Don't worry about polluting your dev DB during a test run.
- Migrations don't run automatically when the app starts. Run
sbt flywayMigrateafter adding one or after wiping volumes. - OpenObserve creates OTLP streams on first ingest. If the Traces tab is empty right after boot, hit the app a few times, refresh, give it a moment.
Reference material lives in docs/. For day one, read in this order:
docs/dev-workflow.md— sbt ergonomics for day-to-day work; common stuck states.docs/principles.md— the five principles the codebase is built around.docs/adding-a-module.md— vertical-slice walkthrough from migration to OpenAPI.
The docs/README.md lists everything else by topic — stack (auth, scheduler, observability…), conventions (domain modeling, sealed monad, error handling), operations (configuration, deployment).