Layouts, live sessions, and mount aspects
Prerequisites
Have at least two named Live route declarations and understand that Scalive mounts once for HTTP rendering and again for the live connection. Review Routes, parameters, and navigation if your routes are still assembled from ad hoc strings.
Choose The Right Boundary
Scalive separates three application-structure concerns:
a
LiveRootLayouttrait LiveRootLayout[-A, -Ctx]Declaratively renders and identifies the outer document shell. renders the outer HTML document and identifies routes that can share live navigation;a
LiveLayouttrait LiveLayout[-A, -Ctx]Declaratively wraps a LiveView while preserving its message type. wraps rendered LiveView content inside that document; anda named live session groups routes that share mount policy, layouts, token settings, and a connected-navigation boundary.
Use the router for application-wide structure, a named session for one coherent area such as authenticated account pages, and a route modifier for one page. This keeps policy close to the broadest boundary that actually needs it.
Install Root And Ordinary Layouts
The root layout owns <html>, <head>, assets, and <body>. Give it a stable
compatibility key:
val root = LiveRootLayout("application-root") { (content, pageTitle, _) =>
htmlRootTag(
headTag(liveTitle(pageTitle, default = "My application")),
bodyTag(content)
)
}Routes with the same root key may navigate over one connected live session. When the key changes, Scalive falls back to a fresh HTTP request instead of trying to reuse an incompatible document shell.
Ordinary layouts wrap the LiveView inside the selected root:
val applicationShell = LiveLayout[Any, Any] { (content, _) =>
div(
headerTag(a(href := "/", "My application")),
mainTag(content)
)
}
val router = Live.router
.withRootLayout(root)
.withLayout(applicationShell)Ordinary layouts are signal-backed view graphs. Their params, request, and
currentUrl context values are read-only Signals, so route-dependent chrome
updates without reconstructing the layout:
val routeShell = LiveLayout[WorkspaceId, CurrentUser] { (content, ctx) =>
div(
dataAttr("workspace") := ctx.params.map(_.value),
p("Signed in as ", ctx.context.name),
mainTag(content)
)
}Root-layout context remains value-backed because the root document is rendered only for the disconnected HTTP response; connected diffs patch the LiveView inside it.
Router layouts are outermost, followed by session layouts and route layouts. Within one level, registration order is preserved. Root layouts do not compose: a route root overrides a session root, which overrides the router root.
Group Routes In A Named Session
A named session applies common modifiers to several routes and defines which routes may use live navigation together:
val accountRoutes = Live
.session("account")
.withLayout(accountLayout)(
(live / "account") -> AccountLiveView(),
(live / "account" / "settings") -> SettingsLiveView()
)
val routes = router(accountRoutes)Session names must be unique in one router. Treat the name as application structure, not as a browser session identifier or authentication record. A live session groups route behavior; your service still owns login state, expiry, and revocation.
Derive Typed Context Before Mount
A LiveMountAspectclass LiveMountAspect[R, A, -In, Claims, Ctx]Produces context immediately before both disconnected and connected LiveView mount. runs before both
the disconnected HTTP mount and the fresh connected mount. It can reject the
request or produce typed context required by the route:
val account = Live
.session("account")
.withMountAspect(currentUser)(
(live / "account").context(AccountLiveView.apply)
)The context constructor receives the aspect result directly. The compiler
rejects a route whose constructor requires context that preceding aspects did
not provide.
Aspects compose from left to right with ++. Each aspect receives the preceding
context and may append another typed value. Prefer a small domain value such as
CurrentUser over passing the complete request or a general service container.
Treat Mount Phases Independently
The disconnected callback sees the browser's original HTTP request, including cookies and headers. The connected callback receives a request synthesized from the socket join URL; it does not retain those original cookies, headers, method, or body.
An aspect therefore returns two values during disconnected mount:
JSON-serializable claims that cross the phase boundary in the signed LiveView session; and
context used only by that disconnected LiveView instance.
The connected callback decodes the claims and independently produces fresh context. Claims are signed but not encrypted and remain visible to the client. Never put passwords, cookie values, access tokens, or other secrets in them. Transfer the smallest non-secret identifier and revalidate mutable authorization state before connected mount.
Use Failure Semantics Deliberately
Disconnected aspect failures are ordinary HTTP responses. Connected failures
use LiveMountFailureenum LiveMountFailureStops the connected LiveMountAspect phase.:
redirect to a typed location when the visitor can recover elsewhere;
reject as unauthorized when no navigation is appropriate; or
report stale state when the browser should reload.
Build authentication with
LiveMountAspect.fromRequestdef fromRequest[R, A, Claims, Ctx](disconnected: LiveMountRequest[A] => zio.ZIO[R, zio.http.Response, (Claims, Ctx)], connected: (Claims, LiveMountRequest[A]) => zio.ZIO[R, LiveMountFailure, Ctx])(using evidence$1: zio.json.JsonCodec[Claims]): LiveMountAspect[R, A, Any, Claims, Ctx]Creates an aspect that does not consume context from a preceding aspect.:
the disconnected callback validates the original request and returns minimal
signed claims, while the connected callback revalidates those claims and loads
fresh authorization context. Either callback can redirect to login using its
phase-specific failure type. Cookie validation, expiry, revocation, and claims
resumption remain application policy.
Continue with Authentication for a complete runnable flow.
Related Tasks
Add protected route context with Authentication and sessions.
Construct LiveViews from application dependencies with Services and dependency injection.
Check navigation across session boundaries in Routes, parameters, and navigation.