Quick start
Before You Begin
Install a JDK, Mill, and Node.js 18 or newer with npm. This quick start uses
Scala 3.8.4 and the dev.scalive::scalive:0.0.1-e2d90be8edfd-SNAPSHOT
artifact. Verify that the tools are available before creating the project:
java -version
mill --version
node --version
npm --versionEach command should print a version; node --version must report v18 or
newer.
Create The Project
Create this project tree:
scalive-quick-start/
├── build.mill
└── app/
├── assets/
│ └── js/
│ └── app.js
├── src/
│ └── quickstart/
│ ├── CounterLiveView.scala
│ ├── Main.scala
│ ├── RootLayout.scala
│ └── Routes.scala
├── package-lock.json
└── package.jsonCreate build.mill at the project root:
package build
import mill.*, scalalib.*
object app extends ScalaModule:
def scalaVersion = "3.8.4"
def mainClass = Some("quickstart.Main")
def repositories = Seq(
"https://central.sonatype.com/repository/maven-snapshots"
)
def mvnDeps = Seq(
mvn"dev.scalive::scalive:0.0.1-e2d90be8edfd-SNAPSHOT"
)
def packageJson = Task.Source(moduleDir / "package.json")
def packageLock = Task.Source(moduleDir / "package-lock.json")
def assetSources = Task.Sources(moduleDir / "assets")
def bundle = Task {
val workDir = Task.dest / "work"
val publicDir = Task.dest / "public"
os.copy(packageJson().path, workDir / "package.json", createFolders = true)
os.copy(packageLock().path, workDir / "package-lock.json")
assetSources().foreach(source =>
os.copy(source.path, workDir / source.path.last)
)
os.proc("npm", "ci").call(cwd = workDir)
os.proc("npm", "run", "build").call(cwd = workDir)
os.copy(
workDir / "dist" / "app.js",
publicDir / "app.js",
createFolders = true
)
PathRef(publicDir)
}
def resources = Task {
super.resources() :+ bundle()
}
end appThe :: in the dependency selects the Scala 3 artifact. Scalive publishes and
supports exactly two Scala coordinates: dev.scalive::scalive, which contains
all production API, render, runtime, protocol, and transport classes, and the
optional test-support coordinate dev.scalive::scalive-testing. Scalive's ZIO
and ZIO HTTP dependencies are supplied transitively.
Create app/package.json:
{
"private": true,
"type": "module",
"scripts": {
"build": "esbuild assets/js/app.js --bundle --platform=browser --format=iife --target=es2020 --outfile=dist/app.js"
},
"dependencies": {
"phoenix": "1.7.21",
"phoenix_live_view": "1.1.28"
},
"devDependencies": {
"esbuild": "0.28.1"
}
}Generate and commit the lockfile:
npm install --package-lock-only --prefix appMill uses npm ci to reproduce this dependency graph and places the bundled
app.js in the application's classpath resources.
Connect The Browser
Create app/assets/js/app.js:
import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view"
const csrfToken = document.querySelector("meta[name='csrf-token']")?.getAttribute("content")
const params = csrfToken ? { _csrf_token: csrfToken } : {}
const liveSocket = new LiveSocket("/live", Socket, { params })
liveSocket.connect()
window.liveSocket = liveSocketView source (documentation/fixtures/quick-start/assets/js/app.js:2-11)
Live.routerval router: LiveRouter mounts its socket at /live
by default. The server injects the CSRF meta element into the root layout's
<head> and binds it to a cookie. The client returns the value as _csrf_token
when it opens the socket. Do not create or hard-code this token in JavaScript.
Define The LiveView
Create app/src/quickstart/CounterLiveView.scala:
package quickstart
import zio.{Task, ZIO}
import scalive.*
final class CounterLiveView extends LiveView[CounterLiveView.Msg, Int]:
import CounterLiveView.Msg
def mount(ctx: MountContext): Task[Int] =
ZIO.succeed(0)
def handleMessage(model: Int, ctx: MessageContext) =
case Msg.Decrement => ZIO.succeed(model - 1)
case Msg.Increment => ZIO.succeed(model + 1)
override def view(model: Signal[Int]): HtmlElement[Msg] =
mainTag(
h1("Scalive counter"),
button(typ := "button", on.click(Msg.Decrement), "Decrease"),
outputTag(aria.live := "polite", model.map(_.toString)),
button(typ := "button", on.click(Msg.Increment), "Increase")
)
object CounterLiveView:
enum Msg:
case Decrement, IncrementView source (documentation/fixtures/quick-start/src/quickstart/CounterLiveView.scala:2-28)
The Int is all state needed to render this interface. Msg lists every input
the view accepts. mount creates the state, handleMessage performs an
effectful transition, and view projects the state into typed HTML.
Add Routes And Layout
Create app/src/quickstart/Routes.scala:
package quickstart
import scalive.*
object Routes:
val home = liveView source (documentation/fixtures/quick-start/src/quickstart/Routes.scala:2-7)
Create app/src/quickstart/RootLayout.scala:
package quickstart
import scalive.*
final class RootLayout(assets: StaticAssets) extends LiveRootLayout[Any, Any]:
def key(ctx: LiveRootLayoutContext[Any, Any]): String = "quick-start-root"
def render[Msg](
content: HtmlElement[Msg],
pageTitle: Option[String],
ctx: LiveRootLayoutContext[Any, Any]
): HtmlElement[Msg] =
htmlRootTag(
lang := "en",
headTag(
metaTag(charset := "utf-8"),
metaTag(nameAttr := "viewport", contentAttr := "width=device-width, initial-scale=1"),
liveTitle(pageTitle, default = "Scalive quick start"),
assets.trackedScript("app.js", defer := true, typ := "text/javascript")
),
bodyTag(content)
)View source (documentation/fixtures/quick-start/src/quickstart/RootLayout.scala:2-23)
The root layout renders the complete document. Its <head> gives Scalive a
place for the CSRF meta element and loads the tracked browser bundle.
Start The Server
Create app/src/quickstart/Main.scala:
package quickstart
import java.time.Duration
import zio.*
import zio.http.Server
import scalive.*
object Main extends ZIOAppDefault:
override val run =
for
assets <- StaticAssets.load(StaticAssetConfig.classpath("public", Seq("app.js")))
config <- ZIO
.fromEither(
ZioHttpConfig(
signingSecret = sys.env.getOrElse(
"SCALIVE_TOKEN_SECRET",
"local-development-secret-change-me"
),
sessionMaxAge = Duration.ofDays(7),
secureCookie = false
)
).mapError(error => new IllegalArgumentException(error.toString))
security = LiveSecurity(config)
application = Live.router
.withRootLayout(RootLayout(assets))(
Routes.home -> CounterLiveView()
)
liveRoutes = ZioHttp.routes(application, security)
routes = liveRoutes ++ assets.routes
_ <- Server.serve(routes).provide(Server.defaultWithPort(8080))
yield ()View source (documentation/fixtures/quick-start/src/quickstart/Main.scala:2-34)
The server loads the browser bundle, builds CSRF-protected Live routes, adds the
static asset routes, and listens on port 8080. For local HTTP, this fixture
uses a fixed development-only signing-secret fallback and sets
secureCookie = false. Do not deploy those settings unchanged: production must
require a stable, high-entropy SCALIVE_TOKEN_SECRET and set
secureCookie = true behind HTTPS. See
Configuration
and Deployment.
Run It
From the project root, run:
mill app.runOpen http://localhost:8080/. The HTTP request first produces disconnected
HTML. The client then connects to /live, Scalive mounts an independent
connected model, and button events travel over the socket as typed messages.
You are done when Mill completes the npm build, the server listens on port
8080, and the browser shows a counter starting at 0. Both buttons should
update it without reloading the page.
If Something Fails
If Mill cannot resolve the Scalive dependency, confirm the snapshot version and repository above, then check startup troubleshooting.
If
npm ci, bundling, orapp.jsfails, check missing assets.If port
8080is already in use, stop the other process or change the port inMain.scala. If the page loads but its buttons do not connect, check socket connections.