Getting started
An AI-first Scala 3 backend template. Wine auctions are the showcase domain.
AI-first means the repo is wired for assistant-driven development out of the box: a committed .mcp.json gives your assistant a live compile/test loop (Metals) and a read-only upstream reference (the madrileno MCP server), and the docs/ double as its knowledge base. See docs/ai-assisted-dev.md for the full setup.
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/.
Quick start
Section titled “Quick start”You’ll need four tools. Install each from its package manager or official installer:
- JDK 21 (Temurin) — adoptium.net/installation (apt repo on Linux,
brew install --cask temurin@21on macOS, winget on Windows) - sbt 1.12+ — scala-sbt.org/download (
sbt --versionto check; the launcher readsproject/build.propertiesand pulls the pinned sbt itself) - scala-cli — scala-cli.virtuslab.org/install — needed for the tools under
scripts/(app,scaffold-module,doctor,dev-console, …). Versions before 1.18 print a warning about TASTY files from Scala 3.9 on every run; it is harmless. - Docker with
docker compose— docs.docker.com/engine/install
The three JVM tools also install together via SDKMAN:
sdk install java 21-temsdk install sbtsdk install scalacli1. Boot the dev stack
Section titled “1. Boot the dev stack”docker compose up -d # Postgres only: everything the app needsdocker compose --profile full up -d # plus Mailpit, OpenObserve and SiloPostgres is the whole default stack. The app stores uploads on disk, writes outgoing mail to its log and keeps telemetry off until you ask for more, so a clone runs with one container and nothing to configure. The full profile adds the three services behind the SMTP, OTLP and S3 integrations; flip the three matching lines in .env (MAILER_SENDER=smtp, OTEL_SDK_DISABLED=false, OBJECT_STORAGE=s3) and the same binary uses them.
All host ports are non-standard so they don’t clash with anything you already have running:
| Service | Profile | Image | Host port(s) | Login |
|---|---|---|---|---|
| Postgres | default | postgres:latest |
55432 → 5432 |
postgres / postgres |
| Mailpit | full |
axllent/mailpit:latest |
51025 (SMTP), 58025 (UI) |
— |
| OpenObserve | full |
public.ecr.aws/zinclabs/openobserve:latest |
55080 (UI + OTLP HTTP) |
root@example.com / Complexpass#123 |
| Silo | full |
pgsty/silo:RELEASE.2026-09-16T00-00-00Z |
59000 (S3 API), 59001 (Console UI) |
minioadmin / minioadmin |
State persists across restarts in named volumes. To wipe and start clean:
docker compose --profile full down -vdocker compose up -d2. Configure env
Section titled “2. Configure env”cp .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 as soon as you enable them. Out of the box it selects the Postgres-only stack: MAILER_SENDER=log, OBJECT_STORAGE=disk, OTEL_SDK_DISABLED=true.
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.
3. Apply database migrations
Section titled “3. Apply database migrations”sbt "runMain madrileno.main.MigrateMain"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. It takes a subcommand (migrate is the default):
sbt "runMain madrileno.main.MigrateMain info" # list applied / pending migrationssbt "runMain madrileno.main.MigrateMain validate" # verify checksums match the filessbt "runMain madrileno.main.MigrateMain clean" # DROP every table — dev DB onlyRun migrate every time you add one under src/main/resources/db/migration/.
4. Run the app
Section titled “4. Run the app”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).
Without an sbt shell, from a script or a coding assistant, the launcher does the same as a one-shot: ./scripts/app.scala start compiles, applies pending migrations, runs the app in the background and returns once the health check answers. restart, stop, status and logs do what they say, and migrate applies migrations without starting. See docs/scripts.md.
Quick smoke test from another terminal:
curl http://localhost:9000/v1/health-checkTo check the whole setup at once — Docker, the compose services, Postgres, and the app — run the doctor (needs scala-cli):
./scripts/doctor.scala5. Look around
Section titled “5. Look around”- App — http://localhost:9000/v1/health-check
- Sent mail — in the application log (
./scripts/app.scala logs), one entry per mail with recipients, subject and text body. With thefullprofile andMAILER_SENDER=smtp: the Mailpit UI at http://localhost:58025. - OpenObserve UI (traces, metrics, logs;
fullprofile andOTEL_SDK_DISABLED=false) — http://localhost:55080 → Login → Traces tab. The first OTLP frame from the app creates the streams. - Uploads — under
uploads/with the default disk backend; in the Silo console at http://localhost:59001 with thefullprofile andOBJECT_STORAGE=s3. Presigned uploads (POST /v1/auctions/{id}/images/presign) needs3; the disk backend has no URLs to sign, so the endpoint answers 500. - OpenAPI / Swagger UI — http://localhost:9000/swagger (dev only — gated on
app.environment=dev). The spec is generated by Baklava from the router specs on every test run —sbt testandtestOnlyrefresh the suites that ran and keep the rest. If/swaggershows nothing, runsbt testFullonce: on a fresh checkout sbt 2’s cachedtestmay run no suites at all (see docs/dev-workflow.md).
Things that catch people out
Section titled “Things that catch people out”reStartreports success even when the app fails to start (e.g. port 9000 is taken). Checkcurl -sf http://localhost:9000/v1/health-checkbefore assuming it’s up; if it never answers, the error is at the end oflogs/madrileno.log../scripts/app.scala startwaits for the health check itself and fails with those lines.- 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/...). An app started by./scripts/app.scalareads.envat every start, so there arestartis enough. docker compose downkeeps volumes;docker compose down -vwipes them. Wipe when you want a clean PG, change OpenObserve credentials, or cycle a corrupted state. Add--profile fullto reach the optional services’ containers and volumes.- 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.
- The app doesn’t run migrations when it starts. Run
sbt "runMain madrileno.main.MigrateMain"or./scripts/app.scala migrateafter adding one or after wiping volumes;./scripts/app.scala startruns them before it starts the app. - 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. Still empty?
OTEL_SDK_DISABLEDistruein the stock.env.
Documentation
Section titled “Documentation”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.
Driving development with an AI assistant? docs/ai-assisted-dev.md covers the Metals + madrileno MCP server setup (the build tools AGENTS.md assumes).
Building a UI against it? docs/frontend.md covers the reference frontend companion and the generated-contract loop that keeps it type-safe; docs/mobile.md covers the reference mobile app on the same contract.
The docs/README.md lists everything else by topic — stack (auth, scheduler, observability…), conventions (domain modeling, sealed monad, error handling), operations (configuration, deployment).
License
Section titled “License”The template is licensed under Apache-2.0, but projects generated from it are unencumbered: you may relicense the code created via ./scripts/init-project.scala under any terms, with no attribution required. The init script removes the LICENSE file (and this section) so you can add a license of your own choosing.