Skip to content
Eezo-frameworkPublic

About

A Scala 3 web framework. Direct-style. The case class is the source of truth. Deploy with one command.

Topics

Resources

Stars

97 stars

Watchers

2 watching

Forks

Repository files navigation

eezo

A Scala 3 web framework where your domain model is the source of truth.
Direct style on virtual threads. Deploy with one command.

Made for Scala 3 CI Maven Central javadoc License: MIT

eezo.io · Tutorials · Docs · API

Shopping for a Scala framework, the Scala Days talk

▶ Watch the Scala Days talk, 27 minutes: Daniel Ciocîrlan and Riccardo Cardin build a shop with Eezo on stage, from the first case class to a working checkout.


What eezo is

Declare your data model, and eezo derives the rest from that one declaration: its table in Postgres, its HTML form, and CRUD routes that serve it.

case class Post(
    id: Id[Post],
    author: Id[User],
    title: String,
    body: String,
    published: Boolean
) derives Table, Form, Resource

A case class automatically derives its Postgres table and a type-safe query API. As you change your data model, Eezo detects schema changes. When you're ready, you can commit them to a fingerprinted SQL migration.

A case class also gets 7 CRUD routes: index, new, create, show, edit, update and destroy, served in a browser over rows in Postgres, with no need for a controller, repository or template written by hand. You can add your own routes as a plain def in a file, and the file path becomes the HTTP route path. You can also override a derived route on the same path.

Handlers are ordinary functions from a request to a response. Eezo runs on virtual threads, so blocking on the database or on an external API is cheap.

UIs are server-side rendered, with minimal functional Scala. A Component[State] has init, update and render, and can return HTML with Scala functions. For the frontend, the browser gets a thin JS client which sends events to the server, and the server renders DOM diffs that get patched on the UI.

No need for

  • effect systems
  • runtime reflection
  • plugins
  • JavaScript
  • configurations

Deploying is done with eezo deploy on Fly.io.

Requirements

  • JDK 25 or newer. Eezo needs virtual threads to not be pinned on synchronized.
  • Scala 3.8.4.
  • sbt 1.5.8 or newer, or sbt 2.0.6 or newer. The plugin is published for both.
  • Postgres, for an application with a database layer. The default connection is a local Postgres. One of the few Eezo configurations is for the database.

Install

project/plugins.sbt:

addSbtPlugin("io.eezo" % "sbt-eezo" % "0.2.0")

build.sbt:

scalaVersion := "3.8.4"

enablePlugins(EezoPlugin)

libraryDependencies += "io.eezo" %% "eezo" % "0.2.0"

scalacOptions ++= Seq("-release", "25", "-deprecation", "-feature", "-unchecked", "-no-indent")

// The application forks, so it runs on the JDK sbt was told about rather than the one sbt runs on.
run / fork := true

Every artifact is published at the same version, under the io.eezo group:

artifact what it carries
eezo the umbrella and the default dependency
eezo-http the http layer: servers, requests and responses, routing, Form, Resource, Layout, HttpApp
eezo-db the database layer: the pool, the sql interpolator, transactions, Table, schema commands and migrations, DbApp
eezo-auth sessions, email and password sign in, CSRF, route guards and row ownership
eezo-live server-side rendered UI with the thin browser client: the diff and patch protocol, the client runtime, PubSub
eezo-core what the others build on: configuration, HTML tree and DSL, derivation, model keys
sbt-eezo the sbt plugin that generates the route table, for sbt 1 and sbt 2

An application has one or two layers, the http layer and the database layer. Use a layer by extending the appropriate trait in your app:

  • DbApp from eezo-db
  • HttpApp from eezo-http
  • EezoApp from eezo which mixes both

The Layers doc explains more.

Hello, eezo

To start, you need two files. In src/main/scala/Main.scala:

import io.eezo.generated.Routes
import io.eezo.http.HttpApp
import io.eezo.http.RouteTable

object Main extends HttpApp {
  override def routes: RouteTable = Routes.table()
}

Then in src/main/scala/app/Hello.scala:

package app

import io.eezo.core.html.*
import io.eezo.http.Request
import io.eezo.http.Response

object Hello {

  def hello(request: Request): Response =
    Response.Ok(
      Html.doctype ++ html(
        head(title("hello, eezo")),
        body(h1("hello, eezo"), p("served at ", code(request.path)))
      )
    )
}

The Hello file is also the HTTP route. The plugin reads app/Hello.scala, mounts GET /hello calling def hello, and writes the table to a generated io.eezo.generated.Routes, which Main contains as its routes. A page is a value of Eezo's own HTML DSL. HttpApp brings main with it:

sbt run              # serves on http://localhost:8080
eezo dev             # serves, restarting on every save, with the route listing
eezo routes          # prints the table

Your first eezo application walks through this in about 20 minutes, and Routing says how a file name becomes a path.

From a model to an application

Add the database layer by depending on either eezo or just eezo-db. Make your case class derive Table, then add your tables (one line) to your schema:

import io.eezo.EezoApp
import io.eezo.db.Schema
import io.eezo.generated.Routes
import io.eezo.http.RouteTable

object Main extends EezoApp {
  override def schema: Schema     = AppSchema
  override def routes: RouteTable = Routes.table()
}

AppSchema registers the models that derive Table, and the schema commands run against it: eezo db status shows the drift between the case classes and the database, eezo db sync applies it in development, eezo db freeze writes a migration, eezo db migrate runs the migrations in production. A model that derives Table beside Form and Resource gets its seven routes backed by Postgres, and the posts you create in the browser survive a restart. Check out the the case class and the schema and migrations pages for more details.

One line in the model's companion guards its routes:

object Post {
  given Owned[Post, User] =
    User.guard.required[Post].owning(_.author).except(Action.Index, Action.Show)
}

Every route now needs a signed-in user, five of them reach only the author's own rows, and the login page travels into the route table with the declaration. Guards and ownership explains more.

The route table is a value, so Main can transform it before serving it. For example, Route.under("/admin") moves a set of routes and every URL their pages emit under a path prefix. The derived pages link to each other without ever naming the prefix. URLs and mounts explains how it works.

A live component holds its state on the server and the browser receives patches over a websocket:

final class Board extends Component[List[Order]] {
  def init(ctx: Init[List[Order]]): List[Order] = {
    ctx.subscribe(Sales.paid)((sale, sales) => sale :: sales)
    read(Table[Order].where(_.paid === true).orderBy(_.at.desc).list())
  }
  def handle(event: Event, sales: List[Order]): List[Order] = sales
  def render(sales: List[Order]): Html = ul(sales.map(s => li(Key(s.id.show), s.name)))
}

The live layer is the model and A live page builds one.

The command line

eezo new <name>       scaffold a new application in ./<name>
eezo dev              serve, restarting on every save
eezo routes           what is mounted
eezo db status        the drift between the models and the database
eezo db sync          apply that drift to the development database
eezo db freeze        write the drift as a migration
eezo db migrate       run the migrations
eezo build            stage the jars, the migrations and a Dockerfile
eezo deploy           build, deploy to Fly.io, migrate, wait for health
eezo upgrade          move to the latest release on Maven Central

The launcher is bin/eezo in this repository. The eezo command line is the reference, and Deploy explains how deploying works, and the possible deploy targets.

Examples

example artifact entry trait what it shows
hello eezo-http HttpApp one handwritten route and no database
reminders eezo-db DbApp one Table and a nightly job over its rows, no server
blog eezo, eezo-auth EezoApp a model deriving all three type classes, guarded and mounted under /admin
todo eezo EezoApp a guided tour of the command line, every output captured from a real run
shop eezo, eezo-auth EezoApp a shop paying through Stripe, with a live sales board

The examples build against the working tree rather than a release, so run them from a clone:

sbt publishLocalForExample      # publishes eezo and sbt-eezo locally and records the version
cd examples
sbt hello/run                   # http://localhost:8080/hello
sbt reminders/run               # runs the job once against the development Postgres
sbt "blog/run db sync --apply"     # creates the tables, then:
sbt blog/run                    # http://localhost:8080, posts under /admin/posts

Each example has a README that explains how it works.

Documentation

The documentation lives at eezo.io/docs and its sources are in docs/. It is organised in four pillars:

The API reference, generated from the sources, is at eezo.io/api. CONTEXT.md is the vocabulary the framework and its documentation use, and docs/adr records the decisions behind its shape.

Contributing

Issues and pull requests are welcome at github.com/Eezo-framework/eezo. The build is what CI runs:

sbt scalafmtCheckAll scalafmtSbtCheck   # one formatter, one configuration
sbt +compile Test/compile               # the plugin is built for sbt 1 and sbt 2
sbt +test

The db suite starts a real Postgres through testcontainers, so it needs a Docker daemon. sbt check runs the regression half of that suite, and sbt backlog runs the tests that describe open work and are expected to fail until it lands. The decisions behind the framework are recorded in docs/adr.

Pull requests target dev, the trunk. main is the latest release: it moves only by fast forward when a version is tagged, so the front page, this README and a plain clone always describe what is on Maven Central. Releasing is bin/release <version> on dev: it bumps the version literals the docs quote, fast forwards main, and pushes the tag that publishes the artifacts and deploys the site.

Licence

Eezo is released under the MIT License, copyright 2026 Riccardo Cardin and Daniel Ciocîrlan. An application that depends on Eezo carries no obligation beyond preserving the copyright notice, and Eezo takes on no dependency that would add one. Attribution notices for third party components are collected in NOTICE.

About

A Scala 3 web framework. Direct-style. The case class is the source of truth. Deploy with one command.

Topics

Resources

Stars

97 stars

Watchers

2 watching

Forks

Releases

Contributors

Languages