Introducing Uni: One Scala 3 Library for the Whole App
Uni is a Scala 3 library that gives an application everything it needs from one dependency, with one set of conventions: object wiring, logging, object serialization (JSON and MessagePack), an HTTP client and server, typed RPC, MCP servers, Rx, CLI tools, and a test framework. The same code runs on the JVM, Scala.js, and Scala Native. Uni has shipped 37 releases since March 2026, and this post is its first proper introduction.

Many libraries, or one?
Building a Scala application usually starts with choosing libraries: one for HTTP, one for JSON, one for logging, one for tests, maybe one for wiring services together. Each is good at its job, and each comes with its own documentation, its own style, and its own release schedule. Before writing business logic, you learn several APIs and keep their versions compatible. Platform support also varies from library to library when you target Scala.js or Scala Native.
Uni takes the other path: learn one library, build the whole app.
- One dependency.
"org.wvlet.uni" %% "uni"brings object wiring, logging, codecs, HTTP, RPC, MCP, Rx, and CLI utilities. On the JVM it depends only on the Scala 3 standard library. - One set of conventions. A case class with
derives Weaverbecomes JSON, MessagePack, an RPC payload, and an MCP tool argument through the same codec. Services are plain classes wired byDesign, and any class gets logging by mixing inLogSupport. - One version to upgrade. All the pieces are released together, so they never disagree on transitive dependencies.
- One codebase on three platforms. The same code runs on the JVM, in Node.js or the browser, and as a Scala Native binary.
What is in the box
| Need | Uni |
|---|---|
| Object wiring and lifecycle | Design: constructor wiring, singletons, onStart/onShutdown hooks |
| Logging | LogSupport: info/debug/warn with source locations |
| Object serialization | Weaver: derives Weaver for JSON and MessagePack |
| HTTP client | Http.client: sync and async, retries, Server-Sent Events, WebSocket |
| HTTP server | REST Server: Netty on the JVM (uni-netty), Node.js on Scala.js, a native server on Scala Native |
| Typed RPC | RPC: services defined as Scala traits, with type-safe clients |
| MCP servers | wvlet.uni.mcp: a Scala trait becomes tools that AI agents can call |
| Reactive streams | Rx: composable streams and variables for async code and UI state |
| CLI | Command Launcher, terminal colors, progress bars |
| Web UI (Scala.js) | RxElement: reactive HTML components driven by Rx |
| Desktop apps | Electron: typed RPC between the UI and the main process over IPC |
| Testing | UniTest (uni-test): shouldBe, shouldMatch, property checks |
Quick start
Uni requires Scala 3.9 or later. Add it to build.sbt:
scalaVersion := "3.9.0"
libraryDependencies += "org.wvlet.uni" %% "uni" % "2026.1.37"
// Testing
libraryDependencies += "org.wvlet.uni" %% "uni-test" % "2026.1.37" % Test
testFrameworks += new TestFramework("wvlet.uni.test.Framework")Define a service as a trait, implement it as a plain class with logging, and let Design wire it:
import wvlet.uni.design.Design
import wvlet.uni.log.LogSupport
import wvlet.uni.mcp.description
import wvlet.uni.weaver.Weaver
case class Greeting(name: String, message: String) derives Weaver
trait GreeterService:
@description("Greet a person by name")
def greet(@description("Who to greet") name: String): Greeting
class GreeterServiceImpl extends GreeterService with LogSupport:
def greet(name: String): Greeting =
info(s"Greeting ${name}")
Greeting(name, s"Hello, ${name}!")
@main def hello =
Design.newDesign.bindImpl[GreeterService, GreeterServiceImpl].build[GreeterService] {
greeter =>
println(Weaver.toJson(greeter.greet("Uni"))) // {"name":"Uni","message":"Hello, Uni!"}
}The same trait is also an MCP server. Every public method becomes a tool that Claude Code or another MCP client can call, with its input schema derived from the method signature:
import wvlet.uni.mcp.MCP
@main def greeterMcp(): Unit = MCP.server
.withName("greeter")
.withVersion("0.1.0")
.withTools[GreeterService](GreeterServiceImpl())
.serveStdio()And it is tested with UniTest:
test("greet a person") {
GreeterServiceImpl().greet("Uni").message shouldBe "Hello, Uni!"
}For a longer walk through, read the Guide or The Uni Book, which builds a CLI, an HTTP service, and a cross-platform app step by step.
Demo: one trait, from server to browser to desktop
Uni's conventions carry across a whole application. Here, one service trait becomes an HTTP API, a web UI, and an Electron desktop app.
Define the service once, in code shared by every platform:
import wvlet.uni.rx.Rx
case class CounterState(value: Int)
trait CounterApi:
def get(): Rx[CounterState]
def increment(amount: Int): Rx[CounterState]
class CounterApiImpl extends CounterApi:
private var value = 0
def get(): Rx[CounterState] = Rx.single(CounterState(value))
def increment(amount: Int): Rx[CounterState] =
value += amount
Rx.single(CounterState(value))Serve it over HTTP. On the JVM, RPCRouter turns the trait into routes, and uni-netty serves them:
import wvlet.uni.http.netty.{NettyServer, RPCHandler}
import wvlet.uni.http.rpc.RPCRouter
@main def counterServer(): Unit = NettyServer
.withPort(8080)
.withRxHandler(RPCHandler(RPCRouter.of[CounterApi](CounterApiImpl())))
.start(_.awaitTermination())Build the UI in Scala.js. RxElement renders HTML that updates itself when an Rx.variable changes, and RPCClient calls the trait's methods with the same JSON codec the server uses. The same component runs in a browser page or in an Electron window:
import wvlet.uni.dom.all.*
import wvlet.uni.dom.all.given
import wvlet.uni.http.Http
import wvlet.uni.http.rpc.RPCClient
import scala.language.implicitConversions
class CounterUI extends RxElement:
private val rpc = RPCClient.of[CounterApi]
private lazy val client = Http.client.newAsyncClient
private val count = Rx.variable(0)
override def render: RxElement = div(
h1("Uni Counter"),
p(count.map(_.toString)), // re-renders when count changes
button(
onclick -> { () => rpc.callAsync[CounterState](client, "increment", Seq(1)).run(show) },
"+1"
)
)
private def show(s: CounterState): Unit = count := s.value
override def onMount(node: Any): Unit =
rpc.callAsync[CounterState](client, "get", Seq.empty).run(show)In a browser, the client sends these calls over HTTP to the server above. Serve the page from the same origin as the server (a dev-server proxy works), because the server does not add CORS headers.
Ship it as a desktop app. In Electron, the main process serves the same trait over IPC instead of a network port, and the renderer calls ElectronRenderer.install() once so CounterUI's calls go over IPC too. No HTTP server, no open ports:
import wvlet.uni.electron.{ElectronRenderer, ElectronRPCServer}
import scala.scalajs.js
import scala.scalajs.js.annotation.JSExportTopLevel
// Main process (Node.js)
@JSExportTopLevel("wireMainProcess")
def wireMainProcess(ipcMain: js.Dynamic): Unit =
ElectronRPCServer.serve(ipcMain, RPCRouter.of[CounterApi](CounterApiImpl()))
// Renderer (Chromium)
@JSExportTopLevel("main")
def main(): Unit =
ElectronRenderer.install()
CounterUI().renderTo("app")The whole app (service, UI, and business logic) is Scala, built from one codebase. examples/electron-app is the runnable version with Tailwind CSS styling and packaging for macOS, Windows, and Linux, and the Electron Tutorial builds it step by step from an empty directory.
Built for AI agents, too
More and more Scala code is written together with AI coding agents, so Uni treats agents as primary readers of its documentation:
llms.txtandllms-full.txtpublish the docs as Markdown, and every page has a.mdcopy.Uni for AI Agents lists the canonical imports, an Airframe-to-Uni mapping, and the mistakes agents most often make.
A Claude Code plugin teaches the agent idiomatic Uni code, with snippets that are compile-checked in CI:
text/plugin marketplace add wvlet/uni /plugin install uni@uni
Because Uni has one style across all of these features, an agent that has learned one part of it can write the rest in the same style.
The Scala 3 successor of Airframe
Uni grew out of Airframe. It keeps Airframe's most-used ideas (Design for object wiring, Rx, Surface for compile-time type information, and derived codecs) and rebuilds them for Scala 3 only, with fewer dependencies and a trimmed scope under the wvlet.uni.* package. Appendix B of the book tells the story, and the migration table maps Airframe imports to Uni. Uni is already in production use: Wvlet, a flow-style query language with its CLI and server, is built on uni, uni-test, and uni-netty.
Uni and the rest of the ecosystem
The Scala ecosystem has many good libraries. The Scala Toolkit, with sttp, uPickle, os-lib, and MUnit, is the easiest start for scripts, teaching, and everyday tasks, and Cask is a simple way to serve HTTP on the JVM. Uni works alongside them: you can add Uni for object wiring and logging and keep using the libraries you already rely on. If a focused library already serves you well, there is no need to switch. Uni and the Scala Toolkit explains when each fits and how to use them together.
Try it
Questions, ideas, and bug reports are welcome on GitHub.