<!-- LLM_VERSION_INFO
FORMAT: text/markdown
CONTENT_TYPE: article
ORIGINAL_URL: https://fly.io/phoenix-files/live-session
ALTERNATE_VERSION: phoenix-files/live-session/index.html (text/html)
EXTRACTION_DATE: 2026-04-18T22:25:30.174Z

This is the markdown version with text-only content (images converted to alt-text).
For rich formatting with images, request the HTML version at: phoenix-files/live-session/index.html
-->

# Live Session in Phoenix

This is a post about the benefits we can get from using `live_session`, and the super powers we get from combining it with hooks. If you want to deploy your Phoenix LiveView app right now, then check out how to [get started](/content/docs/elixir/index.html). You could be up and running in minutes.

Phoenix LiveView often makes us feel like “wow, that was really fast!” and that is not a coincidence. Behind [_LiveView’s magic_](/content/blog/liveview-its-alive/index.html) there are a bunch of design decisions, but also interesting features we can use.

Under the umbrella of LiveView navigation we have the [live_session/3](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Router.html) macro to group live routes into live sessions. We can navigate between the routes in the same session over the existing websocket, without any additional HTTP request, thus making navigation between live routes even faster!

But `live_session` also has some other interesting ramifications. Today we’ll take a closer look at this feature and see that it has more going for it than is immediately obvious.

## Live Session in Action

When we navigate between different pages within our application using LiveView, every time we want to mount a new root LiveView, an HTTP request is made. Then, a connection with the server is established and the LiveView to be rendered is mounted. This means that a full page reload is being done.

If we look at our iex console, each of these steps is described in the logs:

Wouldn’t it be better if we could switch between LiveViews without making any HTTP requests (saving a couple ms in the process)? That’s when `live_session` comes into play!

Let’s start learning how to group different live routes using `live_session`.

In our `router.ex` file, we define a `live_session` and give it an atom as a name:

```elixir
scope "/", MyAppWeb do
  pipe_through :browser

live_session :default do
    live "/users/register", UserRegistrationLive, :new
    live "/users/log_in", UserLoginLive, :new
    live "/users/reset_password", UserForgotPasswordLive, :new
  end
end
```

This way, we can navigate between each route within our `:default` session without any additional HTTP requests, using [live_redirect](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Helpers.html#live_redirect/2) in our LiveView’s templates:

```elixir
<%= live_redirect "Register", to: "/users/register" %>
```

However, keep in mind that live navigation between LiveViews of different sessions using `live_redirect` is not possible since it will be necessary to do a full page reload, going through the plug pipeline.

Every time we navigate through our authentication system a new LiveView is mounted, but this time, as we can see in the logs, there is no additional HTTP request!

## Attaching Hooks to a Group of Routes

We can attach hooks in the mount lifecycle of each LiveView in the session just by combining the `live_session` macro with the `on_mount` callback.

First, we define `on_mount` functions that will be invoked on our LiveView’s mount. For example:

```elixir
defmodule MyAppWeb.UserAuth do

def on_mount(:default, _params, session, socket) do
    {:cont, socket}
  end

def on_mount(:ensure_user_is_admin, _params, session, socket) do
    if session["current_user_role"] == "admin" do
      {:cont, socket}
    else
      {:halt, socket}
    end
  end
end
```

Our `:ensure_user_is_admin` hook stops the mounting process if the user is not an admin and continues the process otherwise. These outcomes are accomplished by returning the tuples `{:halt, socket}` and `{:cont, socket}`, respectively.

Once our hook is defined, we can attach it to our session by using the `on_mount` option. We pass a tuple with our module’s name and the name of the hook we defined above:

```elixir
live_session :admin,
  on_mount: {MyAppWeb.UserAuth, :ensure_user_is_admin} do
  live "/settings", SettingsLive, :edit
end
```

What if we want to attach more than one hook per session? We can do it by defining a list of them:

You can also attach a specific hook to a particular LiveView by calling it at the beginning of the LiveView definition.

```elixir
live_session :admin,
  on_mount: [
    MyAppWeb.UserAuth,
    {MyAppWeb.UserAuth, :mount_current_user},
    {MyAppWeb.UserAuth, :ensure_user_is_admin}
  ] do

live "/settings", SettingsLive, :index
end
```

Did you notice that the first element of the `on_mount` list is different? If you call a hook without specifying a name, LiveView will default to the `:default` hook.

We can use this to add custom behavior to our routes or to define different authorization strategies.

## Security Considerations for Authorization Strategies

When we have a regular web application and we want to perform authentication and authorization operations on each of the routes in a scope; we usually define plug functions with the necessary validations, then we group them into pipelines, and finally, we pipe each of the routes through those security pipelines.

When we first mount a LiveView within a session or redirect between different sessions, an HTTP request is made. This means, all our routes will pipe through our security pipelines. That’s good!

However, what happens when we navigate between LiveViews within the same session? The same stateful connection is used to mount the new LiveView, and no HTTP requests are made. This means: no security pipelines at all!

So, if we want to secure each of the routes within a session, we must do it in another way. Fortunately, we’ve already learned how to do it: we just have to define all our authorization logic inside hooks!

Let’s do it:

```elixir
scope "/", MyAppWeb do
  pipe_through [:browser, :redirect_if_user_is_authenticated]

live_session :only_unauthenticated_users,
    on_mount: [
      {MyAppWeb.UserAuth, :redirect_if_user_is_authenticated}
    ] do
    live "/users/register", UserRegistrationLive, :new
    live "/users/log_in", UserLoginLive, :new
    live "/users/reset_password", UserForgotPasswordLive, :new
  end

post "/users/log_in", UserSessionController, :create
end
```

In order to secure all the routes in the `/` scope, we applied both security mechanisms we mentioned earlier. Plugs/pipelines to secure web requests, and hooks to secure each of the routes in the session, on the LiveView’s mount.

## Using a Different Root Layout for Grouped Like Routes

Navigating among live routes within a single live session lets you avoid the overhead of a full page reload. This prevents the root layout from changing, which you should keep in mind when grouping LiveViews into live sessions!

Conversely, putting routes into different live sessions forces a page reload through the plug pipeline on navigation between them. This is an opportunity that `live_session` doesn’t waste: it takes a `:root_layout` option to let you specify the root layout for the member LiveViews all at once.

Let’s see how we can do it:

```elixir
live_session :admin, root_layout: {MyAppWeb.LayoutView, "root_admin.html"} do
  live "/users/settings", UserSettingsLive, :edit
end
```

This way we can customize what we show to our users. For example, we can show one navigation bar for admin users and a different one for regular users.

## Bonus Example: Set Common Assigns at the Router Level

We can use `live_session` and `:on_mount` to set the common assigns for a group of live routes; all of this just in one place!

Setting assigns at the router level is useful to avoid setting assigns on every LiveView or forgetting to do it in some of them. For example, we can define different hooks to set our active menu item:

```elixir
defmodule Web.MenuAssign do
  @moduledoc """
  Ensures common `assigns` are applied to all LiveViews attaching this hook.
  """
  import Phoenix.LiveView

def on_mount(:settings, _params, _session, socket) do
    {:cont, assign(socket, :active_item, :settings)}
  end

def on_mount(:preferences, _params, _session, socket) do
    {:cont, assign(socket, :active_item, :preferences)}
  end
end
```

Then in the router, we block off a whole section of routes and they will get the `:active_item` assign set automatically.

```elixir
  live_session :preferences,
    on_mount: [
      Web.InitAssigns,
      {Web.InitAssigns, :require_current_user},
      {Web.MenuAssign, :preferences}
    ] do

scope "/preferences", as: :preferences do
      live "/avatar", AvatarPreferenceLive.Index, :index
      live "/notifications", NotificationsPreferenceLive.Index, :new
    end
  end
```

All routes within the `live_session :preferences` have set the assign `:active_item` with the value `:preferences`.

## Wrap Up

`live_session` by itself gives us faster navigation between live routes, but it gains super powers and becomes doubly useful in combination with the `on_mount` callback.

We can use these features to mark boundaries between routes that look or behave differently. In the first case, we can define a specific root layout for a group of LiveViews; in the second one, we can use hooks to modify the behavior of a session’s routes.
