Project anatomy
Follow The Startup Path
The quick start keeps composition explicit. Read it from the process toward the browser:
Mill compiles Scala and bundles browser assets into JVM resources.
Mainloads configuration, security, static assets, and application services.Mainpairs typed routes with LiveViews and starts ZIO HTTP.A route selects a LiveView, and its layouts wrap the rendered page.
The browser loads
app.js, connects, forwards events, and applies updates.
Understand Both Mounts
A Live route mounts once to produce the initial HTTP response and again when the browser establishes its live connection. These mounts create independent models; the HTTP model is not passed into the connected lifecycle. The lifecycle page explains the consequences for state, resources, failures, and reconnects.
Separate The Application Boundaries
Main.scala is the process boundary. It owns configuration, resource loading,
dependency layers, route composition, and server lifetime. Per-visitor state and
HTML rendering do not belong there.
Routes.scala is the URL boundary. Routes begin with
liveval live: LiveRouteSeed[Unit], decode path or query data when needed,
and are paired with LiveViewstrait LiveView[Msg, Model]Defines a server-rendered view with typed messages and model state. by
Live.routerval router: LiveRouter. Named route values also
give navigation code one typed source of URLs.
RootLayout.scala is the document boundary. A
LiveRootLayouttrait LiveRootLayout[-A, -Ctx]Declaratively renders and identifies the outer document shell. renders the outer
<html>, <head>, and <body>. It owns global scripts, stylesheets, metadata,
and the fallback title. When served through ZioHttp.routes with validated
configuration, Scalive injects the browser-bound CSRF meta token into its
<head>.
CounterLiveView.scala is the interactive-page boundary.
mountdef mount(ctx: MountContext): zio.package.Task[Model] creates connection-local
state, handleMessagedef handleMessage(model: Model, ctx: MessageContext): Msg => zio.package.Task[Model]Handles a message against the current immutable model. changes
state, and viewdef view(model: Signal[Model]): HtmlElement[Msg] describes typed HTML
and event bindings. A LiveView does not start the HTTP server or locate its own
asset files.
assets/js/app.js is the browser boundary. It creates LiveSocket, passes the
server-issued CSRF token, and connects to the socket path. Add hooks here only
for behavior that requires browser APIs. Application state and ordinary event
handling stay in Scala.
package.json and the Mill asset task are the build boundary. npm resolves and
bundles browser modules; Mill places outputs on the JVM classpath.
StaticAssetsclass StaticAssetsA loaded static asset manifest, URL/tag helper, and HTTP route set. fingerprints those
outputs, renders tracked URLs, and serves them.
Keep Dependencies Pointing Inward
The process constructs infrastructure and passes dependencies to routed LiveViews. A growing application normally follows this direction:
Main
├── StaticAssets
├── LiveSecurity
├── application services
└── Live.router
├── typed routes
├── root and live layouts
└── LiveViews
├── immutable models
├── typed messages
└── rendered HTML
Browser
└── LiveSocket
├── DOM events to the server
└── server patches to the DOMLiveViews may use injected services, but services should not depend on a particular LiveView or browser connection. This keeps durable domain behavior separate from connection-local presentation state.
Grow The Project
Keep these boundaries as the application expands:
Add route values before wiring new LiveViews into the router.
Use a
LiveLayouttrait LiveLayout[-A, -Ctx]Declaratively wraps a LiveView while preserving its message type. for shared markup inside the document; reserveLiveRootLayoutfor the complete shell.Build ZIO layers at startup for services required by routed LiveViews.
Add browser packages and hooks only for browser-side behavior.
Keep durable state in services or storage rather than a LiveView model.