Client setup and static assets
Prerequisites
Have a Scalive application that can start from Mill, plus Node.js and npm for the browser bundle. Complete the Quick start first if you do not yet have routes and a root layout.
Build The Client Bundle
Install the Phoenix JavaScript packages and bundle a browser entry point. The current Scalive quick-start fixture uses these versions:
{
"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 package-lock.json. The repository's NpmAssets Mill trait
runs npm ci, runs the package's build script, and copies the declared
bundleOutputs from dist into a public resource directory. If your build
uses that trait, include its bundle output in the Scala module's resources:
object myApp extends ScalaCommon with NpmAssets:
def moduleDeps = Seq(scalive)
override def bundleOutputs = Seq("app.js")
def resources = Task {
super.resources() :+ bundle()
}This repository-local build setup is shown in full in the quick start.
Connect LiveSocket
Create the browser entry point imported by the bundle:
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 = liveSocketLive.routerval router: LiveRouter uses /live as its current default socket path. Scalive injects
the csrf-token meta element into the root layout's <head> and associates it
with the CSRF cookie. Return that value as _csrf_token; do not create or
hard-code a token in JavaScript.
Pass Phoenix options such as hooks in the final LiveSocket options object
when the application needs them. The documentation application uses the same
CSRF setup, registers its hooks, calls connect(), and exposes the socket for
browser-console debugging.
Read Connect Metadata
Add small browser-derived values to the params object when mount needs them:
const params = {
...(csrfToken ? { _csrf_token: csrfToken } : {}),
locale: document.documentElement.lang
}Connected capabilities implement
ConnectedMetadatatrait ConnectedMetadataMetadata supplied by a connected client. Values in connectParams are untrusted. and expose
connectParamsdef connectParams: Map[String, zio.json.ast.Json] as
Map[String, zio.json.ast.Json]. Match the phase, then decode and validate the
expected shape:
val locale = ctx.connection match
case Connection.Connected(capabilities) =>
capabilities.connectParams.get("locale").collect {
case Json.Str(value) => value
}
case Connection.Disconnected => NoneThe map is empty during disconnected HTTP rendering and contains the browser's
join parameters during the connected lifecycle. Treat every value as untrusted
client input: use the signed session or server-side state for identity,
authorization, and other security decisions. Do not set or depend on Phoenix's
internal keys such as _mounts and _track_static; their exact values and
reconnect behavior are protocol metadata, not application state.
Load Classpath Assets
For packaged applications, load the exact resources that the build placed below a classpath prefix:
assets <- StaticAssets.load(
StaticAssetConfig.classpath(
resourcePrefix = "public",
assets = Seq("app.css", "app.js")
)
)The StaticAssetConfig.classpathdef classpath(resourcePrefix: String, assets: Iterable[String], mountPath: zio.http.Path = ..., serveOriginals: Boolean = ..., classLoader: ClassLoader = ...): StaticAssetConfigConfigures an explicit set of classpath resources. source requires an
explicit asset list. StaticAssets.loaddef load(config: StaticAssetConfig): zio.package.Task[StaticAssets]Loads and validates an asset manifest. Bytes are read again when a request is served. reads every
configured asset and fails when one is missing. The default mount path is
/static.
Load A Directory
Use StaticAssetConfig.directorydef directory(root: java.nio.file.Path, mountPath: zio.http.Path = ..., serveOriginals: Boolean = ..., assets: Option[Iterable[String]] = ...): StaticAssetConfigConfigures assets stored below a filesystem directory. when assets are deployed
outside the application classpath:
import java.nio.file.Paths
assets <- StaticAssets.load(
StaticAssetConfig.directory(
root = Paths.get("/srv/my-app/public"),
assets = Some(Seq("app.css", "app.js"))
)
)Set assets = None to discover all regular files recursively below the root.
Supplying a list limits the manifest to those relative paths. Configured paths
must be normalized relative paths; empty segments, ., .., and backslashes
are rejected. Keep directory contents unchanged for the lifetime of the loaded
manifest: digests are calculated at load time while response bodies are read
from the source when requested.
Serve Digested Paths
Add StaticAssets.routesval routes: zio.http.Routes[Any, Nothing]GET and HEAD routes serving manifest entries below the configured mount path. to the application routes:
val routes = liveRoutes ++ assets.routesThe routes serve GET and HEAD below the configured mount path. Loading an
asset calculates a SHA-256 digest and inserts the full digest before its file
extension. StaticAssets.path("app.js")def path(asset: String): StringReturns the mounted, root-relative URL for an asset's digested path. therefore
returns a URL such as /static/app-<digest>.js; an unknown digest returns 404.
Current defaults serve digested responses with public, a one-year max-age,
and immutable, and serve original paths with no-cache. Both forms include a
strong ETag containing the digest. Set serveOriginals = false to make the
undigested path return 404. Query strings do not affect asset lookup.
Use pathOptiondef pathOption(asset: String): Option[String]Returns the mounted, root-relative digested URL when the asset is present. when an optional asset may be absent.
pathdef path(asset: String): StringReturns the mounted, root-relative URL for an asset's digested path. and
entrydef entry(asset: String): StaticAssetEntryReturns the load-time metadata for an asset. throw
for a name outside the loaded manifest.
Render Tracked Tags
Pass StaticAssetsclass StaticAssetsA loaded static asset manifest, URL/tag helper, and HTTP route set. to the root layout and render bundle tags in <head>:
headTag(
metaTag(charset := "utf-8"),
assets.trackedStylesheet("app.css"),
assets.trackedScript("app.js", defer := true, typ := "text/javascript")
)The tracked helpers StaticAssets.trackedStylesheetdef trackedStylesheet[Msg](asset: String, mods: Mod[Msg]*): HtmlElement[Msg]Renders a tracked stylesheet <link> using the asset's digested URL. and
also StaticAssets.trackedScriptdef trackedScript[Msg](asset: String, mods: Mod[Msg]*): HtmlElement[Msg]Renders a tracked <script> using the asset's digested URL. use the digested URL and
add phx-track-static. The untracked
stylesheetdef stylesheet[Msg](asset: String, mods: Mod[Msg]*): HtmlElement[Msg] and
scriptdef script[Msg](asset: String, mods: Mod[Msg]*): HtmlElement[Msg] helpers still use
digested URLs but omit that Phoenix marker. Use tracked helpers for the
application bundles whose change should be visible to the LiveView client.
Connected capabilities expose
staticChangeddef staticChanged: Boolean for
reacting to that tracking result, commonly by replacing stale connected state or
initiating a full reload. No connected metadata exists during disconnected
rendering. On a routed root socket join, it is true when the client's non-empty list
of tracked URLs differs from the server-rendered list; query strings, fragments,
and URL origins are ignored during comparison. Missing, malformed, or empty
client tracking metadata yields false, and the result remains stable for that
socket lifecycle. Therefore use it as a deployment-change hint, not proof that
assets loaded successfully or as a security signal.
The complete root layout and startup wiring are available in the quick start.
Related Tasks
Place bundle tags in the document shell with Layouts, live sessions, and mount aspects.
Diagnose a page that renders but never connects in Troubleshooting.
Verify the real client connection with Testing LiveViews.