Quick start
Before You Begin
Install a JDK and Mill. This quick start uses Scala 3.8.4 and the
dev.scalive::scalive:0.0.1-341a561ee032-SNAPSHOT artifact. Verify that both
tools are available before creating the project:
java -version
mill --versionEach command should print a version. Node.js, npm, and a separate browser build are not required for this project.
Create The Project
Create this project tree:
scalive-quick-start/
├── build.mill
└── app/
├── resources/
│ └── public/
│ └── app.js
├── src/
│ └── quickstart/
│ ├── CounterLiveView.scala
│ ├── Main.scala
│ ├── RootLayout.scala
│ └── Routes.scalaCreate 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-341a561ee032-SNAPSHOT"
)
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.
Mill automatically places files under app/resources on the application's
classpath. Scalive packages its supported Phoenix and Phoenix LiveView clients,
so this baseline needs no package manager or bundler. Applications that need
JavaScript package imports, separate modules, generated chunks, workers, fonts,
or another custom browser build can follow the
custom bundle guide.
Connect The Browser
Create app/resources/public/app.js:
const csrfToken = document.querySelector("meta[name='csrf-token']")?.getAttribute("content")
const params = csrfToken ? { _csrf_token: csrfToken } : {}
const liveSocket = new LiveView.LiveSocket("/live", Phoenix.Socket, { params })
liveSocket.connect()
window.liveSocket = liveSocketView source (documentation/fixtures/quick-start/resources/public/app.js:2-8)
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.
The packaged client scripts expose the Phoenix and LiveView globals used here.
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)
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(clientAssets: LiveViewClientAssets, 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"),
clientAssets.phoenixScript,
clientAssets.liveViewScript,
assets.trackedScript("app.js", defer := true, typ := "text/javascript")
),
bodyTag(content)
)View source (documentation/fixtures/quick-start/src/quickstart/RootLayout.scala:2-26)
The root layout renders the complete document. Its <head> gives Scalive a
place for the CSRF meta element and loads Phoenix first, Phoenix LiveView second,
and the application bootstrap last. Keep this order because app.js uses both
client globals.
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:
private val port = 8080
val run =
for
clientAssets <- LiveViewClientAssets.load()
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,
allowedWebSocketOrigins = Set(
WebSocketOrigin.http("localhost", port),
WebSocketOrigin.http("127.0.0.1", port)
)
)
).mapError(error => new IllegalArgumentException(error.toString))
security = LiveSecurity(config)
application = Live.router
.withRootLayout(RootLayout(clientAssets, assets))(
Routes.home -> CounterLiveView()
)
liveRoutes = ZioHttp.routes(application, security)
routes = liveRoutes ++ clientAssets.routes ++ assets.routes
_ <- Server.serve(routes).provide(Server.defaultWithPort(port))
yield ()
end MainView source (documentation/fixtures/quick-start/src/quickstart/Main.scala:2-42)
The server loads Scalive's packaged clients through
LiveViewClientAssetsclass LiveViewClientAssetsThe supported Phoenix and Phoenix LiveView browser clients., loads
app.js as an ordinary classpath asset, builds CSRF-protected Live routes, adds
both asset route sets, and listens on port 8080. The client files use the
default /_scalive/live-view asset mount; the Live socket remains at /live.
For local HTTP, this fixture
uses a fixed development-only signing-secret fallback and sets
secureCookie = false; its WebSocket allowlist admits the exact local page
origins http://localhost:8080 and http://127.0.0.1:8080. Do not deploy those
settings unchanged:
production must require a stable, high-entropy SCALIVE_TOKEN_SECRET, set
secureCookie = true behind HTTPS, and replace the allowlist with every exact
browser-facing HTTP or HTTPS origin. See
Configuration
and Deployment.
The tracked application-script URL contains Scalive's asset-set version. The unversioned
/static/app.js path returns 404 by default; render asset URLs through the
loaded StaticAssets value rather than hard-coding them.
Run It
From the project root, run:
mill app.runOpen http://localhost:8080/, substituting the configured port value if you
changed it. 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.
Although the socket transport becomes WebSocket, its browser Origin remains
the page's HTTP origin, http://localhost:8080 with the default port, rather
than becoming a ws URL.
You are done when Mill compiles the application, the server listens on the
configured port, 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
app.jsor either packaged client script fails to load, check missing assets.If port
8080is already in use, stop the other process or change the singleportvalue inMain.scala; the server binding and local WebSocket origins derive from it. Open the replacement port in the browser. If the page loads but its buttons do not connect, check socket connections.