Skip to content

About

A client/server app with a full scala/zio stack.

Resources

Stars

11 stars

Watchers

4 watching

Forks

Latest commit

 

History

37 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

full-zio-stack

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.

Create a project

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-app

Copier 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.

With Claude Code

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.

Currently on the stack

ZIO Based

zio-http

Caliban

zio-config

zio-logging

langchain4j

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.

zio-auth

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.

zio-telemetry

OpenTelemetry tracing for the server: a span per GraphQL operation, exported over OTLP when an endpoint is configured, and a no-op tracer otherwise.

zio-test

zio-json

quill

The default database layer (quill-jdbc-zio): queries are checked and turned into SQL at compile time.

doobie, Slick

The alternative database layers (template choices). doobie isn't strictly zio, but with the cats zio interop, it's super easy to combine.

Non-ZIO tech

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/

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

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.

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 / MySQL / Postgres / SQLite

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.

Working on the template

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).

The zio-auth variants

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.

Toolchain

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).

First-time setup: the ScalablyTyped facades

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.

Running it

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.

Changing the GraphQL API

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.

Testing

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)
  • CRUDOperationsContract defines 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).
  • ServerContractSpec starts 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.
  • SchemaSpec checks the committed GraphQL schema matches the API.

Everything compiles with -Werror and -Yexplicit-nulls.

Production

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.

Releasing

  1. Regenerate the template and check it: scala-cli run --server=false scripts/MakeTemplate.scala, then python3 tests/check-ai-docs.py static. Commit template/ with the change that produced it.
  2. Make sure CI is green, and run the client checks CI can't: see "What CI doesn't cover" in tests/README.md.
  3. Add the release to CHANGELOG.md, set the same version in claude-plugin/.claude-plugin/plugin.json, and check the plugin with claude plugin validate . and claude plugin validate claude-plugin.
  4. Tag it (git tag v0.1.0). Copier offers the latest tag to new projects, copier update moves existing ones to it, and the plugin's skill uses the latest tag too.

Acknowledgements

  • 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!

About

A client/server app with a full scala/zio stack.

Resources

Stars

11 stars

Watchers

4 watching

Forks

Releases

Packages

Contributors

Languages