Live sessions and mount aspects
Before you start
You need named route declarations, as in Routes, parameters, and navigation, and you should know that Scalive mounts a LiveView once for the HTTP response and again for the live connection (Lifecycle, state ownership, and reconnects).
This guide builds an account area: every page in it needs the signed-in user, shares a header, and one page also checks access to a workspace. The examples use these application types:
final case class SessionId(value: String) derives JsonCodec
final case class AuthClaims(sessionId: SessionId) derives JsonCodec
final case class CurrentUser(id: Long, name: String)
final case class AuthenticatedSession(sessionId: SessionId, currentUser: CurrentUser)
final case class WorkspaceAccess(workspaceId: String, canEdit: Boolean)
trait Authentication:
def authenticate(request: Request): IO[Response, AuthenticatedSession]
def resume(sessionId: SessionId): IO[LiveMountFailure, CurrentUser]
trait Workspaces:
def authorize(user: CurrentUser, workspaceId: String): IO[LiveRouteMountFailure, WorkspaceAccess]View source (documentation/fixtures/guides/src/sessions/Sessions.scala:11-22)
Group routes in a named live session
A named live session groups routes that share layouts and mount policy. Live navigation between its routes keeps the WebSocket connection; navigation to a route outside the session loads a new page. The account area is one session:
val account = Live
.session("account")
.withMountAspect(Aspects.currentUser)
.withLayout(AccountLayout())(
Routes.account.context((user: CurrentUser) => AccountLiveView(user)),
Routes.settings.context((user: CurrentUser) => SettingsLiveView(user)),
Routes.workspace
.withMountAspect(Aspects.workspaceAccess)
.context((context: (CurrentUser, WorkspaceAccess)) =>
WorkspaceLiveView(context._1, context._2)
)
)View source (documentation/fixtures/guides/src/sessions/Sessions.scala:90-103)
Live.session("account") names the group. Modifiers installed on the session
builder, here a mount aspect and a layout, apply to every route passed to it.
Pass the session to Live.router like a route.
Live session names must be unique in one router. A named live session is application structure, not a browser session or a login record; your application still owns login state, expiry, and revocation.
Derive session context before mount
A LiveSessionMountAspectclass LiveSessionMountAspect[R, -In, Claims, Ctx]Produces session context immediately before disconnected and connected LiveView mount.
runs before each mount of a route in its session. It can reject the visitor or
produce typed context, such as the current user:
val currentUser =
LiveSessionMountAspect.fromRequest[Authentication, AuthClaims, CurrentUser](
request =>
ZIO.serviceWithZIO[Authentication](_.authenticate(request.request)).map { session =>
AuthClaims(session.sessionId) -> session.currentUser
},
(claims, _) => ZIO.serviceWithZIO[Authentication](_.resume(claims.sessionId))
)View source (documentation/fixtures/guides/src/sessions/Sessions.scala:27-34)
The first function runs for the HTTP request. It authenticates the browser's cookies and returns two values: signed claims that travel to the live connection, and the context for this mount. The second function runs for the connected mount. It receives the claims and loads fresh context from them. Treat mount phases independently explains why the work is split this way.
Routes in the session receive the context through context, as in
Routes.account.context((user: CurrentUser) => AccountLiveView(user)). The
compiler rejects a route that needs context the session does not provide.
Layouts installed on the session can read the context too:
final class AccountLayout extends LiveLayout[Any, CurrentUser]:
def view[Msg](content: HtmlElement[Msg], ctx: LiveLayoutContext[Any, CurrentUser])
: HtmlElement[Msg] =
div(
headerTag(p(s"Signed in as ${ctx.context.name}")),
mainTag(content)
)View source (documentation/fixtures/guides/src/sessions/Sessions.scala:51-57)
Session aspects can be installed only on Live.session builders, through
withMountAspect or as the aspect passed to withAdmission. Aspects compose
from left to right with ++: each receives the preceding context and may add
another typed value. Prefer a small domain value such as CurrentUser over the
complete request or a general service container.
Derive fresh context for every route mount
A LiveRouteMountAspectclass LiveRouteMountAspect[R, A, -In, Ctx]Derives fresh typed route context before every disconnected or connected route mount.
checks or loads something for one destination. The workspace route checks that
the user may open the requested workspace:
val workspaceAccess =
LiveRouteMountAspect.make[Workspaces, String, CurrentUser, WorkspaceAccess] {
(destination, user) =>
ZIO.serviceWithZIO[Workspaces](_.authorize(user, destination.params))
}View source (documentation/fixtures/guides/src/sessions/Sessions.scala:38-42)
It receives a LiveRouteMountRequestclass LiveRouteMountRequest[+A](params: A, url: zio.http.URL)Stable input available to a LiveRouteMountAspect on every route mount.
with the route's typed parameters and URL, plus the context produced so far,
here the session's CurrentUser. Install it on a route builder with
withMountAspect:
Routes.workspace
.withMountAspect(Aspects.workspaceAccess)
.context((context: (CurrentUser, WorkspaceAccess)) =>
WorkspaceLiveView(context._1, context._2)
)View source (documentation/fixtures/guides/src/sessions/Sessions.scala:97-101)
The aspect runs for the HTTP render, the initial connected mount, and every live navigation to the route. Its result is derived again each time and is never serialized into a token.
A live patch is different: it keeps the current LiveView mounted and calls
handleParams, so it does not rerun the route aspect. If a patch can change the
protected resource, authorize it again in handleParams and at each sensitive
operation, or navigate instead so the destination runs its route aspect.
Choose the right boundary
Use the router for application-wide structure, a named live session for one coherent area such as the account pages, and a route modifier for one page. This keeps policy at the broadest boundary that needs it.
| Boundary | LiveSessionMountAspect | LiveRouteMountAspect |
|---|---|---|
| Installation scope | A named live session | One route declaration |
| Inputs | Phase-specific request and preceding session context | Typed destination parameters, destination URL, and preceding context |
| Timing | Disconnected mount, initial connected mount, and connected navigation within the named live session | Every disconnected or connected mount of that route, including connected navigation |
| Serialization | Minimal signed claims cross from HTTP to the connected lifecycle | No claims; output is never serialized |
| Failure boundary | HTTP Response when disconnected; LiveMountFailure when connected | LiveRouteMountFailure defines both HTTP and connected outcomes |
| Intended use | Identity and policy shared by a route group | Destination-specific loading, admission, and authorization |
Both kinds decide whether a lifecycle may start and produce typed context. Connected-turn guards reuse that context when policy must run again before later application work. Connected resource initializers attach cleanup-owned side effects after admission; they do not decide admission or produce context.
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 WebSocket join URL; it does not retain those cookies, headers, method, or body.
A session 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 for the initial join and connected navigation within the named live session. 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. HTTP-only cookies and headers must be consumed at an ordinary HTTP boundary or by a session aspect during disconnected mount; route aspects intentionally cannot read them.
Combine session and route context
Context accumulates from left to right. In the account session, the session
aspect produces CurrentUser and the route aspect adds WorkspaceAccess, so
the workspace route's factory receives (CurrentUser, WorkspaceAccess). One
more aspect would produce ((CurrentUser, WorkspaceAccess), NextContext).
The default ContextAppend discards only the initial Any identity context and
accumulates each later output as a pair without flattening. Define a custom
ContextAppend only when a domain-specific accumulated type is clearer and its
left projection can recover the preceding context.
Mount admission does not rerun merely because another application turn arrives
while a view remains mounted. Append a route
guardConnectedTurnsdef guardConnectedTurns[Result <: Unit](guard: Ctx => zio.IO[LiveConnectedTurnFailure, Result]): LiveRouteMountAspectBuilder[R, A, Need, Ctx]Appends a policy check before each connected application turn.
or session
guardConnectedTurnsdef guardConnectedTurns[Result <: Unit](guard: Ctx => zio.IO[LiveConnectedTurnFailure, Result]): LiveSessionBuilder.Admitted[R, Ctx]Appends a policy check before each connected application turn.
after the aspects or admission that produce its context when policy must be
checked before every later application turn. Session guards run before route
guards and are inherited by nested LiveViews. See
Lifecycle hooks for the complete
scope, ordering, and controlled outcomes.
To bind each physical connection to the application session, so a logout can
close every open tab, install the session aspect with withAdmission instead of
withMountAspect. The authentication guide
shows the complete flow.
Use failure semantics deliberately
Disconnected session-aspect failures are ordinary HTTP responses. Connected
session failures use LiveMountFailureenum LiveMountFailureStops connected mount admission before a LiveView lifecycle is installed.:
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 session authentication with
LiveSessionMountAspect.fromRequestdef fromRequest[R, Claims, Ctx](disconnected: LiveSessionMountRequest => zio.ZIO[R, zio.http.Response, (Claims, Ctx)], connected: (Claims, LiveSessionMountRequest) => zio.ZIO[R, LiveMountFailure, Ctx])(using evidence$1: zio.json.JsonCodec[Claims]): LiveSessionMountAspect[R, Any, Claims, Ctx]Creates a session 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.
Route aspects fail with
LiveRouteMountFailureenum LiveRouteMountFailureA route-mount rejection with safe HTTP and connected-admission semantics.. Its
standard outcomes preserve HTTP semantics while producing a controlled connected
rejection:
redirectuses a typed location in both phases;redirectUnsafeaccepts an unchecked URL and is only for trusted destinations;unauthorized,forbidden, andnotFoundrender HTTP 401, 403, and 404 respectively, while all reject connected admission as unauthorized; andcustomsupplies an explicit disconnectedResponseand connectedLiveMountFailurefor uncommon policies.
Use notFound when hiding record existence is part of policy, as the workspace
aspect's service does, forbidden when the HTTP distinction is useful, and
unauthorized for missing authentication. These mount outcomes do not replace
per-turn guards or domain authorization at the operation that reads or changes
protected state.
Attach connected resource initialization
Install withConnectedResources on the named live-session builder for every
root mounted in that group, or on a route builder for one destination. It is
available on plain, typed, and admitted session builders and on ordinary and
routed route builders. Each callback receives the builder's current context and
ConnectedResources, returning Task[Result] with Result <: Unit.
Install after the aspect that supplies the callback's required context. A plain
builder supplies Any; a later .context(factory) is not a context declaration
for an earlier initializer. If another aspect or admission is appended afterward,
the initializer retains its installation-time context via projection. Its
execution still waits for all session and route admission/context checks,
including those declared after it. It cannot provide context to later aspects
or return a handle to the page model. Capture services or use the supplied typed
context; the callback's Task does not introduce another environment requirement.
Modifiers compose additively: session initializers run first, then route initializers, in declaration order within each boundary. They run lazily before connected page mount, never during disconnected rendering or rejected admission. Page factories and render compilation may already have run. Callback failures are mount failures rather than controlled authentication rejections; execution stops and the same connected resource scope releases prior successful acquisitions.
Even a session-installed initializer belongs to each connected root lifecycle, not the session name, login session, or WebSocket. It runs anew on reconnect or navigation, not on patches or ordinary messages, and is not inherited by nested LiveViews. Use a service for shared session ownership. See route and session resources for the executable example, bounded acquisition/finalization requirements, and when to keep acquisition directly in the page's mount.
Related tasks
Add a complete login and logout flow with Authentication and sessions.
Wrap a session's pages in shared markup with Layouts.
Recheck connected policy with Lifecycle hooks.
Construct LiveViews from application dependencies with Services and dependency injection.
Check navigation across named live-session boundaries in Routes, parameters, and navigation.