A template for client/server apps with a full Scala 3 / ZIO 2 stack. These are some of the technologies I'm using, I briefly describe why and mention some options in case my choices are not yours. One of the most difficult thing in our field is to be able to choose a set of technologies for a project that fit well together (or can be easily made to fit), are well supported, are easy to find developers who know them (or easy to train in them), modern, etc.
Generate a project with the stack below, your choice of HTTP server, database and database layer, and a working CRUD screen, tests and CI included.
You need Copier 9.4+ (pipx install copier, or run it with uvx copier), a JDK
17+ and sbt. For the Scala.js client you also need Node.js and npm; for MariaDB, MySQL or PostgreSQL, Docker (for the
development database and the database tests).
copier copy --trust gh:rleibman/full-zio-stack my-appCopier asks a few questions; every one has a default, so Enter all the way through gives the recommended stack:
| Question | Choices (default first) |
|---|---|
| What to generate | server and Scala.js client, or server only |
| HTTP server | zio-http, http4s |
| Database | MariaDB, MySQL, PostgreSQL, SQLite |
| Database layer | Quill, doobie, Slick |
| AI | none, langchain4j (Anthropic, OpenAI or Ollama, chosen by configuration) |
| Authentication | none, zio-auth (needs zio-http and a GitHub Packages token) |
| Packaging | Docker image, Debian package with a systemd service, none |
| CI | GitHub Actions, none |
| License | MIT, Apache-2.0, BSD-3-Clause, GPL-3.0, proprietary |
plus the project's name, organization, package, author and ports. To skip the questions, pass the answers:
copier copy --trust --defaults \
--data project_name="Acme Orders" --data organization=com.acme --data author_name="Jane Doe" \
--data http_server=http4s --data db_layer=doobie --data database=postgres \
gh:rleibman/full-zio-stack acme-orders(--trust lets the template run its one post-generation task, git init.)
What you get: a Scala 3 / ZIO 2 project whose sample entity, ModelObject, is wired through every layer (model,
Flyway migration, database layer, Caliban GraphQL API, and a Material UI CRUD screen), with tests that run against
a real database, a README, and an AGENTS.md that describes the project for people and AI agents. Its README says
how to run it.
The client's facades. The client uses ScalablyTyped facades for React
and MUI, built by the generated project's stLib/ with a locally published fork of the converter, until the sbt 2
support it relies on is released upstream. On a machine without that converter, generate a server-only project.
Updating. Projects remember their answers (.copier-answers.yml). With a clean working tree,
copier update --trust --defaults brings in the template's later improvements as a merge; resolve any conflicts and
run the tests. CHANGELOG.md lists what each version changes.
This repository is also a Claude Code plugin. Install it once:
/plugin marketplace add rleibman/full-zio-stack
/plugin install full-zio-stack@full-zio-stack
Then ask for what you want, e.g. "build a full stack app called Acme Orders with Postgres and http4s": Claude
confirms the answers with you, generates the project with Copier, checks that it builds, runs and answers requests,
and builds your features on top. Generated projects carry their own AGENTS.md and an add-entity skill, so asking
to "add a Customer entity" adds it through every layer, following the project's conventions.
Optional AI: a service and a GraphQL mutation that asks an LLM (Anthropic, OpenAI or Ollama) for a description. The provider is configuration, so it can change without regenerating.
Optional authentication: login, registration with an emailed confirmation, password recovery and JWT sessions, over a
UserStore that keeps users and PBKDF2 password hashes. Its routes are zio-http routes, so it needs that server; it
is published to GitHub Packages, so building a project that uses it needs a GITHUB_TOKEN with read:packages.
OpenTelemetry tracing for the server: a span per GraphQL operation, exported over OTLP when an endpoint is configured, and a no-op tracer otherwise.
The default database layer (quill-jdbc-zio): queries are checked and turned into SQL at compile time.
The alternative database layers (template choices). doobie isn't strictly zio, but with the cats zio interop, it's super easy to combine.
scala.js (https://www.scala-js.org/)
I really wouldn't use anything else right now to create web pages. My biggest complaint about scala-js is that because all the
source documents are scala it puts graphic web designers at a disadvantage. It really forces you to separate the graphic
design domain (css) from the web content (traditionally html, now scala produced html), this is not necessarily a bad thing, but
it may force your developers to do things that normally the graphic design team takes care of.
Alternatives: https://www.playframework.com/
scalajs-react (https://github.com/japgolly/scalajs-react)
I've been using scalajs-react for a long time, so historical reasons lead me to choose it. I particularly like it's use of it's zio-like Callback (it would be even better if it actually morphed to use zio). If I was choosing today, I'd probably give slinky a strong consideration, at least at first reading it seems a bit easier to use. Alternatives: https://github.com/shadaj/slinky
scalablytyped (https://github.com/oyvindberg/ScalablyTyped)
An amazing project (that I've participated in, so I'm biased) that takes every typescript project from http://definitelytyped.org/ and creates scala bindings for it. For React projects you can choose slinky or scalajs-react flavors. Also, coming soon, an sbt plugin that lets you chose exactly what javascript libraries you want to wrap.
Material UI (https://mui.com/material-ui/)
Replaced Semantic UI, which is no longer maintained (and needed a findDOMNode shim to survive React 19). The facades
come from ScalablyTyped; client/.../components/MuiExtensions.scala fills the few gaps in them.
Alternatives: https://react-bootstrap.github.io/
MariaDB is the default. The schema lives in Flyway migrations (db-core/src/main/resources/db/migration/<database>/,
where MariaDB and MySQL share mysql), applied when the server starts. SQLite stores timestamps as integer epoch
milliseconds and supports only limited ALTER TABLE, which matters when you write later migrations.
This repository is both the template and a runnable app that contains every variant at once. CLAUDE.md explains how
it is organised; the short version: the sbt modules below are real code, overlay/ holds the files that exist only in
generated projects, and scripts/MakeTemplate.scala builds template/ (what Copier reads) from both.
| Module | What it is |
|---|---|
model/ |
Domain types shared by the server and the client (cross-built for the JVM and Scala.js), and the repository contract: Repository[F[_]], one ...Operations[F] per entity, and RepositoryError. The server implements it with ZIO, the client with AsyncCallback |
db-core/ |
ZIORepository (= Repository[DataIO]), the in-memory MockRepository, the pooled DataSource + Flyway, the migrations, and the contract test suites every database layer must pass |
db-quill/, db-doobie/, db-slick/ |
The three database layers. Each is built once per database as db-<layer>-<database> (mariadb, mysql, postgres, sqlite): shared code in src/main/scala, the database-specific bits (Quill context, doobie mappings, Slick profile) in src/main-<database>/scala. The server uses db-quill-mariadb; every variant passes the same contract tests |
server-core/ |
Configuration, the Caliban GraphQL API (resolved directly against the repository), and the layer wiring (AppLayers). Independent of the HTTP server. src/main-ai-* and src/main-auth-* hold the one file each of those choices changes; server-core-ai and server-core-auth are the projects that compile the non-default ones |
server-ziohttp/, server-http4s/ |
The two HTTP servers (zio-http, and http4s/ember through zio-interop-cats): GraphQL at /api/graphql, GraphiQL at /api/graphiql, /health, and the client's static files. Both pass the same contract tests |
client/ |
Scala.js + scalajs-react + Material UI, bundled with vite. Talks to the server with a GraphQL client generated from the server's schema |
stLib/ |
A standalone sbt build that generates the ScalablyTyped facades (React, MUI) |
ModelObject is a sample entity wired through every layer: model, migration, data service, GraphQL, and a CRUD screen.
To add your own entity, copy its pattern (every file involved has ModelObject in its name).
auth=zio-auth depends on zio-auth, which is published to GitHub Packages
and not to Maven Central, so even reading it needs credentials: export GITHUB_TOKEN (a personal access token with
read:packages) before building server-core-auth, server-ziohttp-auth, client-auth, or a generated project with
authentication. CI builds them only when the ZIO_AUTH_PACKAGES_TOKEN secret is set, because a workflow's built-in
token cannot read another repository's packages.
Scala 3.9, sbt 2.x (the launcher picks the version from project/build.properties), a JDK 17+, Node/npm (for the
ScalablyTyped facades and the vite bundler), and Docker (for the development database and the database tests).
The facades live in stLib/, a standalone sbt 2 build. Upstream ScalablyTyped only publishes an sbt 1 plugin, so it uses
a fork of the converter published locally from ~/projects/third-party/Converter (see stLib/project/plugins.sbt). Generate
and publish them to your local ivy repository once, and again whenever stLib/package.json changes (then bump version
in stLib/build.sbt and stlibVersion in build.sbt):
cd stLib && npm install && sbt --error publishLocal && cd ..The first run takes several minutes and several GB of memory.
docker compose up -d # MariaDB on localhost:13306, matching application.conf
sbt client/webDebugDist # builds the client into ./debugDist
STATIC_CONTENT_DIR=debugDist sbt server-ziohttp/run # http://localhost:8080 (or server-http4s/run)Flyway creates the tables on startup. GraphiQL is at http://localhost:8080/api/graphiql.
Configuration is in server-core/src/main/resources/application.conf. Every value can be overridden by the environment
variable named next to it (HTTP_PORT, DB_URL, DB_PASSWORD...), or the whole file with -Dconfig.file=....
For a production build of the client, sbt client/webDist writes ./dist, which the server serves by default.
The client's GraphQL code is generated from server-core/src/main/graphql/schema.graphql. After changing the API, run
sbt server-core/calibanRender to update that file; SchemaSpec fails until you do.
sbt test # incremental in sbt 2: reruns only the tests whose code changed
sbt testFull # everything
sbt testAuth # the zio-auth variants; needs GITHUB_TOKEN (see below)CRUDOperationsContractdefines what every entity's operations must do. It runs against the in-memory mock and, through Testcontainers, against a real database for each database layer (Docker required).ServerContractSpecstarts the HTTP server on a random port and checks it over real HTTP: health, GraphQL queries, mutations and error codes, GraphiQL, static files, client-side-route fallback, and path traversal.SchemaSpecchecks the committed GraphQL schema matches the API.
Everything compiles with -Werror and -Yexplicit-nulls.
The server uses sbt-native-packager (sbt server-ziohttp/Debian/packageBin builds a Debian package with a systemd
service). Packaging choices (Docker, systemd, none) will be a template question.
- Regenerate the template and check it:
scala-cli run --server=false scripts/MakeTemplate.scala, thenpython3 tests/check-ai-docs.py static. Committemplate/with the change that produced it. - Make sure CI is green, and run the client checks CI can't: see "What CI doesn't cover" in
tests/README.md. - Add the release to
CHANGELOG.md, set the same version inclaude-plugin/.claude-plugin/plugin.json, and check the plugin withclaude plugin validate .andclaude plugin validate claude-plugin. - Tag it (
git tag v0.1.0). Copier offers the latest tag to new projects,copier updatemoves existing ones to it, and the plugin's skill uses the latest tag too.
- This blog post uses a stack that's very similar to the one described here, I borrowed from it extensively, mostly in it's use of zio.
- Oyvindberg has been super, super helpful with not only the ScalablyTyped project but with looking over my shoulder as I make mistakes.
- All the people from all the projects above that work to make the scala culture so amazing!