Skip to content
scalive
Menu
ConnectingLiveReconnectingOffline
GitHub

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:

bash
java -version
mill --version

Each 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:

text
scalive-quick-start/
├── build.mill
└── app/
    ├── resources/
    │   └── public/
    │       └── app.js
    ├── src/
    │   └── quickstart/
    │       ├── CounterLiveView.scala
    │       ├── Main.scala
    │       ├── RootLayout.scala
    │       └── Routes.scala

Create build.mill at the project root:

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

The :: 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:

Source
javascript
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 = liveSocket

Live.router 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:

Source
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, Increment

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:

Source
scala
package quickstart

import scalive.*

object Routes:
  val home = live

Create app/src/quickstart/RootLayout.scala:

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

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:

Source
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 Main

The server loads Scalive's packaged clients through LiveViewClientAssets, 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:

bash
mill app.run

Open 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.js or either packaged client script fails to load, check missing assets.

  • If port 8080 is already in use, stop the other process or change the single port value in Main.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.