Repository: phoenixframework/phoenix
Pull request #6181 (2025-04-09 14:36:15)

Add fetch() support to LongPoll when XMLHTTPRequest is not available
Hi, we noticed that when opening a socket in a ServiceWorker and using `LongPoll`, the socket fails because neither `XMLHTTPRequest` or `XDomainRequest` are available in the ServiceWorker scope.

I realise this is a bit of an edge case, as if you have ServiceWorker support, you probably already have native `WebSocket` support. In our instance, we sometimes fall back to LongPoll when network conditions block connections.

To work around this, I've added support for using `fetch`, which is in the global scope of a ServiceWorker. To ensure we don't break any existing setups, `LongPoll` requests still prefer `XDomainRequest` and `XMLHTTPRequest` in the same ordering as before; this only acts as a fallback when neither are available.

SteffenDE: Very nice! I'd love to see an extra test [here](https://github.com/phoenixframework/phoenix/blob/main/assets/test/longpoll_test.js) that mocks XMLHTTPRequest to undefined or whatever and then expects the respecting fetch calls.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 787a06082..fbc5b7582 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,7 @@ This release requires Erlang/OTP 25+.
 
   * Extend tailwindcss support in new apps with [daisyUI](https://daisyui.com/) for light/dark/system mode support for entire app, including core components
   * Simplify layout handling for new apps. Now there is only a single `root.html.heex` which wraps the render pipeline. Other dynamic layouts, like `app.html.heex` are called as needed within templates as regular function components
-  * Simplify core components and live generators to more closely match basic `phx.gen.html` crud. This serves as a better base for seasoned devs to start with, and lessons the amount of code newcomers need to get up to speed with on the basics
+  * Simplify core components and live generators to more closely match basic `phx.gen.html` crud. This serves as a better base for seasoned devs to start with, and lessens the amount of code newcomers need to get up to speed with on the basics
   * Introduce magic links (passwordless auth) and "sudo mode" to `mix phx.gen.auth` while simplifying the generated structure
   * Introduce scopes to Phoenix generators, designed to make secure data access the *default*, not something you remember (or forget) to do later
 
@@ -25,6 +25,32 @@ This release introduces deprecation warnings for several features that have been
   * Specifying layouts without modules, such as `put_layout(conn, :print)` or `put_layout(conn, html: :print)` is deprecated
   * The `:trailing_slash` option in `Phoenix.Router` has been deprecated in favor of using `Phoenix.VerifiedRoutes`. The overall usage of helpers will be deprecated in the future
 
+## 1.8.0-rc.3 (2025-05-07)
+  - [phx.gen.auth] allow configuring the scope's assign key in phx.gen.auth
+  - [phx.new] Do not override theme in root layout if explicitly set
+
+## Enhancements
+
+## 1.8.0-rc.2 (2025-04-29)
+
+## Big Fixes
+  - [phx.gen.live] only subscribe to pubsub if connected
+  - [phx.gen.auth] remove unused current_password field
+  - [phx.gen.auth] use context_app for scopes to fix generated scopes in umbrella apps
+
+## 1.8.0-rc.1 (2025-04-16)
+
+## Enhancements
+  - [phx.new] Support PORT in dev
+  - [phx.gen.auth] - Replace `utc_now/0 + truncate/1` with `utc_now/1`
+  - [phx.gen.auth] - Make dev mailbox link more obvious
+
+## Big Fixes
+  - [phx.new] Fix Tailwind custom variants for loading classes (#6194)
+  - [phx.new] Fix heroicons path for umbrella apps
+  - Fix crash when an open :show page gets a PubSub broadcast for items (#6197)
+  - Fix missing index for scoped resources (#6186)
+
 ## 1.8.0-rc.0 (2025-04-01) 🚀
 
 - First release candidate!
diff --git a/assets/js/phoenix/ajax.js b/assets/js/phoenix/ajax.js
index 63832d513..e53c75a52 100644
--- a/assets/js/phoenix/ajax.js
+++ b/assets/js/phoenix/ajax.js
@@ -9,10 +9,41 @@ export default class Ajax {
     if(global.XDomainRequest){
       let req = new global.XDomainRequest() // IE8, IE9
       return this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback)
-    } else {
+    } else if(global.XMLHttpRequest){
       let req = new global.XMLHttpRequest() // IE7+, Firefox, Chrome, Opera, Safari
       return this.xhrRequest(req, method, endPoint, headers, body, timeout, ontimeout, callback)
+    } else if(global.fetch && global.AbortController){
+      // Fetch with AbortController for modern browsers
+      return this.fetchRequest(method, endPoint, headers, body, timeout, ontimeout, callback);
+    } else {
+      throw new Error("No suitable XMLHttpRequest implementation found");
+    }
+  }
+
+  static fetchRequest(method, endPoint, headers, body, timeout, ontimeout, callback){
+    let options = {
+      method,
+      headers,
+      body,
+    };
+    let controller = null;
+    if(timeout){
+      controller = new AbortController();
+      const timeoutId = setTimeout(() => controller.abort(), timeout);
+      options.signal = controller.signal;
     }
+    global.fetch(endPoint, options)
+      .then(response => response.text())
+      .then(data => this.parseJSON(data))
+      .then(data => callback && callback(data))
+      .catch(err => {
+        if(err.name === "AbortError" && ontimeout){
+          ontimeout();
+        } else {
+          callback && callback(null);
+        }
+      });
+    return controller;
   }
 
   static xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback){
diff --git a/guides/asset_management.md b/guides/asset_management.md
index efefb358b..07a510797 100644
--- a/guides/asset_management.md
+++ b/guides/asset_management.md
@@ -39,15 +39,16 @@ If you want to import JavaScript dependencies, you have at least three options t
    import topbar from "topbar"
    ```
 
-   New applications use this third approach to import Heroicons, avoiding
-   vendoring a copy of all icons when you may only use a few or even none,
-   avoiding Node.js and `npm`, and tracking an explicit version that is easy to
-   update thanks to Mix. It is important to note that git dependencies cannot
-   be used by Hex packages, so if you intend to publish your project to Hex,
-   consider vendoring the files instead.
-
-Note that if you use third party JS package managers, you might need to adjust your deployment steps
-to properly include the packages. If you're using `mix phx.gen.release --docker`, have a look at the
+   New applications use this third approach to import icons, such as Heroicons,
+   to avoid vendoring a copy of all icons and to avoid additional system
+   dependencies such as `npm`, while you can still track explicit versions
+   thanks to Mix. It is important to note that git dependencies cannot be used
+   by Hex packages, so if you intend to publish your project to Hex, consider
+   alternatives approaches.
+
+Note that if you use third party JS package managers, you might need to adjust your
+deployment steps to properly include the packages. If you're using
+`mix phx.gen.release --docker`, have a look at the
 [documentation](Mix.Tasks.Phx.Gen.Release.html#module-docker) for further details.
 
 ## Images, fonts, and external files
@@ -215,3 +216,105 @@ $ mix deps.unlock tailwind
 ```
 
 You may optionally remove and delete the `heroicons` dependency as well.
+
+## Alternative icon libraries
+
+Phoenix ships with the [Heroicons](https://heroicons.com/) library for icons support.
+This is done by embedding icons as CSS classes, which guarantees only the icons actually
+used by your application are sent to the client, thanks to Tailwind.
+
+If you prefer to use an alternative icon set, it should be possible to adapt the
+code that embeds Heroicons to use another library. Let's see exactly how to do that
+using [Remix Icon](https://remixicon.com/) as an example:
+
+First replace the `heroicon` repository in your `mix.exs` by `remixicons`:
+
+```elixir
+{:remixicons,
+  github: "Remix-Design/RemixIcon",
+  sparse: "icons",
+  tag: "v4.6.0",
+  app: false,
+  compile: false,
+  depth: 1},
+```
+
+Then replace `assets/vendor/heroicons.js`, which traverses the heroicons dependency, by `assets/vendor/remixicons.js`, which traverses remix icons instead:
+
+```js
+const plugin = require("tailwindcss/plugin")
+const fs = require("fs")
+const path = require("path")
+
+module.exports = plugin(function({matchComponents, theme}) {
+  let baseDir = path.join(__dirname, "../../deps/remixicons/icons");
+  let values = {};
+  let icons = fs
+    .readdirSync(baseDir, { withFileTypes: true })
+    .filter((dirent) => dirent.isDirectory())
+    .map((dirent) => dirent.name);
+
+  icons.forEach((dir) => {
+    fs.readdirSync(path.join(baseDir, dir)).map((file) => {
+      let name = path.basename(file, ".svg");
+      values[name] = { name, fullPath: path.join(baseDir, dir, file) };
+    });
+  });
+
+  matchComponents(
+    {
+      ri: ({ name, fullPath }) => {
+        let content = fs
+          .readFileSync(fullPath)
+          .toString()
+          .replace(/\r?\n|\r/g, "");
+
+        return {
+          [`--ri-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
+          "-webkit-mask": `var(--ri-${name})`,
+          mask: `var(--ri-${name})`,
+          "background-color": "currentColor",
+          "vertical-align": "middle",
+          display: "inline-block",
+          width: theme("spacing.10"),
+          height: theme("spacing.10"),
+        };
+      },
+    },
+    { values },
+  );
+})
+```
+
+And then change `assets/css/app.css` to import your new plugin instead.
+
+Finally, update the `icon` function in `lib/my_app_web/components/core_components.ex`
+to match on `ri-` prefixes instead:
+
+```
+@doc """
+Renders a [Remix Icon](https://remixicon.com).
+
+You can customize the size and colors of the icons by
+setting width, height, and background color classes.
+
+## Examples
+
+    <.icon name="ri-github-fill" />
+    <.icon name="ri-github" class="ml-1 w-3 h-3 animate-spin" />
+"""
+attr :name, :string, required: true
+attr :class, :string, default: "size-5"
+
+def icon(%{name: "ri-" <> _} = assigns) do
+  ~H"""
+  <i class={[@name, @class]} aria-hidden="true"></i>
+  """
+end
+```
+
+Now replace the Heroicons in your application by Remix ones and you are good to go!
+
+The approach above may also work with other libraries, it is a matter of adapting
+the Tailwind plugin to traverse these libraries and generate the proper classes.
+Some iconsets may also be available as regular Hex packages too.
diff --git a/guides/authn_authz/mix_phx_gen_auth.md b/guides/authn_authz/mix_phx_gen_auth.md
index 843a05803..de5b74336 100644
--- a/guides/authn_authz/mix_phx_gen_auth.md
+++ b/guides/authn_authz/mix_phx_gen_auth.md
@@ -64,7 +64,7 @@ The following are notes about the generated authentication system.
 The generated code ships with an authentication module with a handful of plugs that fetch the current user, require authentication and so on. For instance, in an app named MyApp which had `mix phx.gen.auth Accounts User users` run on it, you will find a module named `MyAppWeb.UserAuth` with plugs such as:
 
   * `fetch_current_scope_for_user` - fetches the current user information if available and stores it as `:current_scope` assign
-  * `require_authenticated_user` - must be invoked after `fetch_current_user` and requires that a current user exists and is authenticated
+  * `require_authenticated_user` - must be invoked after `fetch_current_scope_for_user` and requires that a current user exists and is authenticated
   * `redirect_if_user_is_authenticated` - used for the few pages that must not be available to authenticated users (only generated for controller based authentication)
   * `require_sudo_mode` - used for pages that contain sensitive operations and enforces recent authentication
 
@@ -172,4 +172,4 @@ The following links describe the original implementation of the authentication s
   * José Valim's blog post - [An upcoming authentication solution for Phoenix](https://dashbit.co/blog/a-new-authentication-solution-for-phoenix)
   * Berenice Medel's blog post on generating LiveViews for authentication (rather than conventional Controllers & Views) - [Bringing Phoenix Authentication to Life](https://fly.io/phoenix-files/phx-gen-auth/)
   * [Original design spec](https://github.com/dashbitco/mix_phx_gen_auth_demo/blob/auth/README.md)
-  * [Pull request on bare Phoenix app][https://github.com/dashbitco/mix_phx_gen_auth_demo/pull/1]
+  * [Pull request on bare Phoenix app](https://github.com/dashbitco/mix_phx_gen_auth_demo/pull/1)
diff --git a/guides/authn_authz/scopes.md b/guides/authn_authz/scopes.md
index 0d5e4940e..c40066fa1 100644
--- a/guides/authn_authz/scopes.md
+++ b/guides/authn_authz/scopes.md
@@ -1,12 +1,10 @@
 # Scopes
 
-A scope is a data structure used to keep information about the current request or session, such as the current user logged in, the organization/company it belongs to, permissions, and so on. Think about it as a container that holds information that is required in the huge majority of pages in your application. It can also hold important request metadata, such as IP addresses.
+A scope is a data structure used to keep information about the current request or session, such as the current user, the user's organization/company, permissions, and so on. Think of a scope as a container with information required by the majority of pages in your application. A scope can also hold request metadata, such as IP addresses.
 
-Scopes also play a very important role in security. OWASP (Open Worldwide Application Security Project) lists "Broken access control" as the biggest security risk in web applications. That's because most data in an application is not publicly available. Instead, it most often belongs to a user, a team, or an organization. Therefore, it is extremely important that, when you query the database, your queries, inserts, updates, and deletes are properly scoped to the current user/team/organization.
+Scopes play an important role in security. [OWASP](https://owasp.org/) lists "Broken access control" as a [top-10 security risk](https://owasp.org/Top10/). Most application data is private for a user, a team, or an organization. Your database CRUD operations must be properly scoped to the current user/team/organization. Phoenix generators such as `mix phx.gen.html`, `mix phx.gen.json`, and `mix phx.gen.live` automatically use your custom scopes.
 
-By using scopes, you have a single data structure that contains all relevant information, which is then passed around so all of your operations are properly scoped. By defining your own scopes, Phoenix generators such as `mix phx.gen.html`, `mix phx.gen.json`, and `mix phx.gen.live` will automatically make sure all operations pertain to that scope, ensuring that all generated code is safe by default.
-
-Scopes are also flexible: you can have more than one scope in your application and choose the relevant scope when invoking the relevant generator. When you run `mix phx.gen.auth`, it will automatically generate a scope for you, but you may also add your own.
+Scopes are flexible. You can have more than one scope in your application and choose the specific scope when invoking a generator. When you run `mix phx.gen.auth`, it will automatically generate a scope for you, but you may also add your own.
 
 This guide will:
 
@@ -17,7 +15,7 @@ This guide will:
 
 ## phx.gen.auth
 
-When you invoke `mix phx.gen.auth`, it will generate a default scope for you. This scope ties the generated resources to the currently authenticated user. Let's see it in action:
+The task `mix phx.gen.auth` will generate a default scope. This scope ties the generated resources to the currently authenticated user. Let's see it in action:
 
 ```console
 $ mix phx.gen.auth Accounts User users
@@ -84,7 +82,7 @@ end
 
 ## Integration of scopes in the Phoenix generators
 
-If a default scope is defined in your application's config, the generators will generate scoped resources by default. The generated LiveViews / Controllers will automatically pass the scope to the context functions. `mix phx.gen.auth` automatically sets its scope as default, if there is not already a default scope defined:
+If a default scope is defined in your application's config, the generators will build scoped resources by default. The generated LiveViews / Controllers will automatically pass the scope to the context functions. `mix phx.gen.auth` automatically sets its scope as default, if there is not already a default scope defined:
 
 ```elixir
 # config/config.exs
diff --git a/guides/data_modelling/cross_context_boundaries.md b/guides/data_modelling/cross_context_boundaries.md
index 221d1cfb7..28268caa4 100644
--- a/guides/data_modelling/cross_context_boundaries.md
+++ b/guides/data_modelling/cross_context_boundaries.md
@@ -194,13 +194,15 @@ Now that our cart is associated to the items we place in it, let's set up the ca
   @doc false
   def changeset(cart_item, attrs) do
     cart_item
-    |> cast(attrs, [:price_when_carted, :quantity])
-    |> validate_required([:price_when_carted, :quantity])
+-   |> cast(attrs, [:price_when_carted, :quantity])
++   |> cast(attrs, [:quantity])
+-   |> validate_required([:price_when_carted, :quantity])
++   |> validate_required([:quantity])
 +   |> validate_number(:quantity, greater_than_or_equal_to: 0, less_than: 100)
   end
 ```
 
-First, we replaced the `cart_id` field with a standard `belongs_to` pointing at our `ShoppingCart.Cart` schema. Next, we replaced our `product_id` field by adding our first cross-context data dependency with a `belongs_to` for the `Catalog.Product` schema. Here, we intentionally coupled the data boundaries because it provides exactly what we need: an isolated context API with the bare minimum knowledge necessary to reference a product in our system. Next, we added a new validation to our changeset. With `validate_number/3`, we ensure any quantity provided by user input is between 0 and 100.
+First, we replaced the `cart_id` field with a standard `belongs_to` pointing at our `ShoppingCart.Cart` schema. Next, we replaced our `product_id` field by adding our first cross-context data dependency with a `belongs_to` for the `Catalog.Product` schema. Here, we intentionally coupled the data boundaries because it provides exactly what we need: an isolated context API with the bare minimum knowledge necessary to reference a product in our system. Second, we remove `price_when_carted` from the list of permitted attributes to ensure any price provided by user input is discarded. Finally, we added a new validation to our changeset. With `validate_number/3`, we ensure any quantity provided by user input is between 0 and 100.
 
 With our schemas in place, we can start integrating the new data structures and `ShoppingCart` context APIs into our web-facing features.
 
diff --git a/guides/data_modelling/your_first_context.md b/guides/data_modelling/your_first_context.md
index dc63e4d92..ec2286a33 100644
--- a/guides/data_modelling/your_first_context.md
+++ b/guides/data_modelling/your_first_context.md
@@ -40,7 +40,7 @@ Remember to update your repository by running migrations:
     $ mix ecto.migrate
 ```
 
-Phoenix generated the web files as expected in `lib/hello_web/`. We can also see our context files were generated inside a `lib/hello/catalog.ex` file and our product schema in the directory of the same name. Note the difference between `lib/hello` and `lib/hello_web`. We have a `Catalog` module to serve as the public API for product catalog functionality, as well as a `Catalog.Product` struct, which is an Ecto schema for casting and validating product data. Phoenix also provided web and context tests for us, it also included test helpers for creating entities via the `Hello.Catalog` context, which we'll look at later. For now, let's follow the instructions and add the route according to the console instructions, in `lib/hello_web/router.ex`:
+Phoenix generated the web files as expected in `lib/hello_web/`. We can also see our context functions were generated inside a `lib/hello/catalog.ex` file and our product schema file is placed in the directory of the same name. Note the difference between `lib/hello` and `lib/hello_web`. We have a `Catalog` module to serve as the public API for product catalog functionality, as well as a `Catalog.Product` struct, which is an Ecto schema for casting and validating product data. Phoenix also provided web and context tests for us, it also included test helpers for creating entities via the `Hello.Catalog` context, which we'll look at later. For now, let's follow the instructions and add the route according to the console instructions, in `lib/hello_web/router.ex`:
 
 ```diff
   scope "/", HelloWeb do
diff --git a/guides/deployment/fly.md b/guides/deployment/fly.md
index 2417b57bb..00fbafb00 100644
--- a/guides/deployment/fly.md
+++ b/guides/deployment/fly.md
@@ -1,7 +1,8 @@
 # Deploying on Fly.io
 
+The main goal for this guide is to get a Phoenix application running on [Fly.io](https://fly.io).
 
-Fly.io maintains their own guide for Elixir/Phoenix here: [Fly.io/docs/elixir/getting-started/](https://fly.io/docs/elixir/getting-started/) we will keep this guide up but for the latest and greatest check with them!
+Fly.io maintains their own guide for Elixir/Phoenix here: [Fly.io/docs/elixir/getting-started/](https://fly.io/docs/elixir/getting-started/). We will keep this guide up but for the latest and greatest check with them!
 
 ## What we'll need
 
@@ -13,10 +14,6 @@ You can just:
 $ mix phx.new my_app
 ```
 
-## Goals
-
-The main goal for this guide is to get a Phoenix application running on [Fly.io](https://fly.io).
-
 ## Sections
 
 Let's separate this process into a few steps, so we can keep track of where we are.
@@ -24,6 +21,7 @@ Let's separate this process into a few steps, so we can keep track of where we a
 - Install the Fly.io CLI
 - Sign up for Fly.io
 - Deploy the app to Fly.io
+- Clustering your application
 - Extra Fly.io tips
 - Helpful Fly.io resources
 
@@ -117,6 +115,32 @@ If everything looks good, open your app on Fly
 $ fly open
 ```
 
+## Clustering your application
+
+Elixir and the Erlang VM have the incredible ability to be clustered together and pass messages seamlessly between nodes. Phoenix comes with all of the knobs in place, you only need to set the appropriate environment variables before deploying.
+
+If you used `fly launch` to deploy your app, those environment variables are already in place, if not, open up `rel/env.ssh.eex` and add:
+
+```sh
+export ERL_AFLAGS="-proto_dist inet6_tcp"
+export RELEASE_DISTRIBUTION="name"
+export RELEASE_NODE="${FLY_APP_NAME}-${FLY_IMAGE_REF##*-}@${FLY_PRIVATE_IP}"
+
+export ECTO_IPV6="true"
+export DNS_CLUSTER_QUERY="${FLY_APP_NAME}.internal"
+```
+
+The first three environment variables are managed by Elixir and the Erlang/VM:
+
+  * `ERL_AFLAGS` - configures Erlang to use IPv6 for its distribution
+  * `RELEASE_DISTRIBUTION` - configures Erlang to named nodes
+  * `RELEASE_NODE` - attaches a name to the node, using Fly's app name and deploy reference
+
+The last two are handled by your `config/runtime.exs`:
+
+  * `ECTO_IPV6` - connect to the database using IPv6
+  * `DNS_CLUSTER_QUERY` - configures your app to find other nodes using the given DNS query
+
 ## Extra Fly.io tips
 
 ### Getting an IEx shell into a running node
@@ -151,32 +175,6 @@ iex(my_app@fdaa:0:1da8:a7b:ac4:b204:7e29:2)1>
 
 Now we have a running IEx shell into our node! You can safely disconnect using CTRL+C, CTRL+C.
 
-### Clustering your application
-
-Elixir and the BEAM have the incredible ability to be clustered together and pass messages seamlessly between nodes. Phoenix comes with all of the knobs in place, you only need to set the appropriate environment variables before deploying.
-
-If you used `fly launch` to deploy your app, those environement variables are already in place, if not, open up `rel/env.ssh.eex` and add:
-
-```sh
-export ERL_AFLAGS="-proto_dist inet6_tcp"
-export RELEASE_DISTRIBUTION="name"
-export RELEASE_NODE="${FLY_APP_NAME}-${FLY_IMAGE_REF##*-}@${FLY_PRIVATE_IP}"
-
-export ECTO_IPV6="true"
-export DNS_CLUSTER_QUERY="${FLY_APP_NAME}.internal"
-```
-
-The first three environment variables are managed by Elixir and the Erlang/VM:
-
-  * `ERL_AFLAGS` - configures Erlang to use IPv6 for its distribution
-  * `RELEASE_DISTRIBUTION` - configures Erlang to named nodes
-  * `RELEASE_NODE` - attaches a name to the node, using Fly's app name and deploy reference
-
-The last two are handled by your `config/runtime.exs`:
-
-  * `ECTO_IPV6` - connect to the database using IPv6
-  * `DNS_CLUSTER_QUERY` - configures your app to find other nodes using the given DNS query
-
 #### Running multiple instances
 
 There are two ways to run multiple instances.
diff --git a/guides/deployment/gigalixir.md b/guides/deployment/gigalixir.md
index 9eb7d1ed0..d074e704c 100644
--- a/guides/deployment/gigalixir.md
+++ b/guides/deployment/gigalixir.md
@@ -1,13 +1,11 @@
 # Deploying on Gigalixir
 
+Our main goal for this guide is to get a Phoenix application running on Gigalixir.
+
 ## What we'll need
 
 The only thing we'll need for this guide is a working Phoenix application. For those of us who need a simple application to deploy, please follow the [Up and Running guide](https://hexdocs.pm/phoenix/up_and_running.html).
 
-## Goals
-
-Our main goal for this guide is to get a Phoenix application running on Gigalixir.
-
 ## Steps
 
 Let's separate this process into a few steps, so we can keep track of where we are.
@@ -130,31 +128,17 @@ $ gigalixir config
 
 There's nothing we need to do to get our app running on Gigalixir, but for a production app, you probably want to enforce SSL.
 
-### Force HTTPS/SSL Connections
-
-To do that, see [Force SSL](https://hexdocs.pm/phoenix/using_ssl.html#force-ssl)
-
 ### Database Connection Security
-You may also want to use SSL for your database connection.
 
-For a free tier database, update your `Repo` config with:
+You may also want to use SSL for your database connection. In your `config/runtime.exs`:
 
 ```elixir
-ssl: true,
-ssl_opts: [
-  verify: :verify_none,
+ssl: [
+  verify: :verify_peer,
   cacerts: :public_key.cacerts_get()
 ]
 ```
 
-For a production database, use this instead:
-
-```elixir
-ssl: true,
-allowed_tls_versions: [:"tlsv1.2"],
-ssl_opts: [verify: :verify_none]
-```
-
 ## Deploy Time!
 
 Our project is now ready to be deployed on Gigalixir.
diff --git a/guides/deployment/heroku.md b/guides/deployment/heroku.md
index b65d13bef..832ef19ca 100644
--- a/guides/deployment/heroku.md
+++ b/guides/deployment/heroku.md
@@ -1,13 +1,11 @@
 # Deploying on Heroku
 
+Our main goal for this guide is to get a Phoenix application running on Heroku.
+
 ## What we'll need
 
 The only thing we'll need for this guide is a working Phoenix application. For those of us who need a simple application to deploy, please follow the [Up and Running guide](https://hexdocs.pm/phoenix/up_and_running.html).
 
-## Goals
-
-Our main goal for this guide is to get a Phoenix application running on Heroku.
-
 ## Limitations
 
 Heroku is a great platform and Elixir performs well on it. However, you may run into limitations if you plan to leverage advanced features provided by Elixir and Phoenix, such as:
@@ -26,7 +24,7 @@ Heroku is a great platform and Elixir performs well on it. However, you may run
 
 If you are just getting started, or you don't expect to use the features above, Heroku should be enough for your needs. For instance, if you are migrating an existing application running on Heroku to Phoenix, keeping a similar set of features, Elixir will perform just as well or even better than your current stack.
 
-If you want a platform-as-a-service without these limitations, try [Gigalixir](gigalixir.html). If you would rather deploy to a cloud platform, such as EC2, Google Cloud, etc, consider using `mix release`.
+If you want a platform-as-a-service without these limitations, there are alternatives listed in the sidebar and also generally available elsewhere. If you would rather deploy to a cloud platform, such as EC2, Google Cloud, etc, consider using `mix release`.
 
 ## Steps
 
diff --git a/guides/deployment/releases.md b/guides/deployment/releases.md
index 2d9a28887..ba3399872 100644
--- a/guides/deployment/releases.md
+++ b/guides/deployment/releases.md
@@ -1,13 +1,11 @@
 # Deploying with Releases
 
+Our main goal for this guide is to package your Phoenix application into a self-contained directory that includes the Erlang VM, Elixir, all of your code and dependencies. This package can then be dropped into a production machine.
+
 ## What we'll need
 
 The only thing we'll need for this guide is a working Phoenix application. For those of us who need a simple application to deploy, please follow the [Up and Running guide](up_and_running.html).
 
-## Goals
-
-Our main goal for this guide is to package your Phoenix application into a self-contained directory that includes the Erlang VM, Elixir, all of your code and dependencies. This package can then be dropped into a production machine.
-
 ## Releases, assemble!
 
 If you are not familiar with Elixir releases yet, we recommend you to read [Elixir's excellent docs](https://hexdocs.pm/mix/Mix.Tasks.Release.html) before continuing.
@@ -94,11 +92,9 @@ You can start the release by calling `_build/prod/rel/my_app/bin/my_app start`,
 
 Now you can get all of the files under the `_build/prod/rel/my_app` directory, package it, and run it in any production machine with the same OS and architecture as the one that assembled the release. For more details, check the [docs for `mix release`](https://hexdocs.pm/mix/Mix.Tasks.Release.html).
 
-But before we finish this guide, there is one more feature from releases that most Phoenix application will use, so let's talk about that.
-
-## Ecto migrations and custom commands
+## Ecto migrations
 
-A common need in production systems is to execute custom commands required to set up the production environment. One of such commands is precisely migrating the database. Since we don't have `Mix`, a *build* tool, inside releases, which are a production artifact, we need to bring said commands directly into the release.
+A common need in production systems is to execute custom commands required to set up the production environment. One of such commands is precisely migrating the database. Since we don't have `Mix`, a *build* tool, inside releases, which are production artifacts, we need to bring said commands directly into the release.
 
 The `phx.gen.release` command created the following `release.ex` file in your project `lib/my_app/release.ex`, with the following content:
 
@@ -124,6 +120,7 @@ defmodule MyApp.Release do
   end
 
   defp load_app do
+    Application.ensure_all_started(:ssl)
     Application.ensure_loaded(@app)
   end
 end
@@ -137,9 +134,13 @@ Now you can assemble a new release with `MIX_ENV=prod mix release` and you can i
 $ _build/prod/rel/my_app/bin/my_app eval "MyApp.Release.migrate"
 ```
 
-And that's it! If you peek inside the `migrate` script, you'll see it wraps exactly this invocation.
+And that's it! If you peek inside the `migrate` script, you'll see it wraps exactly this invocation. Depending on where you are deploying your application, you can invoke the `migrate` command separately, or you may want to change the `server` script to migrate your database before starting your app.
 
-You can use this approach to create any custom command to run in production. In this case, we used `load_app`, which calls `Application.ensure_loaded/1` to load the current application without starting it. However, you may want to write a custom command that starts the whole application. In such cases, `Application.ensure_all_started/1` must be used. Keep in mind, starting the application will start all processes for the current application, including the Phoenix endpoint. This can be circumvented by changing your supervision tree to not start certain children under certain conditions. For example, in the release commands file you could do:
+## Custom commands
+
+You can use the same approach used for migrations to create any custom command to run in production. The idea is that each command invokes `load_app`, which calls `Application.ensure_loaded/1` to load the current application without starting it.
+
+However, some commands may need to start the whole application. In such cases, `Application.ensure_all_started/1` must be used instead of `Application.load/1`. Keep in mind starting the application will all processes in its supervision tree, including the Phoenix endpoint. This can be circumvented by changing your supervision tree to not start certain children under certain conditions. For example, in the release commands file you could do:
 
 ```elixir
 defp start_app do
@@ -261,6 +262,82 @@ Where `my_app` is the name of your app. At the end, you will have an application
 
 A few points about configuring a containerized application:
 
-- If you run your app in a container, the `Endpoint` needs to be configured to listen on a "public" `:ip` address (like `0.0.0.0`) so that the app can be reached from outside the container. Whether the host should publish the container's ports to its own public IP or to localhost depends on your needs.
 - The more configuration you can provide at runtime (using `config/runtime.exs`), the more reusable your images will be across environments. In particular, secrets like database credentials and API keys should not be compiled into the image, but rather should be provided when creating containers based on that image. This is why the `Endpoint`'s `:secret_key_base` is configured in `config/runtime.exs` by default.
+
 - If possible, any environment variables that are needed at runtime should be read in `config/runtime.exs`, not scattered throughout your code. Having them all visible in one place will make it easier to ensure the containers get what they need, especially if the person doing the infrastructure work does not work on the Elixir code. Libraries in particular should never directly read environment variables; all their configuration should be handed to them by the top-level application, preferably [without using the application environment](https://hexdocs.pm/elixir/library-guidelines.html#avoid-application-configuration).
+
+## Clustering
+
+Elixir and the Erlang VM have the incredible ability to be clustered together and pass messages seamlessly between nodes. To enable clustering, we need two distinct features:
+
+* Node connection: different instances of the same service should communicate with each other. This is a feature of the Erlang VM.
+
+* Service discovery: for a given service, you must be able to find the IP address of all instances. Phoenix ships with `dns_cluster` to provide out-of-the-box DNS-based service discovery but alternative methods may be used.
+
+### DNS Discovery
+
+Your clustering configuration is typically added to `rel/env.sh.eex`. This is a file that is executed before you release starts, and it is a perfect place to configure your application runtime based on your deployment environment. Here is a general skeleton:
+
+```sh
+# Uncomment if IPv6 is required
+# export ECTO_IPV6="true"
+# export ERL_AFLAGS="-proto_dist inet6_tcp"
+
+# Erlang uses a port mapper daemon on each node,
+# it by default runs on port 4369
+export ERL_EPMD_PORT=4369
+
+# Use the ports 4370-4372 for nodes to communicate.
+export ERL_AFLAGS="-kernel inet_dist_listen_min 4370 inet_dist_listen_max 4372"
+
+export RELEASE_DISTRIBUTION="name"
+export RELEASE_NODE="app-${PLATFORM_DEPLOYMENT_SHA}@${PLATFORM_DEPLOYMENT_IP}"
+export DNS_CLUSTER_QUERY="your-app.internal"
+```
+
+The script above is doing a couple things:
+
+* It configures your app to use ports 4369, 4370, 4371, and 4372 for communication. You may need to explicitly expose those as internal TCP ports in your deployment platform (in addition to the HTTP port of your choice)
+
+* It then configures your app to use fully qualified names. The name of each app will include the current deployment sha as `PLATFORM_DEPLOYMENT_SHA` (the name of the exact environment variable is platform dependent), so each deployment establishes its own cluster, and the current IP as `PLATFORM_DEPLOYMENT_IP` (also platform specific). If the IP is not available, you may be able to compute it as `NODE_IP=hostname | tr -d ' '`
+
+* Then finally you define a DNS query which will be used to find the IPs of the other instances
+
+Some platforms, such as [Fly.io](https://fly.io/docs/networking/private-networking/), [Railway](https://docs.railway.com/guides/private-networking), and [Render](https://render.com/docs/private-network#direct-ip-communication-advanced), provide private networks with DNS querying out of the box. You only need to adapt the `DNS_CLUSTER_QUERY` variable accordingly.
+
+Other platforms, such as [Digital Ocean App Platform](https://www.digitalocean.com/products/app-platform) and [Northflank](https://northflank.com/features/platform), allow nodes to directly connect to each other, but they do not provide DNS service discovery. In this next section, we explore different service discovery mechanisms.
+
+### Alternative discovery mechanisms
+
+While not all platforms support DNS queries for service discovery, there are many alternative strategies for connecting your nodes together. Please checkout the following libraries:
+
+  * [libcluster](https://github.com/bitwalker/libcluster) - provides strategies for connecting your nodes using gossip protocols, kubernetes, ec2, and others
+
+  * [libcluster_postgres](https://github.com/supabase/libcluster_postgres/) - a plugin for `libcluster` which uses PostgreSQL for node discovery. Given most applications already use a database, and likely PostgreSQL, this is a suitable option which does not require additional setup
+
+When using the libraries above, you can likely remove `dns_query` from your application dependencies.
+
+### `epmd`-less deployment
+
+In the snippet above, we used ports 4369, 4370, 4371, and 4372. However, the Erlang VM allows running the distribution over a fixed port, also known as `epmd`-less deployments. To enable such, do this.
+
+Remove the lines:
+
+```sh
+export ERL_EPMD_PORT=4369
+export ERL_AFLAGS="-kernel inet_dist_listen_min 4370 inet_dist_listen_max 4372"
+```
+
+Add a file rel/vm.args.eex with the following:
+
+```
+-start_epmd false -erl_epmd_port 6789
+```
+
+Add a file rel/remote.vm.args.eex with the following:
+
+```
+-start_epmd false -erl_epmd_port 6789 -dist_listen false
+```
+
+And now only port 6789 (in addition to the HTTP one) needs to be exposed internally between instances.
\ No newline at end of file
diff --git a/guides/howto/custom_error_pages.md b/guides/howto/custom_error_pages.md
index 49622cad6..a1a965f36 100644
--- a/guides/howto/custom_error_pages.md
+++ b/guides/howto/custom_error_pages.md
@@ -50,7 +50,7 @@ Before we dive into this, let's see what the rendered `404 Not Found` message lo
 import Config
 
 config :hello, HelloWeb.Endpoint,
-  http: [port: 4000],
+  ...,
   debug_errors: false,
   code_reloader: true,
   ...
diff --git a/guides/introduction/installation.md b/guides/introduction/installation.md
index a3fa0aa39..3447e6af3 100644
--- a/guides/introduction/installation.md
+++ b/guides/introduction/installation.md
@@ -8,6 +8,8 @@ In order to build a Phoenix application, we will need a few dependencies install
 
 Please take a look at this list and make sure to install anything necessary for your system. Having dependencies installed in advance can prevent frustrating problems later on.
 
+If you just want to get started quickly, the [Up and Running](up_and_running.md) page includes a link to Phoenix Express, which will get Erlang, Elixir and Phoenix installed and running in seconds.
+
 ## Elixir 1.15 or later
 
 Phoenix is written in Elixir, and our application code will also be written in Elixir. We won't get far in a Phoenix app without it! The Elixir site maintains a great [Installation Page](https://elixir-lang.org/install.html) to help.
diff --git a/guides/introduction/up_and_running.md b/guides/introduction/up_and_running.md
index ff49af148..9ac72ad32 100644
--- a/guides/introduction/up_and_running.md
+++ b/guides/introduction/up_and_running.md
@@ -18,9 +18,7 @@ For Windows PowerShell:
 > curl.exe -fsSO https://new.phoenixframework.org/myapp.bat; .\myapp.bat
 ```
 
-The above will install Erlang, Elixir, and Phoenix, and generate a fresh Phoenix application. It will also automatically pick one of PostgreSQL or MySQL as the database, and fallback to SQLite if none of them are available. Once the command above completes, it will open up a Phoenix application, with the steps necessary to complete your installation.
-
-> Your Phoenix application name is taken from the path.
+The above will install Erlang, Elixir, and Phoenix, and generate a fresh Phoenix application. It will also automatically pick one of PostgreSQL or MySQL as the database, and fallback to SQLite if none of them are available. Once the command above completes, it will open up a Phoenix application, with the steps necessary to complete your installation. Note your Phoenix application name is taken from the path.
 
 If your operating system is not supported, or the command above fails, don't fret! You can still start your Phoenix application using `mix phx.new`.
 
diff --git a/guides/real_time/channels.md b/guides/real_time/channels.md
index 67319c21b..7436cda32 100644
--- a/guides/real_time/channels.md
+++ b/guides/real_time/channels.md
@@ -42,7 +42,7 @@ All the client needs is a suitable library; see the [Client Libraries](#client-l
 Each client library communicates using one of the "transports" that Channels understand.
 Currently, that's either Websockets or long polling, but other transports may be added in the future.
 
-Unlike stateless HTTP connections, Channels support long-lived connections, each backed by a lightweight BEAM process, working in parallel and maintaining its own state.
+Unlike stateless HTTP connections, Channels support long-lived connections, each backed by a lightweight Erlang VM process, working in parallel and maintaining its own state.
 
 This architecture scales well; Phoenix Channels [can support millions of subscribers with reasonable latency on a single box](https://phoenixframework.org/blog/the-road-to-2-million-websocket-connections), passing hundreds of thousands of messages per second.
 And that capacity can be multiplied by adding more nodes to the cluster.
@@ -158,7 +158,7 @@ PubSub is provided by the `Phoenix.PubSub` module. Interested parties can receiv
 This is useful to broadcast messages on channel and also for application development in general. For instance, letting all connected [live views](https://github.com/phoenixframework/phoenix_live_view) to know that a new comment has been added to a post.
 
 The PubSub system takes care of getting messages from one node to another so that they can be sent to all subscribers across the cluster.
-By default, this is done using [Phoenix.PubSub.PG2](https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.PG2.html), which uses native BEAM messaging.
+By default, this is done using [Phoenix.PubSub.PG2](https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.PG2.html), which uses native Erlang VM messaging.
 
 If your deployment environment does not support distributed Elixir or direct communication between servers, Phoenix also ships with a [Redis Adapter](https://hexdocs.pm/phoenix_pubsub_redis/Phoenix.PubSub.Redis.html) that uses Redis to exchange PubSub data. Please see the [Phoenix.PubSub docs](https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html) for more information.
 
diff --git a/installer/mix.exs b/installer/mix.exs
index 3f25458be..9e03442c0 100644
--- a/installer/mix.exs
+++ b/installer/mix.exs
@@ -6,7 +6,7 @@ end
 defmodule Phx.New.MixProject do
   use Mix.Project
 
-  @version "1.8.0-rc.0"
+  @version "1.8.0-rc.3"
   @scm_url "https://github.com/phoenixframework/phoenix"
 
   # If the elixir requirement is updated, we need to update:
diff --git a/installer/templates/phx_assets/app.css b/installer/templates/phx_assets/app.css
index 3ff6aabd2..50e9202dc 100644
--- a/installer/templates/phx_assets/app.css
+++ b/installer/templates/phx_assets/app.css
@@ -79,8 +79,8 @@
   --color-success-content: oklch(98% 0.014 180.72);
   --color-warning: oklch(66% 0.179 58.318);
   --color-warning-content: oklch(98% 0.022 95.277);
-  --color-error: oklch(65% 0.241 354.308);
-  --color-error-content: oklch(97% 0.014 343.198);
+  --color-error: oklch(58% 0.253 17.585);
+  --color-error-content: oklch(96% 0.015 12.422);
   --radius-selector: 0.25rem;
   --radius-field: 0.25rem;
   --radius-box: 0.5rem;
@@ -92,11 +92,11 @@
 }
 
 /* Add variants based on LiveView classes */
-@custom-variant phx-click-loading ([".phx-click-loading&", ".phx-click-loading &"]);
-@custom-variant phx-submit-loading ([".phx-submit-loading&", ".phx-submit-loading &"]);
-@custom-variant phx-change-loading ([".phx-change-loading&", ".phx-change-loading &"]);
+@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
+@custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &);
+@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);
 
 /* Make LiveView wrapper divs transparent for layout */
-[data-phx-root-id] { display: contents }
+[data-phx-session] { display: contents }
 
 /* This file is for your main application CSS */
diff --git a/installer/templates/phx_single/config/dev.exs b/installer/templates/phx_single/config/dev.exs
index e03e2a297..5b5a2e52b 100644
--- a/installer/templates/phx_single/config/dev.exs
+++ b/installer/templates/phx_single/config/dev.exs
@@ -10,10 +10,10 @@ config :<%= @app_name %>, <%= @endpoint_module %>,<%= if @inside_docker_env? do
   # Bind to 0.0.0.0 to expose the server to the docker host machine.
   # This makes make the service accessible from any network interface.
   # Change to `ip: {127, 0, 0, 1}` to allow access only from the server machine.
-  http: [ip: {0, 0, 0, 0}, port: 4000],<% else %>
+  http: [ip: {0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "4000")],<% else %>
   # Binding to loopback ipv4 address prevents access from other machines.
   # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
-  http: [ip: {127, 0, 0, 1}, port: 4000],<% end %>
+  http: [ip: {127, 0, 0, 1}, port: String.to_integer(System.get_env("PORT") || "4000")],<% end %>
   check_origin: false,
   code_reloader: true,
   debug_errors: true,
@@ -53,7 +53,7 @@ config :<%= @app_name %>, <%= @endpoint_module %>,
     patterns: [
       ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",<%= if @gettext do %>
       ~r"priv/gettext/.*(po)$",<% end %>
-      ~r"lib/<%= @lib_web_name %>/(controllers|live|components)/.*(ex|heex)$"
+      ~r"lib/<%= @lib_web_name %>/(?:controllers|live|components|router)/?.*\.(ex|heex)$"
     ]
   ]<% end %>
 
diff --git a/installer/templates/phx_static/default.css b/installer/templates/phx_static/default.css
index 66836523a..40e1e8b93 100644
--- a/installer/templates/phx_static/default.css
+++ b/installer/templates/phx_static/default.css
@@ -1488,10 +1488,10 @@
     width: 1rem;
     height: 1rem;
   }
-  .hero-exclamation-circle-mini {
-    --hero-exclamation-circle-mini: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2020%2020%22%20fill%3D%22currentColor%22%20aria-hidden%3D%22true%22%20data-slot%3D%22icon%22%3E%20%20%3Cpath%20fill-rule%3D%22evenodd%22%20d%3D%22M18%2010a8%208%200%201%201-16%200%208%208%200%200%201%2016%200Zm-8-5a.75.75%200%200%201%20.75.75v4.5a.75.75%200%200%201-1.5%200v-4.5A.75.75%200%200%201%2010%205Zm0%2010a1%201%200%201%200%200-2%201%201%200%200%200%200%202Z%22%20clip-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E');
-    -webkit-mask: var(--hero-exclamation-circle-mini);
-    mask: var(--hero-exclamation-circle-mini);
+  .hero-exclamation-circle {
+    --hero-exclamation-circle: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2020%2020%22%20fill%3D%22currentColor%22%20aria-hidden%3D%22true%22%20data-slot%3D%22icon%22%3E%20%20%3Cpath%20fill-rule%3D%22evenodd%22%20d%3D%22M18%2010a8%208%200%201%201-16%200%208%208%200%200%201%2016%200Zm-8-5a.75.75%200%200%201%20.75.75v4.5a.75.75%200%200%201-1.5%200v-4.5A.75.75%200%200%201%2010%205Zm0%2010a1%201%200%201%200%200-2%201%201%200%200%200%200%202Z%22%20clip-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E');
+    -webkit-mask: var(--hero-exclamation-circle);
+    mask: var(--hero-exclamation-circle);
     mask-repeat: no-repeat;
     background-color: currentColor;
     vertical-align: middle;
@@ -1499,10 +1499,10 @@
     width: 1.25rem;
     height: 1.25rem;
   }
-  .hero-information-circle-mini {
-    --hero-information-circle-mini: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2020%2020%22%20fill%3D%22currentColor%22%20aria-hidden%3D%22true%22%20data-slot%3D%22icon%22%3E%20%20%3Cpath%20fill-rule%3D%22evenodd%22%20d%3D%22M18%2010a8%208%200%201%201-16%200%208%208%200%200%201%2016%200Zm-7-4a1%201%200%201%201-2%200%201%201%200%200%201%202%200ZM9%209a.75.75%200%200%200%200%201.5h.253a.25.25%200%200%201%20.244.304l-.459%202.066A1.75%201.75%200%200%200%2010.747%2015H11a.75.75%200%200%200%200-1.5h-.253a.25.25%200%200%201-.244-.304l.459-2.066A1.75%201.75%200%200%200%209.253%209H9Z%22%20clip-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E');
-    -webkit-mask: var(--hero-information-circle-mini);
-    mask: var(--hero-information-circle-mini);
+  .hero-information-circle {
+    --hero-information-circle: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2020%2020%22%20fill%3D%22currentColor%22%20aria-hidden%3D%22true%22%20data-slot%3D%22icon%22%3E%20%20%3Cpath%20fill-rule%3D%22evenodd%22%20d%3D%22M18%2010a8%208%200%201%201-16%200%208%208%200%200%201%2016%200Zm-7-4a1%201%200%201%201-2%200%201%201%200%200%201%202%200ZM9%209a.75.75%200%200%200%200%201.5h.253a.25.25%200%200%201%20.244.304l-.459%202.066A1.75%201.75%200%200%200%2010.747%2015H11a.75.75%200%200%200%200-1.5h-.253a.25.25%200%200%201-.244-.304l.459-2.066A1.75%201.75%200%200%200%209.253%209H9Z%22%20clip-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E');
+    -webkit-mask: var(--hero-information-circle);
+    mask: var(--hero-information-circle);
     mask-repeat: no-repeat;
     background-color: currentColor;
     vertical-align: middle;
@@ -1554,10 +1554,10 @@
     width: 1rem;
     height: 1rem;
   }
-  .hero-x-mark-solid {
-    --hero-x-mark-solid: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22currentColor%22%20aria-hidden%3D%22true%22%20data-slot%3D%22icon%22%3E%20%20%3Cpath%20fill-rule%3D%22evenodd%22%20d%3D%22M5.47%205.47a.75.75%200%200%201%201.06%200L12%2010.94l5.47-5.47a.75.75%200%201%201%201.06%201.06L13.06%2012l5.47%205.47a.75.75%200%201%201-1.06%201.06L12%2013.06l-5.47%205.47a.75.75%200%200%201-1.06-1.06L10.94%2012%205.47%206.53a.75.75%200%200%201%200-1.06Z%22%20clip-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E');
-    -webkit-mask: var(--hero-x-mark-solid);
-    mask: var(--hero-x-mark-solid);
+  .hero-x-mark {
+    --hero-x-mark: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22currentColor%22%20aria-hidden%3D%22true%22%20data-slot%3D%22icon%22%3E%20%20%3Cpath%20fill-rule%3D%22evenodd%22%20d%3D%22M5.47%205.47a.75.75%200%200%201%201.06%200L12%2010.94l5.47-5.47a.75.75%200%201%201%201.06%201.06L13.06%2012l5.47%205.47a.75.75%200%201%201-1.06%201.06L12%2013.06l-5.47%205.47a.75.75%200%200%201-1.06-1.06L10.94%2012%205.47%206.53a.75.75%200%200%201%200-1.06Z%22%20clip-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E');
+    -webkit-mask: var(--hero-x-mark);
+    mask: var(--hero-x-mark);
     mask-repeat: no-repeat;
     background-color: currentColor;
     vertical-align: middle;
@@ -2288,7 +2288,7 @@
     }
   }
 }
-[data-phx-root-id] {
+[data-phx-session] {
   display: contents;
 }
 @layer base {
@@ -2460,8 +2460,8 @@
     --color-success-content: oklch(98% 0.014 180.72);
     --color-warning: oklch(66% 0.179 58.318);
     --color-warning-content: oklch(98% 0.022 95.277);
-    --color-error: oklch(65% 0.241 354.308);
-    --color-error-content: oklch(97% 0.014 343.198);
+    --color-error: oklch(58% 0.253 17.585);
+    --color-error-content: oklch(96% 0.015 12.422);
     --radius-selector: 0.25rem;
     --radius-field: 0.25rem;
     --radius-box: 0.5rem;
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs
index 682c1d2ce..3006e76fd 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs
@@ -10,10 +10,10 @@ config :<%= @web_app_name %>, <%= @endpoint_module %>,<%= if @inside_docker_env?
   # Bind to 0.0.0.0 to expose the server to the docker host machine.
   # This makes make the service accessible from any network interface.
   # Change to `ip: {127, 0, 0, 1}` to allow access only from the server machine.
-  http: [ip: {0, 0, 0, 0}, port: 4000],<% else %>
+  http: [ip: {0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "4000")],<% else %>
   # Binding to loopback ipv4 address prevents access from other machines.
   # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
-  http: [ip: {127, 0, 0, 1}, port: 4000],<% end %>
+  http: [ip: {127, 0, 0, 1}, port: String.to_integer(System.get_env("PORT") || "4000")],<% end %>
   check_origin: false,
   code_reloader: true,
   debug_errors: true,
@@ -53,7 +53,7 @@ config :<%= @web_app_name %>, <%= @endpoint_module %>,
     patterns: [
       ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",<%= if @gettext do %>
       ~r"priv/gettext/.*(po)$",<% end %>
-      ~r"lib/<%= @web_app_name %>/(controllers|live|components)/.*(ex|heex)$"
+      ~r"lib/<%= @web_app_name %>/(?:controllers|live|components|router)/?.*\.(ex|heex)$"
     ]
   ]<% end %>
 
diff --git a/installer/templates/phx_web/components/core_components.ex b/installer/templates/phx_web/components/core_components.ex
index d7081a5c4..bb915097e 100644
--- a/installer/templates/phx_web/components/core_components.ex
+++ b/installer/templates/phx_web/components/core_components.ex
@@ -64,15 +64,15 @@ defmodule <%= @web_namespace %>.CoreComponents do
         @kind == :info && "alert-info",
         @kind == :error && "alert-error"
       ]}>
-        <.icon :if={@kind == :info} name="hero-information-circle-mini" class="size-5 shrink-0" />
-        <.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="size-5 shrink-0" />
+        <.icon :if={@kind == :info} name="hero-information-circle" class="size-5 shrink-0" />
+        <.icon :if={@kind == :error} name="hero-exclamation-circle" class="size-5 shrink-0" />
         <div>
           <p :if={@title} class="font-semibold">{@title}</p>
           <p>{msg}</p>
         </div>
         <div class="flex-1" />
         <button type="button" class="group self-start cursor-pointer" aria-label=<%= maybe_heex_attr_gettext.("close", @gettext) %>>
-          <.icon name="hero-x-mark-solid" class="size-5 opacity-40 group-hover:opacity-70" />
+          <.icon name="hero-x-mark" class="size-5 opacity-40 group-hover:opacity-70" />
         </button>
       </div>
     </div>
@@ -88,7 +88,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
       <.button phx-click="go" variant="primary">Send!</.button>
       <.button navigate={~p"/"}>Home</.button>
   """
-  attr :rest, :global, include: ~w(href navigate patch method)
+  attr :rest, :global, include: ~w(href navigate patch method download)
   attr :variant, :string, values: ~w(primary)
   slot :inner_block, required: true
 
@@ -155,6 +155,8 @@ defmodule <%= @web_namespace %>.CoreComponents do
   attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
   attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
   attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
+  attr :class, :string, default: nil, doc: "the input class to use over defaults"
+  attr :error_class, :string, default: nil, doc: "the input error class to use over defaults"
 
   attr :rest, :global,
     include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
@@ -181,14 +183,14 @@ defmodule <%= @web_namespace %>.CoreComponents do
     <fieldset class="fieldset mb-2">
       <label>
         <input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
-        <span class="fieldset-label">
+        <span class="label">
           <input
             type="checkbox"
             id={@id}
             name={@name}
             value="true"
             checked={@checked}
-            class="checkbox checkbox-sm"
+            class={@class || "checkbox checkbox-sm"}
             {@rest}
           />{@label}
         </span>
@@ -202,11 +204,11 @@ defmodule <%= @web_namespace %>.CoreComponents do
     ~H"""
     <fieldset class="fieldset mb-2">
       <label>
-        <span :if={@label} class="fieldset-label mb-1">{@label}</span>
+        <span :if={@label} class="label mb-1">{@label}</span>
         <select
           id={@id}
           name={@name}
-          class={["w-full select", @errors != [] && "select-error"]}
+          class={[@class || "w-full select", @errors != [] && (@error_class || "select-error")]}
           multiple={@multiple}
           {@rest}
         >
@@ -223,11 +225,14 @@ defmodule <%= @web_namespace %>.CoreComponents do
     ~H"""
     <fieldset class="fieldset mb-2">
       <label>
-        <span :if={@label} class="fieldset-label mb-1">{@label}</span>
+        <span :if={@label} class="label mb-1">{@label}</span>
         <textarea
           id={@id}
           name={@name}
-          class={["w-full textarea", @errors != [] && "textarea-error"]}
+          class={[
+            @class || "w-full textarea",
+            @errors != [] && (@error_class || "textarea-error")
+          ]}
           {@rest}
         >{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
       </label>
@@ -241,13 +246,16 @@ defmodule <%= @web_namespace %>.CoreComponents do
     ~H"""
     <fieldset class="fieldset mb-2">
       <label>
-        <span :if={@label} class="fieldset-label mb-1">{@label}</span>
+        <span :if={@label} class="label mb-1">{@label}</span>
         <input
           type={@type}
           name={@name}
           id={@id}
           value={Phoenix.HTML.Form.normalize_value(@type, @value)}
-          class={["w-full input", @errors != [] && "input-error"]}
+          class={[
+            @class || "w-full input",
+            @errors != [] && (@error_class || "input-error")
+          ]}
           {@rest}
         />
       </label>
@@ -260,7 +268,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
   defp error(assigns) do
     ~H"""
     <p class="mt-1.5 flex gap-2 items-center text-sm text-error">
-      <.icon name="hero-exclamation-circle-mini" class="size-5" />
+      <.icon name="hero-exclamation-circle" class="size-5" />
       {render_slot(@inner_block)}
     </p>
     """
@@ -396,7 +404,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
 
   ## Examples
 
-      <.icon name="hero-x-mark-solid" />
+      <.icon name="hero-x-mark" />
       <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
   """
   attr :name, :string, required: true
diff --git a/installer/templates/phx_web/components/layouts.ex b/installer/templates/phx_web/components/layouts.ex
index 3981b63b6..9f284b715 100644
--- a/installer/templates/phx_web/components/layouts.ex
+++ b/installer/templates/phx_web/components/layouts.ex
@@ -11,11 +11,29 @@ defmodule <%= @web_namespace %>.Layouts do
 
   embed_templates "layouts/*"
 
+  @doc """
+  Renders the app layout
+
+  ## Examples
+
+      <Layouts.app flash={@flash}>
+        <h1>Content</h1>
+      </Layout.app>
+      
+  """
+  attr :flash, :map, required: true, doc: "the map of flash messages"
+
+  attr :current_scope, :map,
+    default: nil,
+    doc: "the current [scope](https://hexdocs.pm/phoenix/scopes.html)"
+
+  slot :inner_block, required: true
+
   def app(assigns) do
     ~H"""
     <header class="navbar px-4 sm:px-6 lg:px-8">
       <div class="flex-1">
-        <a href="/" class="flex-1 flex items-center gap-2">
+        <a href="/" class="flex-1 flex w-fit items-center gap-2">
           <img src={~p"/images/logo.svg"} width="36" />
           <span class="text-sm font-semibold">v{Application.spec(:phoenix, :vsn)}</span>
         </a>
@@ -75,19 +93,19 @@ defmodule <%= @web_namespace %>.Layouts do
         hidden
       >
         <%= maybe_eex_gettext.("Attempting to reconnect", @gettext) %>
-        <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 motion-safe:animate-spin" />
+        <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
       </.flash>
 
       <.flash
         id="server-error"
         kind={:error}
         title=<%= maybe_heex_attr_gettext.("Something went wrong!", @gettext) %>
-        phx-disconnected={show(".phx-client-error #client-error") |> JS.remove_attribute("hidden")}
-        phx-connected={hide("#client-error") |> JS.set_attribute({"hidden", ""})}
+        phx-disconnected={show(".phx-server-error #server-error") |> JS.remove_attribute("hidden")}
+        phx-connected={hide("#server-error") |> JS.set_attribute({"hidden", ""})}
         hidden
       >
-        <%= maybe_eex_gettext.("Hang in there while we get back on track", @gettext) %>
-        <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 motion-safe:animate-spin" />
+        <%= maybe_eex_gettext.("Attempting to reconnect", @gettext) %>
+        <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
       </.flash>
     </div>
     """
diff --git a/installer/templates/phx_web/components/layouts/root.html.heex b/installer/templates/phx_web/components/layouts/root.html.heex
index cb10aaa05..ef859d6ae 100644
--- a/installer/templates/phx_web/components/layouts/root.html.heex
+++ b/installer/templates/phx_web/components/layouts/root.html.heex
@@ -22,7 +22,9 @@
             document.documentElement.setAttribute("data-theme", theme);
           }
         };
-        setTheme(localStorage.getItem("phx:theme") || "system");
+        if (!document.documentElement.hasAttribute("data-theme")) {
+          setTheme(localStorage.getItem("phx:theme") || "system");
+        }
         window.addEventListener("storage", (e) => e.key === "phx:theme" && setTheme(e.newValue || "system"));
         window.addEventListener("phx:set-theme", ({ detail: { theme } }) => setTheme(theme));
       })();
diff --git a/lib/mix/phoenix/schema.ex b/lib/mix/phoenix/schema.ex
index cd4692c86..25aaf45b9 100644
--- a/lib/mix/phoenix/schema.ex
+++ b/lib/mix/phoenix/schema.ex
@@ -83,7 +83,7 @@ defmodule Mix.Phoenix.Schema do
     repo_alias = if String.ends_with?(Atom.to_string(repo), ".Repo"), do: "", else: ", as: Repo"
     file = Mix.Phoenix.context_lib_path(ctx_app, basename <> ".ex")
     table = opts[:table] || schema_plural
-    scope = Mix.Phoenix.Scope.scope_from_opts(otp_app, opts[:scope], opts[:no_scope])
+    scope = Mix.Phoenix.Scope.scope_from_opts(ctx_app, opts[:scope], opts[:no_scope])
     {cli_attrs, uniques, redacts} = extract_attr_flags(cli_attrs)
     {assocs, attrs} = partition_attrs_and_assocs(module, attrs(cli_attrs))
     types = types(attrs)
diff --git a/lib/mix/tasks/phx.gen.auth.ex b/lib/mix/tasks/phx.gen.auth.ex
index c06de7c05..22bc0c394 100644
--- a/lib/mix/tasks/phx.gen.auth.ex
+++ b/lib/mix/tasks/phx.gen.auth.ex
@@ -103,6 +103,13 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
       $ mix phx.gen.auth Store User users
       $ mix phx.gen.auth Backoffice Admin admins
 
+  Note that when invoking `phx.gen.auth` multiple times, it will also generate
+  multiple [scopes](guides/authn_authz/scopes.md). Typically, only one scope is needed,
+  thus you will probably want to customize the generated code afterwards. Also, it
+  is expected that the generated code is not fully free of conflicts. One example is the
+  browser pipeline, which will try to assign both scopes as `:current_scope` by default.
+  You can customize the generated assign key with the `--assign-key` option.
+
   ## Binary ids
 
   The `--binary-id` option causes the generated migration to use
@@ -140,6 +147,15 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
   ```
 
   This will generate a scope named `app_user` instead of `user`. You can read more about scopes in the [Scopes guide](scopes.html).
+
+  Additionally, the scope's assign key can be customized by passing the `--assign-key` option. For example:
+
+  ```console
+  $ mix phx.gen.auth Accounts User users --assign-key current_user_scope
+  ```
+
+  This is useful when you want to run `mix phx.gen.auth` multiple times in the same project, but note that
+  often it might make more sense to reuse the same scope with additional fields instead of separate scopes.
   """
 
   use Mix.Task
@@ -157,7 +173,8 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
     prefix: :string,
     live: :boolean,
     compile: :boolean,
-    scope: :string
+    scope: :string,
+    assign_key: :string
   ]
 
   @doc false
@@ -173,7 +190,8 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
     hashing_library = build_hashing_library!(opts)
 
     context_args =
-      OptionParser.to_argv(Keyword.drop(opts, [:scope]), switches: @switches) ++ parsed
+      OptionParser.to_argv(Keyword.drop(opts, [:scope, :assign_key]), switches: @switches) ++
+        parsed
 
     {context, schema} = Gen.Context.build(context_args ++ ["--no-scope"], __MODULE__)
 
@@ -212,7 +230,8 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
       live?: Keyword.fetch!(context.opts, :live),
       datetime_module: datetime_module(schema),
       datetime_now: datetime_now(schema),
-      scope_config: scope_config(context, opts[:scope])
+      scope_config:
+        scope_config(context, opts[:scope], Keyword.get(opts, :assign_key, "current_scope"))
     ]
 
     paths = Mix.Phoenix.generator_paths()
@@ -227,8 +246,8 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
     |> maybe_inject_mix_dependency(hashing_library)
     |> inject_routes(paths, binding)
     |> maybe_inject_router_import(binding)
-    |> maybe_inject_router_plug()
-    |> maybe_inject_app_layout_menu()
+    |> maybe_inject_router_plug(binding)
+    |> maybe_inject_app_layout_menu(binding)
     |> Gen.Notifier.maybe_print_mailer_installation_instructions()
     |> print_shell_instructions()
   end
@@ -289,7 +308,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
     end
   end
 
-  defp scope_config(context, requested_scope) do
+  defp scope_config(context, requested_scope, assign_key) do
     existing_scopes = Mix.Phoenix.Scope.scopes_from_config(context.context_app)
 
     {_, default_scope} =
@@ -301,8 +320,8 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
       if Map.has_key?(existing_scopes, key) do
         {false, existing_scopes[key], nil}
       else
-        {true, new_scope(context, key, default_scope),
-         scope_config_string(context, key, default_scope)}
+        {true, new_scope(context, key, default_scope, assign_key),
+         scope_config_string(context, key, default_scope, assign_key)}
       end
 
     %{
@@ -325,17 +344,20 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
         "#{context.basename}_#{context.schema.singular}"
 
       # my_app_accounts_user
-      is_new_scope?(existing_scopes, "#{context.context_app}_#{context.basename}_#{context.schema.singular}") ->
+      is_new_scope?(
+        existing_scopes,
+        "#{context.context_app}_#{context.basename}_#{context.schema.singular}"
+      ) ->
         "#{context.context_app}_#{context.basename}_#{context.schema.singular}"
 
       true ->
-        Mix.raise """
+        Mix.raise("""
         Could not generate a scope name for #{context.schema.singular}! These scopes already exist:
 
             * #{Enum.map(existing_scopes, fn {name, _scope} -> name end) |> Enum.join("\n    * ")}
 
         You can customize the scope name by passing the --scope option.
-        """
+        """)
     end
   end
 
@@ -344,11 +366,11 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
     not Map.has_key?(existing_scopes, key)
   end
 
-  defp new_scope(context, key, default_scope) do
+  defp new_scope(context, key, default_scope, assign_key) do
     Mix.Phoenix.Scope.new!(key, %{
       default: !default_scope,
       module: Module.concat([context.module, "Scope"]),
-      assign_key: :current_scope,
+      assign_key: String.to_atom(assign_key),
       access_path: [
         String.to_atom(context.schema.singular),
         context.schema.opts[:primary_key] || :id
@@ -362,13 +384,13 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
     })
   end
 
-  defp scope_config_string(context, key, default_scope) do
+  defp scope_config_string(context, key, default_scope, assign_key) do
     """
     config :#{context.context_app}, :scopes,
       #{key}: [
         default: #{if default_scope, do: false, else: true},
         module: #{inspect(context.module)}.Scope,
-        assign_key: :current_scope,
+        assign_key: :#{assign_key},
         access_path: [:#{context.schema.singular}, :#{context.schema.opts[:primary_key] || :id}],
         schema_key: :#{context.schema.singular}_#{context.schema.opts[:primary_key] || :id},
         schema_type: :#{if(context.schema.binary_id, do: :binary_id, else: :id)},
@@ -417,6 +439,10 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
 
         phx.gen.auth will create a new #{scope.name} scope.
 
+        Note that if you run `phx.gen.live` multiple times, the generated assign key for
+        the generated scopes can conflict with each other. You can pass `--assign-key` to customize
+        the assign key for the generated scope.
+
         Do you want to proceed with the generation?\
         """) || System.halt()
 
@@ -697,13 +723,13 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
     context
   end
 
-  defp maybe_inject_router_plug(%Context{context_app: ctx_app} = context) do
+  defp maybe_inject_router_plug(%Context{context_app: ctx_app} = context, binding) do
     web_prefix = Mix.Phoenix.web_path(ctx_app)
     file_path = Path.join(web_prefix, "router.ex")
-    help_text = Injector.router_plug_help_text(file_path, context)
+    help_text = Injector.router_plug_help_text(file_path, binding)
 
     with {:ok, file} <- read_file(file_path),
-         {:ok, new_file} <- Injector.router_plug_inject(file, context) do
+         {:ok, new_file} <- Injector.router_plug_inject(file, binding) do
       print_injecting(file_path, " - plug")
       File.write!(file_path, new_file)
     else
@@ -724,11 +750,9 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
     context
   end
 
-  defp maybe_inject_app_layout_menu(%Context{} = context) do
-    schema = context.schema
-
+  defp maybe_inject_app_layout_menu(%Context{} = context, binding) do
     if file_path = get_layout_html_path(context) do
-      case Injector.app_layout_menu_inject(schema, File.read!(file_path)) do
+      case Injector.app_layout_menu_inject(binding, File.read!(file_path)) do
         {:ok, new_content} ->
           print_injecting(file_path)
           File.write!(file_path, new_content)
@@ -739,11 +763,11 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
         {:error, :unable_to_inject} ->
           Mix.shell().info("""
 
-          #{Injector.app_layout_menu_help_text(file_path, schema)}
+          #{Injector.app_layout_menu_help_text(file_path, binding)}
           """)
       end
     else
-      {_dup, inject} = Injector.app_layout_menu_code_to_inject(schema)
+      {_dup, inject} = Injector.app_layout_menu_code_to_inject(binding)
 
       missing =
         context
@@ -761,7 +785,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
       Please ensure this phoenix app was not generated with
       --no-html. If you have changed the name of your root
       layout file, please add the following code to it where you'd
-      like the #{schema.singular} menu items to be rendered.
+      like the #{binding[:schema].singular} menu items to be rendered.
 
       #{inject}
       """)
diff --git a/lib/mix/tasks/phx.gen.auth/injector.ex b/lib/mix/tasks/phx.gen.auth/injector.ex
index c058d7bbc..ae6ab097c 100644
--- a/lib/mix/tasks/phx.gen.auth/injector.ex
+++ b/lib/mix/tasks/phx.gen.auth/injector.ex
@@ -98,10 +98,10 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do
   """
   @spec router_plug_inject(String.t(), context) ::
           {:ok, String.t()} | :already_injected | {:error, :unable_to_inject}
-  def router_plug_inject(file, %Context{schema: schema}) when is_binary(file) do
+  def router_plug_inject(file, binding) when is_binary(file) do
     inject_unless_contains(
       file,
-      router_plug_code(schema),
+      router_plug_code(binding),
       # Matches the entire line containing `anchor_line` and captures
       # the whitespace before the anchor. In the replace string
       #
@@ -119,34 +119,34 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do
   Instructions to provide the user when `inject_router_plug/2` fails.
   """
   @spec router_plug_help_text(String.t(), context) :: String.t()
-  def router_plug_help_text(file_path, %Context{schema: schema}) do
+  def router_plug_help_text(file_path, binding) do
     """
-    Add the #{router_plug_name(schema)} plug to the :browser pipeline in #{Path.relative_to_cwd(file_path)}:
+    Add the #{router_plug_name(binding)} plug to the :browser pipeline in #{Path.relative_to_cwd(file_path)}:
 
         pipeline :browser do
           ...
           #{@router_plug_anchor_line}
-          #{router_plug_code(schema)}
+          #{router_plug_code(binding)}
         end
     """
   end
 
-  defp router_plug_code(%Schema{} = schema) do
-    "plug " <> router_plug_name(schema)
+  defp router_plug_code(binding) do
+    "plug " <> router_plug_name(binding)
   end
 
-  defp router_plug_name(schema) do
-    ":fetch_current_scope_for_#{schema.singular}"
+  defp router_plug_name(binding) do
+    ":fetch_#{binding[:scope_config].scope.assign_key}_for_#{binding[:schema].singular}"
   end
 
   @doc """
   Injects a menu in the application layout
   """
-  def app_layout_menu_inject(%Schema{} = schema, template_str) do
+  def app_layout_menu_inject(binding, template_str) do
     with {:error, :unable_to_inject} <-
-           app_layout_menu_inject_at_end_of_nav_tag(template_str, schema),
+           app_layout_menu_inject_at_end_of_nav_tag(binding, template_str),
          {:error, :unable_to_inject} <-
-           app_layout_menu_inject_after_opening_body_tag(template_str, schema) do
+           app_layout_menu_inject_after_opening_body_tag(binding, template_str) do
       {:error, :unable_to_inject}
     end
   end
@@ -154,11 +154,11 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do
   @doc """
   Instructions to provide the user when `app_layout_menu_inject/2` fails.
   """
-  def app_layout_menu_help_text(file_path, %Schema{} = schema) do
-    {_dup_check, code} = app_layout_menu_code_to_inject(schema)
+  def app_layout_menu_help_text(file_path, binding) do
+    {_dup_check, code} = app_layout_menu_code_to_inject(binding)
 
     """
-    Add the following #{schema.singular} menu items to your #{Path.relative_to_cwd(file_path)} layout file:
+    Add the following #{binding[:schema].singular} menu items to your #{Path.relative_to_cwd(file_path)} layout file:
 
     #{code}
     """
@@ -167,14 +167,16 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do
   @doc """
   Menu code to inject into the application layout template.
   """
-  def app_layout_menu_code_to_inject(%Schema{} = schema, padding \\ 4, newline \\ "\n") do
+  def app_layout_menu_code_to_inject(binding, padding \\ 4, newline \\ "\n") do
+    schema = binding[:schema]
+    scope_config = binding[:scope_config]
     already_injected_str = "#{schema.route_prefix}/log-in"
 
     template = """
     <ul class="menu menu-horizontal w-full relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
-      <%= if @current_scope do %>
+      <%= if @#{scope_config.scope.assign_key} do %>
         <li>
-          {@current_scope.#{schema.singular}.email}
+          {@#{scope_config.scope.assign_key}.#{schema.singular}.email}
         </li>
         <li>
           <.link href={~p"#{schema.route_prefix}/settings"}>Settings</.link>
@@ -207,9 +209,9 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do
     {String.length(padding), newline}
   end
 
-  defp app_layout_menu_inject_at_end_of_nav_tag(file, schema) do
+  defp app_layout_menu_inject_at_end_of_nav_tag(binding, file) do
     {padding, newline} = formatting_info(file, "<\/nav>")
-    {dup_check, code} = app_layout_menu_code_to_inject(schema, padding, newline)
+    {dup_check, code} = app_layout_menu_code_to_inject(binding, padding, newline)
 
     inject_unless_contains(
       file,
@@ -219,10 +221,10 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do
     )
   end
 
-  defp app_layout_menu_inject_after_opening_body_tag(file, schema) do
+  defp app_layout_menu_inject_after_opening_body_tag(binding, file) do
     anchor_line = "<body"
     {padding, newline} = formatting_info(file, anchor_line)
-    {dup_check, code} = app_layout_menu_code_to_inject(schema, padding, newline)
+    {dup_check, code} = app_layout_menu_code_to_inject(binding, padding, newline)
 
     inject_unless_contains(
       file,
diff --git a/lib/mix/tasks/phx.gen.cert.ex b/lib/mix/tasks/phx.gen.cert.ex
index 363b6d151..0d558b15c 100644
--- a/lib/mix/tasks/phx.gen.cert.ex
+++ b/lib/mix/tasks/phx.gen.cert.ex
@@ -125,7 +125,7 @@ defmodule Mix.Tasks.Phx.Gen.Cert do
     configuration in config/dev.exs:
 
       config #{inspect(app)}, #{inspect(Mix.Phoenix.web_module(base))}.Endpoint,
-        http: [port: 4000],
+        http: [port: String.to_integer(System.get_env("PORT") || "4000")],
         https: [
           port: 4001,
           cipher_suite: :strong,
diff --git a/lib/mix/tasks/phx.gen.release.ex b/lib/mix/tasks/phx.gen.release.ex
old mode 100644
new mode 100755
index 3f3bf1e29..c6c38a5ed
--- a/lib/mix/tasks/phx.gen.release.ex
+++ b/lib/mix/tasks/phx.gen.release.ex
@@ -29,6 +29,13 @@ defmodule Mix.Tasks.Phx.Gen.Release do
 
     * `.dockerignore` - A docker ignore file with standard elixir defaults
 
+  By default, the build uses whatever base image matches your development system’s
+  active versions at generation time. To override those defaults, specify:
+
+  * `otp` — the OTP version to use
+
+  * `elixir` — the Elixir version to use
+
   For extended release configuration, the `mix release.init` task can be used
   in addition to this task. See the `Mix.Release` docs for more details.
 
@@ -105,7 +112,7 @@ defmodule Mix.Tasks.Phx.Gen.Release do
     end
 
     if opts.docker do
-      gen_docker(binding)
+      gen_docker(binding, opts)
     end
 
     File.chmod!("rel/overlays/bin/server", 0o755)
@@ -178,11 +185,13 @@ defmodule Mix.Tasks.Phx.Gen.Release do
 
   defp parse_args(args) do
     args
-    |> OptionParser.parse!(strict: [ecto: :boolean, docker: :boolean])
+    |> OptionParser.parse(strict: [ecto: :boolean, docker: :boolean, elixir: :string, otp: :string])
     |> elem(0)
     |> Keyword.put_new_lazy(:ecto, &ecto_sql_installed?/0)
     |> Keyword.put_new_lazy(:socket_db_adaptor_installed, &socket_db_adaptor_installed?/0)
     |> Keyword.put_new(:docker, false)
+    |> Keyword.put_new(:elixir, false)
+    |> Keyword.put_new(:otp, false)
     |> Map.new()
   end
 
@@ -244,14 +253,14 @@ defmodule Mix.Tasks.Phx.Gen.Release do
     end)
   end
 
-  defp gen_docker(binding) do
-    wanted_elixir_vsn =
+  defp gen_docker(binding, opts) do
+    wanted_elixir_vsn = opts[:elixir] ||
       case Version.parse!(System.version()) do
         %{major: major, minor: minor, pre: ["dev"]} -> "#{major}.#{minor - 1}.0"
         _ -> System.version()
       end
 
-    otp_vsn = otp_vsn()
+    otp_vsn =  opts[:otp] || otp_vsn()
 
     vsns =
       case elixir_and_debian_vsn(wanted_elixir_vsn, otp_vsn) do
@@ -290,7 +299,7 @@ defmodule Mix.Tasks.Phx.Gen.Release do
       :error ->
         raise """
         unable to fetch supported Docker image for Elixir #{wanted_elixir_vsn} and Erlang #{otp_vsn}.
-        Please check https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=#{otp_vsn}\
+        Please check https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=#{otp_vsn} \
         for a suitable Elixir version
         """
     end
diff --git a/lib/phoenix/channel.ex b/lib/phoenix/channel.ex
index 0f902dc6b..d1b83bdd9 100644
--- a/lib/phoenix/channel.ex
+++ b/lib/phoenix/channel.ex
@@ -34,8 +34,8 @@ defmodule Phoenix.Channel do
 
   The first argument is the topic, the second argument is a map payload given by
   the client, and the third argument is an instance of `Phoenix.Socket`. The
-  `socket` to all channel callbacks, so check its module and documentation to
-  learn its fields and the different ways to interact with it.
+  `socket` is provided to all channel callbacks, so check its module and
+  documentation to learn its fields and the different ways to interact with it.
 
   ## Authorization
 
diff --git a/lib/phoenix/code_reloader.ex b/lib/phoenix/code_reloader.ex
index 0910f784c..69a335869 100644
--- a/lib/phoenix/code_reloader.ex
+++ b/lib/phoenix/code_reloader.ex
@@ -330,6 +330,11 @@ defmodule Phoenix.CodeReloader do
   defp format_output(output) do
     output
     |> String.trim()
+    |> remove_ansi_escapes()
     |> Plug.HTML.html_escape()
   end
+
+  defp remove_ansi_escapes(text) do
+    Regex.replace(~r/\e\[[0-9;]*[a-zA-Z]/, text, "")
+  end
 end
diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex
index 8f8a52bcf..75027af9d 100644
--- a/lib/phoenix/endpoint/supervisor.ex
+++ b/lib/phoenix/endpoint/supervisor.ex
@@ -33,8 +33,26 @@ defmodule Phoenix.Endpoint.Supervisor do
       cond do
         Code.ensure_loaded?(mod) and function_exported?(mod, :init, 2) ->
           IO.warn(
-            "#{inspect(mod)}.init/2 is deprecated, use config/runtime.exs instead " <>
-              "or pass additional options when starting the endpoint in your supervision tree"
+            """
+            your #{inspect(mod)} defines a init/2 callback, which is now deprecated. \
+            This callback is invoked when your endpoint is initialized as part of your supervision tree. \
+            Instead, you should either:
+
+            1. Move all dynamic configuration to config/runtime.exs (preferred). For example:
+
+                # config/runtime.exs
+                import Config
+
+                if config_env() == :prod do
+                  config #{inspect(otp_app)}, #{inspect(mod)},
+                    http: [:inet6, port: System.fetch_env!("PORT")]
+                end
+
+            2. Pass the configuration you returned from the `init/2` callback \
+            as additional options when starting the endpoint in your supervision tree. \
+            For example: {#{inspect(mod)}, some_extra_options: true}
+            """,
+            []
           )
 
           {:ok, init_conf} = mod.init(:supervisor, env_conf)
@@ -88,6 +106,7 @@ defmodule Phoenix.Endpoint.Supervisor do
         server_children(mod, conf, server?) ++
         socket_children(mod, conf, :drainer_spec) ++
         watcher_children(mod, conf, server?)
+
     Supervisor.init(children, strategy: :one_for_one)
   end
 
diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex
index 9f9b6f167..e6b48e092 100644
--- a/lib/phoenix/router.ex
+++ b/lib/phoenix/router.ex
@@ -854,12 +854,40 @@ defmodule Phoenix.Router do
   Defines a list of plugs (and pipelines) to send the connection through.
 
   Plugs are specified using the atom name of any imported 2-arity function
-  which takes a `%Plug.Conn{}` and options and returns a `%Plug.Conn{}`; for
+  which takes a `Plug.Conn` and options and returns a `Plug.Conn`. For
   example, `:require_authenticated_user`.
 
-  Pipelines are defined in the router; see `pipeline/2` for more information.
+  Pipelines are defined in the router, see `pipeline/2` for more information.
 
-      pipe_through [:my_imported_function, :my_pipeline]
+      pipe_through [:require_authenticated_user, :my_browser_pipeline]
+
+  ## Multiple invocations
+
+  `pipe_through/1` can be invoked multiple times within the same scope. Each
+  invocation appends new plugs and pipelines to run, which are applied to all
+  routes **after** the `pipe_through/1` invocation. For example:
+
+      scope "/" do
+        pipe_through [:browser]
+        get "/", HomeController, :index
+
+        pipe_through [:require_authenticated_user]
+        get "/settings", UserController, :edit
+      end
+
+  In the example above, `/` pipes through `browser` only, while `/settings` pipes
+  through both `browser` and `require_authenticated_user`. Therefore, to avoid
+  confusion, we recommend a single `pipe_through` at the top of each scope:
+
+      scope "/" do
+        pipe_through [:browser]
+        get "/", HomeController, :index
+      end
+
+      scope "/" do
+        pipe_through [:browser, :require_authenticated_user]
+        get "/settings", UserController, :edit
+      end
   """
   defmacro pipe_through(pipes) do
     pipes =
diff --git a/mix.exs b/mix.exs
index a3a4b13ee..5a4468802 100644
--- a/mix.exs
+++ b/mix.exs
@@ -8,7 +8,7 @@ defmodule Phoenix.MixProject do
     end
   end
 
-  @version "1.8.0-rc.0"
+  @version "1.8.0-rc.3"
   @scm_url "https://github.com/phoenixframework/phoenix"
 
   # If the elixir requirement is updated, we need to make the installer
diff --git a/package.json b/package.json
index 0b9e5d355..d766a5185 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "phoenix",
-  "version": "1.8.0-rc.0",
+  "version": "1.8.0-rc.3",
   "description": "The official JavaScript client for the Phoenix web framework.",
   "license": "MIT",
   "module": "./priv/static/phoenix.mjs",
diff --git a/priv/templates/phx.gen.auth/auth.ex b/priv/templates/phx.gen.auth/auth.ex
index e0e742042..2cae2be61 100644
--- a/priv/templates/phx.gen.auth/auth.ex
+++ b/priv/templates/phx.gen.auth/auth.ex
@@ -64,14 +64,14 @@ defmodule <%= inspect auth_module %> do
 
   Will reissue the session token if it is older than the configured age.
   """
-  def fetch_current_scope_for_<%= schema.singular %>(conn, _opts) do
+  def fetch_<%= scope_config.scope.assign_key %>_for_<%= schema.singular %>(conn, _opts) do
     with {token, conn} <- ensure_<%= schema.singular %>_token(conn),
          {<%= schema.singular %>, token_inserted_at} <- <%= inspect context.alias %>.get_<%= schema.singular %>_by_session_token(token) do
       conn
-      |> assign(:current_scope, <%= inspect scope_config.scope.alias %>.for_<%= schema.singular %>(<%= schema.singular %>))
+      |> assign(:<%= scope_config.scope.assign_key %>, <%= inspect scope_config.scope.alias %>.for_<%= schema.singular %>(<%= schema.singular %>))
       |> maybe_reissue_<%= schema.singular %>_session_token(<%= schema.singular %>, token_inserted_at)
     else
-      nil -> assign(conn, :current_scope, <%= inspect scope_config.scope.alias %>.for_<%= schema.singular %>(nil))
+      nil -> assign(conn, :<%= scope_config.scope.assign_key %>, <%= inspect scope_config.scope.alias %>.for_<%= schema.singular %>(nil))
     end
   end
 
@@ -120,7 +120,7 @@ defmodule <%= inspect auth_module %> do
 
   # Do not renew session if the <%= schema.singular %> is already logged in
   # to prevent CSRF errors or data being last in tabs that are still open
-  defp renew_session(conn, <%= schema.singular %>) when conn.assigns.current_scope.<%= schema.singular %>.id == <%= schema.singular %>.id do
+  defp renew_session(conn, <%= schema.singular %>) when conn.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>.id == <%= schema.singular %>.id do
     conn
   end
 
@@ -180,28 +180,28 @@ defmodule <%= inspect auth_module %> do
   defp <%= schema.singular %>_session_topic(token), do: "<%= schema.plural %>_sessions:#{Base.url_encode64(token)}"
 
   @doc """
-  Handles mounting and authenticating the current_scope in LiveViews.
+  Handles mounting and authenticating the <%= scope_config.scope.assign_key %> in LiveViews.
 
   ## `on_mount` arguments
 
-    * `:mount_current_scope` - Assigns current_scope
+    * `:mount_<%= scope_config.scope.assign_key %>` - Assigns <%= scope_config.scope.assign_key %>
       to socket assigns based on <%= schema.singular %>_token, or nil if
       there's no <%= schema.singular %>_token or no matching <%= schema.singular %>.
 
     * `:require_authenticated` - Authenticates the <%= schema.singular %> from the session,
-      and assigns the current_scope to socket assigns based
+      and assigns the <%= scope_config.scope.assign_key %> to socket assigns based
       on <%= schema.singular %>_token.
       Redirects to login page if there's no logged <%= schema.singular %>.
 
   ## Examples
 
   Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
-  the `current_scope`:
+  the `<%= scope_config.scope.assign_key %>`:
 
       defmodule <%= inspect context.web_module %>.PageLive do
         use <%= inspect context.web_module %>, :live_view
 
-        on_mount {<%= inspect auth_module %>, :mount_current_scope}
+        on_mount {<%= inspect auth_module %>, :mount_<%= scope_config.scope.assign_key %>}
         ...
       end
 
@@ -211,14 +211,14 @@ defmodule <%= inspect auth_module %> do
         live "/profile", ProfileLive, :index
       end
   """
-  def on_mount(:mount_current_scope, _params, session, socket) do
-    {:cont, mount_current_scope(socket, session)}
+  def on_mount(:mount_<%= scope_config.scope.assign_key %>, _params, session, socket) do
+    {:cont, mount_<%= scope_config.scope.assign_key %>(socket, session)}
   end
 
   def on_mount(:require_authenticated, _params, session, socket) do
-    socket = mount_current_scope(socket, session)
+    socket = mount_<%= scope_config.scope.assign_key %>(socket, session)
 
-    if socket.assigns.current_scope && socket.assigns.current_scope.<%= schema.singular %> do
+    if socket.assigns.<%= scope_config.scope.assign_key %> && socket.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %> do
       {:cont, socket}
     else
       socket =
@@ -231,9 +231,9 @@ defmodule <%= inspect auth_module %> do
   end
 
   def on_mount(:require_sudo_mode, _params, session, socket) do
-    socket = mount_current_scope(socket, session)
+    socket = mount_<%= scope_config.scope.assign_key %>(socket, session)
 
-    if <%= inspect context.alias %>.sudo_mode?(socket.assigns.current_scope.<%= schema.singular %>, -10) do
+    if <%= inspect context.alias %>.sudo_mode?(socket.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>, -10) do
       {:cont, socket}
     else
       socket =
@@ -245,8 +245,8 @@ defmodule <%= inspect auth_module %> do
     end
   end
 
-  defp mount_current_scope(socket, session) do
-    Phoenix.Component.assign_new(socket, :current_scope, fn ->
+  defp mount_<%= scope_config.scope.assign_key %>(socket, session) do
+    Phoenix.Component.assign_new(socket, :<%= scope_config.scope.assign_key %>, fn ->
       {<%= schema.singular %>, _} =
         if <%= schema.singular %>_token = session["<%= schema.singular %>_token"] do
           <%= inspect context.alias %>.get_<%= schema.singular %>_by_session_token(<%= schema.singular %>_token)
@@ -258,7 +258,7 @@ defmodule <%= inspect auth_module %> do
 
   @doc "Returns the path to redirect to after log in."
   # the <%= schema.singular %> was already logged in, redirect to settings
-  def signed_in_path(%Plug.Conn{assigns: %{current_scope: %<%= inspect scope_config.scope.alias %>{<%= schema.singular %>: %<%= inspect context.alias %>.<%= inspect schema.alias %>{}}}}) do
+  def signed_in_path(%Plug.Conn{assigns: %{<%= scope_config.scope.assign_key %>: %<%= inspect scope_config.scope.alias %>{<%= schema.singular %>: %<%= inspect context.alias %>.<%= inspect schema.alias %>{}}}}) do
     ~p"<%= schema.route_prefix %>/settings"
   end
 
@@ -272,7 +272,7 @@ defmodule <%= inspect auth_module %> do
   Plug for routes that require sudo mode.
   """
   def require_sudo_mode(conn, _opts) do
-    if <%= inspect context.alias %>.sudo_mode?(conn.assigns.current_scope.<%= schema.singular %>, -10) do
+    if <%= inspect context.alias %>.sudo_mode?(conn.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>, -10) do
       conn
     else
       conn
@@ -287,7 +287,7 @@ defmodule <%= inspect auth_module %> do
   Plug for routes that require the <%= schema.singular %> to not be authenticated.
   """
   def redirect_if_<%= schema.singular %>_is_authenticated(conn, _opts) do
-    if conn.assigns.current_scope do
+    if conn.assigns.<%= scope_config.scope.assign_key %> do
       conn
       |> redirect(to: signed_in_path(conn))
       |> halt()
@@ -302,7 +302,7 @@ defmodule <%= inspect auth_module %> do
   Plug for routes that require the <%= schema.singular %> to be authenticated.
   """
   def require_authenticated_<%= schema.singular %>(conn, _opts) do
-    if conn.assigns.current_scope && conn.assigns.current_scope.<%= schema.singular %> do
+    if conn.assigns.<%= scope_config.scope.assign_key %> && conn.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %> do
       conn
     else
       conn
diff --git a/priv/templates/phx.gen.auth/confirmation_live.ex b/priv/templates/phx.gen.auth/confirmation_live.ex
index d95fcff04..e42c53abf 100644
--- a/priv/templates/phx.gen.auth/confirmation_live.ex
+++ b/priv/templates/phx.gen.auth/confirmation_live.ex
@@ -19,7 +19,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         >
           <input type="hidden" name={@form[:token].name} value={@form[:token].value} />
           <.input
-            :if={!@current_scope}
+            :if={!@<%= scope_config.scope.assign_key %>}
             field={@form[:remember_me]}
             type="checkbox"
             label="Keep me logged in"
@@ -39,7 +39,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         >
           <input type="hidden" name={@form[:token].name} value={@form[:token].value} />
           <.input
-            :if={!@current_scope}
+            :if={!@<%= scope_config.scope.assign_key %>}
             field={@form[:remember_me]}
             type="checkbox"
             label="Keep me logged in"
diff --git a/priv/templates/phx.gen.auth/login_live.ex b/priv/templates/phx.gen.auth/login_live.ex
index 691ab2308..e40284326 100644
--- a/priv/templates/phx.gen.auth/login_live.ex
+++ b/priv/templates/phx.gen.auth/login_live.ex
@@ -6,11 +6,11 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   def render(assigns) do
     ~H"""
     <Layouts.app flash={@flash} <%= scope_config.scope.assign_key %>={@<%= scope_config.scope.assign_key %>}>
-      <div class="mx-auto max-w-sm">
+      <div class="mx-auto max-w-sm space-y-4">
         <.header class="text-center">
           <p>Log in</p>
           <:subtitle>
-            <%%= if @current_scope do %>
+            <%%= if @<%= scope_config.scope.assign_key %> do %>
               You need to reauthenticate to perform sensitive actions on your account.
             <%% else %>
               Don't have an account? <.link
@@ -22,6 +22,16 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
           </:subtitle>
         </.header>
 
+        <div :if={local_mail_adapter?()} class="alert alert-info">
+          <.icon name="hero-information-circle" class="size-6 shrink-0" />
+          <div>
+            <p>You are running the local mail adapter.</p>
+            <p>
+              To see sent emails, visit <.link href="/dev/mailbox" class="underline">the mailbox page</.link>.
+            </p>
+          </div>
+        </div>
+
         <.form
           :let={f}
           for={@form}
@@ -30,7 +40,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
           phx-submit="submit_magic"
         >
           <.input
-            readonly={!!@current_scope}
+            readonly={!!@<%= scope_config.scope.assign_key %>}
             field={f[:email]}
             type="email"
             label="Email"
@@ -54,7 +64,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
           phx-trigger-action={@trigger_submit}
         >
           <.input
-            readonly={!!@current_scope}
+            readonly={!!@<%= scope_config.scope.assign_key %>}
             field={f[:email]}
             type="email"
             label="Email"
@@ -68,7 +78,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
             autocomplete="current-password"
           />
           <.input
-            :if={!@current_scope}
+            :if={!@<%= scope_config.scope.assign_key %>}
             field={f[:remember_me]}
             type="checkbox"
             label="Keep me logged in"
@@ -77,15 +87,6 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
             Log in <span aria-hidden="true">→</span>
           </.button>
         </.form>
-
-        <div :if={local_mail_adapter?()} class="alert alert-outline mt-8">
-          <div>
-            <p>You are running the local mail adapter.</p>
-            <p>
-              To see sent emails, visit <.link href="/dev/mailbox" class="underline">the mailbox page</.link>.
-            </p>
-          </div>
-        </div>
       </div>
     </Layouts.app>
     """
@@ -94,7 +95,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   def mount(_params, _session, socket) do
     email =
       Phoenix.Flash.get(socket.assigns.flash, :email) ||
-        get_in(socket.assigns, [:current_scope, Access.key(:<%= schema.singular %>), Access.key(:email)])
+        get_in(socket.assigns, [:<%= scope_config.scope.assign_key %>, Access.key(:<%= schema.singular %>), Access.key(:email)])
 
     form = to_form(%{"email" => email}, as: "<%= schema.singular %>")
 
diff --git a/priv/templates/phx.gen.auth/registration_live.ex b/priv/templates/phx.gen.auth/registration_live.ex
index d063aa75a..20b64c73f 100644
--- a/priv/templates/phx.gen.auth/registration_live.ex
+++ b/priv/templates/phx.gen.auth/registration_live.ex
@@ -38,7 +38,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     """
   end
 
-  def mount(_params, _session, %{assigns: %{current_scope: %{<%= schema.singular %>: <%= schema.singular %>}}} = socket)
+  def mount(_params, _session, %{assigns: %{<%= scope_config.scope.assign_key %>: %{<%= schema.singular %>: <%= schema.singular %>}}} = socket)
       when not is_nil(<%= schema.singular %>) do
     {:ok, redirect(socket, to: <%= inspect auth_module %>.signed_in_path(socket))}
   end
diff --git a/priv/templates/phx.gen.auth/routes.ex b/priv/templates/phx.gen.auth/routes.ex
index f40cc6ae8..9f780d968 100644
--- a/priv/templates/phx.gen.auth/routes.ex
+++ b/priv/templates/phx.gen.auth/routes.ex
@@ -28,7 +28,7 @@
     pipe_through [:browser]
 
     <%= if live? do %>live_session :current_<%= schema.singular %>,
-      on_mount: [{<%= inspect auth_module %>, :mount_current_scope}] do
+      on_mount: [{<%= inspect auth_module %>, :mount_<%= scope_config.scope.assign_key %>}] do
       live "/<%= schema.plural %>/register", <%= inspect schema.alias %>Live.Registration, :new
       live "/<%= schema.plural %>/log-in", <%= inspect schema.alias %>Live.Login, :new
       live "/<%= schema.plural %>/log-in/:token", <%= inspect schema.alias %>Live.Confirmation, :new
diff --git a/priv/templates/phx.gen.auth/schema.ex b/priv/templates/phx.gen.auth/schema.ex
index dae8f1cbe..28d55502f 100644
--- a/priv/templates/phx.gen.auth/schema.ex
+++ b/priv/templates/phx.gen.auth/schema.ex
@@ -7,7 +7,6 @@ defmodule <%= inspect schema.module %> do
     field :email, :string
     field :password, :string, virtual: true, redact: true
     field :hashed_password, :string, redact: true
-    field :current_password, :string, virtual: true, redact: true
     field :confirmed_at, <%= inspect schema.timestamp_type %>
     field :authenticated_at, <%= inspect schema.timestamp_type %>, virtual: true
 
@@ -113,9 +112,9 @@ defmodule <%= inspect schema.module %> do
   """
   def confirm_changeset(<%= schema.singular %>) do
     <%= case schema.timestamp_type do %>
-    <% :naive_datetime -> %>now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
-    <% :utc_datetime -> %>now = DateTime.utc_now() |> DateTime.truncate(:second)
-    <% :utc_datetime_usec -> %>now = DateTime.utc_now() |> DateTime.truncate(:microsecond)
+    <% :naive_datetime -> %>now = NaiveDateTime.utc_now(:second)
+    <% :utc_datetime -> %>now = DateTime.utc_now(:second)
+    <% :utc_datetime_usec -> %>now = DateTime.utc_now(:microsecond)
     <% end %>change(<%= schema.singular %>, confirmed_at: now)
   end
 
diff --git a/priv/templates/phx.gen.auth/session_confirm.html.heex b/priv/templates/phx.gen.auth/session_confirm.html.heex
index 0e6ea6422..d9ee7391a 100644
--- a/priv/templates/phx.gen.auth/session_confirm.html.heex
+++ b/priv/templates/phx.gen.auth/session_confirm.html.heex
@@ -10,7 +10,7 @@
     >
       <input type="hidden" name={@form[:token].name} value={@form[:token].value} />
       <.input
-        :if={!@current_scope}
+        :if={!@<%= scope_config.scope.assign_key %>}
         field={@form[:remember_me]}
         type="checkbox"
         label="Keep me logged in"
@@ -24,7 +24,7 @@
     <.form :if={@<%= schema.singular %>.confirmed_at} for={@form} id="login_form" action={~p"<%= schema.route_prefix %>/log-in"}>
       <input type="hidden" name={@form[:token].name} value={@form[:token].value} />
       <.input
-        :if={!@current_scope}
+        :if={!@<%= scope_config.scope.assign_key %>}
         field={@form[:remember_me]}
         type="checkbox"
         label="Keep me logged in"
diff --git a/priv/templates/phx.gen.auth/session_controller.ex b/priv/templates/phx.gen.auth/session_controller.ex
index c6146695d..8cd12782c 100644
--- a/priv/templates/phx.gen.auth/session_controller.ex
+++ b/priv/templates/phx.gen.auth/session_controller.ex
@@ -47,7 +47,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   end
 
   def update_password(conn, %{"<%= schema.singular %>" => <%= schema.singular %>_params} = params) do
-    <%= schema.singular %> = conn.assigns.current_scope.<%= schema.singular %>
+    <%= schema.singular %> = conn.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>
     true = <%= inspect context.alias %>.sudo_mode?(<%= schema.singular %>)
     {:ok, _<%= schema.singular %>, expired_tokens} = <%= inspect context.alias %>.update_<%= schema.singular %>_password(<%= schema.singular %>, <%= schema.singular %>_params)
 
@@ -60,7 +60,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   end<% else %>
 
   def new(conn, _params) do
-    email = get_in(conn.assigns, [:current_scope, Access.key(:<%= schema.singular %>), Access.key(:email)])
+    email = get_in(conn.assigns, [:<%= scope_config.scope.assign_key %>, Access.key(:<%= schema.singular %>), Access.key(:email)])
     form = Phoenix.Component.to_form(%{"email" => email}, as: "<%= schema.singular %>")
 
     render(conn, :new, form: form)
diff --git a/priv/templates/phx.gen.auth/session_new.html.heex b/priv/templates/phx.gen.auth/session_new.html.heex
index 2a51a5dec..45fd31d7b 100644
--- a/priv/templates/phx.gen.auth/session_new.html.heex
+++ b/priv/templates/phx.gen.auth/session_new.html.heex
@@ -1,9 +1,9 @@
 <Layouts.app flash={@flash} <%= scope_config.scope.assign_key %>={@<%= scope_config.scope.assign_key %>}>
-  <div class="mx-auto max-w-sm">
+  <div class="mx-auto max-w-sm space-y-4">
     <.header class="text-center">
       <p>Log in</p>
       <:subtitle>
-        <%%= if @current_scope do %>
+        <%%= if @<%= scope_config.scope.assign_key %> do %>
           You need to reauthenticate to perform sensitive actions on your account.
         <%% else %>
           Don't have an account? <.link
@@ -15,9 +15,19 @@
       </:subtitle>
     </.header>
 
+    <div :if={local_mail_adapter?()} class="alert alert-info">
+      <.icon name="hero-information-circle" class="size-6 shrink-0" />
+      <div>
+        <p>You are running the local mail adapter.</p>
+        <p>
+          To see sent emails, visit <.link href="/dev/mailbox" class="underline">the mailbox page</.link>.
+        </p>
+      </div>
+    </div>
+
     <.form :let={f} for={@form} as={:<%= schema.singular %>} id="login_form_magic" action={~p"<%= schema.route_prefix %>/log-in"}>
       <.input
-        readonly={!!@current_scope}
+        readonly={!!@<%= scope_config.scope.assign_key %>}
         field={f[:email]}
         type="email"
         label="Email"
@@ -34,7 +44,7 @@
 
     <.form :let={f} for={@form} as={:<%= schema.singular %>} id="login_form_password" action={~p"<%= schema.route_prefix %>/log-in"}>
       <.input
-        readonly={!!@current_scope}
+        readonly={!!@<%= scope_config.scope.assign_key %>}
         field={f[:email]}
         type="email"
         label="Email"
@@ -48,7 +58,7 @@
         autocomplete="current-password"
       />
       <.input
-        :if={!@current_scope}
+        :if={!@<%= scope_config.scope.assign_key %>}
         field={f[:remember_me]}
         type="checkbox"
         label="Keep me logged in"
@@ -57,14 +67,5 @@
         Log in <span aria-hidden="true">→</span>
       </.button>
     </.form>
-
-    <div :if={local_mail_adapter?()} class="alert alert-outline mt-8">
-      <div>
-        <p>You are running the local mail adapter.</p>
-        <p>
-          To see sent emails, visit <.link href="/dev/mailbox" class="underline">the mailbox page</.link>.
-        </p>
-      </div>
-    </div>
   </div>
 </Layouts.app>
diff --git a/priv/templates/phx.gen.auth/settings_controller.ex b/priv/templates/phx.gen.auth/settings_controller.ex
index b43ed81f5..409f9d3b9 100644
--- a/priv/templates/phx.gen.auth/settings_controller.ex
+++ b/priv/templates/phx.gen.auth/settings_controller.ex
@@ -15,7 +15,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   def update(conn, %{"action" => "update_email"} = params) do
     %{"<%= schema.singular %>" => <%= schema.singular %>_params} = params
-    <%= schema.singular %> = conn.assigns.current_scope.<%= schema.singular %>
+    <%= schema.singular %> = conn.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>
 
     case <%= inspect context.alias %>.change_<%= schema.singular %>_email(<%= schema.singular %>, <%= schema.singular %>_params) do
       %{valid?: true} = changeset ->
@@ -39,7 +39,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   def update(conn, %{"action" => "update_password"} = params) do
     %{"<%= schema.singular %>" => <%= schema.singular %>_params} = params
-    <%= schema.singular %> = conn.assigns.current_scope.<%= schema.singular %>
+    <%= schema.singular %> = conn.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>
 
     case <%= inspect context.alias %>.update_<%= schema.singular %>_password(<%= schema.singular %>, <%= schema.singular %>_params) do
       {:ok, <%= schema.singular %>, _} ->
@@ -54,7 +54,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   end
 
   def confirm_email(conn, %{"token" => token}) do
-    case <%= inspect context.alias %>.update_<%= schema.singular %>_email(conn.assigns.current_scope.<%= schema.singular %>, token) do
+    case <%= inspect context.alias %>.update_<%= schema.singular %>_email(conn.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>, token) do
       :ok ->
         conn
         |> put_flash(:info, "Email changed successfully.")
@@ -68,7 +68,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   end
 
   defp assign_email_and_password_changesets(conn, _opts) do
-    <%= schema.singular %> = conn.assigns.current_scope.<%= schema.singular %>
+    <%= schema.singular %> = conn.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>
 
     conn
     |> assign(:email_changeset, <%= inspect context.alias %>.change_<%= schema.singular %>_email(<%= schema.singular %>))
diff --git a/priv/templates/phx.gen.auth/settings_live.ex b/priv/templates/phx.gen.auth/settings_live.ex
index 02627a65c..0d6dd971e 100644
--- a/priv/templates/phx.gen.auth/settings_live.ex
+++ b/priv/templates/phx.gen.auth/settings_live.ex
@@ -65,7 +65,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   def mount(%{"token" => token}, _session, socket) do
     socket =
-      case <%= inspect context.alias %>.update_<%= schema.singular %>_email(socket.assigns.current_scope.<%= schema.singular %>, token) do
+      case <%= inspect context.alias %>.update_<%= schema.singular %>_email(socket.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>, token) do
         :ok ->
           put_flash(socket, :info, "Email changed successfully.")
 
@@ -77,7 +77,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   end
 
   def mount(_params, _session, socket) do
-    <%= schema.singular %> = socket.assigns.current_scope.<%= schema.singular %>
+    <%= schema.singular %> = socket.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>
     email_changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_email(<%= schema.singular %>, %{}, validate_email: false)
     password_changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_password(<%= schema.singular %>, %{}, hash_password: false)
 
@@ -95,7 +95,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     %{"<%= schema.singular %>" => <%= schema.singular %>_params} = params
 
     email_form =
-      socket.assigns.current_scope.<%= schema.singular %>
+      socket.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>
       |> <%= inspect context.alias %>.change_<%= schema.singular %>_email(<%= schema.singular %>_params, validate_email: false)
       |> Map.put(:action, :validate)
       |> to_form()
@@ -105,7 +105,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   def handle_event("update_email", params, socket) do
     %{"<%= schema.singular %>" => <%= schema.singular %>_params} = params
-    <%= schema.singular %> = socket.assigns.current_scope.<%= schema.singular %>
+    <%= schema.singular %> = socket.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>
     true = <%= inspect context.alias %>.sudo_mode?(<%= schema.singular %>)
 
     case <%= inspect context.alias %>.change_<%= schema.singular %>_email(<%= schema.singular %>, <%= schema.singular %>_params) do
@@ -128,7 +128,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     %{"<%= schema.singular %>" => <%= schema.singular %>_params} = params
 
     password_form =
-      socket.assigns.current_scope.<%= schema.singular %>
+      socket.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>
       |> <%= inspect context.alias %>.change_<%= schema.singular %>_password(<%= schema.singular %>_params, hash_password: false)
       |> Map.put(:action, :validate)
       |> to_form()
@@ -138,7 +138,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   def handle_event("update_password", params, socket) do
     %{"<%= schema.singular %>" => <%= schema.singular %>_params} = params
-    <%= schema.singular %> = socket.assigns.current_scope.<%= schema.singular %>
+    <%= schema.singular %> = socket.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>
     true = <%= inspect context.alias %>.sudo_mode?(<%= schema.singular %>)
 
     case <%= inspect context.alias %>.change_<%= schema.singular %>_password(<%= schema.singular %>, <%= schema.singular %>_params) do
diff --git a/priv/templates/phx.gen.context/access_no_schema.ex b/priv/templates/phx.gen.context/access_no_schema.ex
index 66b765e91..e24173435 100644
--- a/priv/templates/phx.gen.context/access_no_schema.ex
+++ b/priv/templates/phx.gen.context/access_no_schema.ex
@@ -39,7 +39,7 @@
       {:error, ...}
 
   """
-  def create_<%= schema.singular %>(attrs \\ %{}) do
+  def create_<%= schema.singular %>(attrs) do
     raise "TODO"
   end
 
diff --git a/priv/templates/phx.gen.context/access_no_schema_scope.ex b/priv/templates/phx.gen.context/access_no_schema_scope.ex
index 2ad0eaabf..eef5d57b3 100644
--- a/priv/templates/phx.gen.context/access_no_schema_scope.ex
+++ b/priv/templates/phx.gen.context/access_no_schema_scope.ex
@@ -47,7 +47,7 @@
       {:error, ...}
 
   """
-  def create_<%= schema.singular %>(%<%= inspect scope.alias %>{} = _scope, attrs \\ %{}) do
+  def create_<%= schema.singular %>(%<%= inspect scope.alias %>{} = _scope, attrs) do
     raise "TODO"
   end
 
diff --git a/priv/templates/phx.gen.context/schema_access.ex b/priv/templates/phx.gen.context/schema_access.ex
index 9bedfd035..b55b1d9bc 100644
--- a/priv/templates/phx.gen.context/schema_access.ex
+++ b/priv/templates/phx.gen.context/schema_access.ex
@@ -42,7 +42,7 @@
       {:error, %Ecto.Changeset{}}
 
   """
-  def create_<%= schema.singular %>(attrs \\ %{}) do
+  def create_<%= schema.singular %>(attrs) do
     %<%= inspect schema.alias %>{}
     |> <%= inspect schema.alias %>.changeset(attrs)
     |> Repo.insert()
diff --git a/priv/templates/phx.gen.context/schema_access_scope.ex b/priv/templates/phx.gen.context/schema_access_scope.ex
index 5e4a71fc4..1f415317e 100644
--- a/priv/templates/phx.gen.context/schema_access_scope.ex
+++ b/priv/templates/phx.gen.context/schema_access_scope.ex
@@ -67,7 +67,7 @@
       {:error, %Ecto.Changeset{}}
 
   """
-  def create_<%= schema.singular %>(%<%= inspect scope.alias %>{} = scope, attrs \\ %{}) do
+  def create_<%= schema.singular %>(%<%= inspect scope.alias %>{} = scope, attrs) do
     with {:ok, <%= schema.singular %> = %<%= inspect schema.alias %>{}} <-
            %<%= inspect schema.alias %>{}
            |> <%= inspect schema.alias %>.changeset(attrs, scope)
diff --git a/priv/templates/phx.gen.live/index.ex b/priv/templates/phx.gen.live/index.ex
index 922872436..18ddcb1da 100644
--- a/priv/templates/phx.gen.live/index.ex
+++ b/priv/templates/phx.gen.live/index.ex
@@ -43,7 +43,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   @impl true
   def mount(_params, _session, socket) do<%= if scope do %>
-    <%= inspect context.alias %>.subscribe_<%= schema.plural %>(<%= socket_scope %>)
+    if connected?(socket) do
+      <%= inspect context.alias %>.subscribe_<%= schema.plural %>(<%= socket_scope %>)
+    end
 <% end %>
     {:ok,
      socket
diff --git a/priv/templates/phx.gen.live/show.ex b/priv/templates/phx.gen.live/show.ex
index 81d29749c..bf9e9e80f 100644
--- a/priv/templates/phx.gen.live/show.ex
+++ b/priv/templates/phx.gen.live/show.ex
@@ -29,7 +29,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   @impl true
   def mount(%{"<%= primary_key %>" => <%= primary_key %>}, _session, socket) do<%= if scope do %>
-    <%= inspect context.alias %>.subscribe_<%= schema.plural %>(<%= socket_scope %>)
+    if connected?(socket) do
+      <%= inspect context.alias %>.subscribe_<%= schema.plural %>(<%= socket_scope %>)
+    end
 <% end %>
     {:ok,
      socket
@@ -53,5 +55,10 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
      socket
      |> put_flash(:error, "The current <%= schema.singular %> was deleted.")
      |> push_navigate(to: ~p"<%= scope_socket_route_prefix %><%= schema.route_prefix %>")}
+  end
+
+  def handle_info({type, %<%= inspect schema.module %>{}}, socket)
+      when type in [:created, :updated, :deleted] do
+    {:noreply, socket}
   end<% end %>
 end
diff --git a/priv/templates/phx.gen.release/release.ex b/priv/templates/phx.gen.release/release.ex
index 521d17768..86d00a153 100644
--- a/priv/templates/phx.gen.release/release.ex
+++ b/priv/templates/phx.gen.release/release.ex
@@ -23,6 +23,8 @@ defmodule <%= app_namespace %>.Release do
   end
 
   defp load_app do
+    # Many platforms require SSL when connecting to the database
+    Application.ensure_all_started(:ssl)
     Application.ensure_loaded(@app)
   end
 end
diff --git a/priv/templates/phx.gen.schema/migration.exs b/priv/templates/phx.gen.schema/migration.exs
index fc21e1dee..53de8d7bb 100644
--- a/priv/templates/phx.gen.schema/migration.exs
+++ b/priv/templates/phx.gen.schema/migration.exs
@@ -10,7 +10,9 @@ defmodule <%= inspect schema.repo %>.Migrations.Create<%= Macro.camelize(schema.
 <% end %><%= if scope do %>      add :<%= scope.schema_key %>, <%= if scope.schema_table do %>references(:<%= scope.schema_table %>, type: <%= inspect scope.schema_migration_type %>, on_delete: :delete_all)<% else %><%= inspect scope.schema_migration_type %><% end %>
 <% end %>
       timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}" %>)
-    end
+    end<%= if scope do %>
+
+    create index(:<%= schema.table %>, [:<%= scope.schema_key %>])<% end %>
 <%= if Enum.any?(schema.indexes) do %><%= for index <- schema.indexes do %>
     <%= index %><% end %>
 <% end %>  end


Test patch:
diff --git a/assets/test/longpoll_test.js b/assets/test/longpoll_test.js
index 6d51ff73b..f02871a39 100644
--- a/assets/test/longpoll_test.js
+++ b/assets/test/longpoll_test.js
@@ -190,3 +190,67 @@ describe("Socket with LongPoll", () => {
     })
   })
 })
+
+describe("Ajax.request", () => {
+  let originalXMLHttpRequest, originalFetch, originalAbortController
+
+  beforeEach(() => {
+    originalXMLHttpRequest = global.XMLHttpRequest
+    originalFetch = global.fetch
+    originalAbortController = global.AbortController
+
+    // Mock AbortController
+    global.AbortController = jest.fn(() => ({
+      abort: jest.fn(),
+      signal: {}
+    }))
+
+    // Mock XMLHttpRequest
+    global.XMLHttpRequest = jest.fn(() => ({
+      open: jest.fn(),
+      send: jest.fn(),
+      setRequestHeader: jest.fn(),
+      onreadystatechange: null,
+      readyState: 4,
+      status: 200,
+      responseText: JSON.stringify({success: true})
+    }))
+
+    // Mock fetch
+    global.fetch = jest.fn(() =>
+      Promise.resolve({
+        text: () => Promise.resolve(JSON.stringify({success: true}))
+      })
+    )
+  })
+
+  afterEach(() => {
+    global.XMLHttpRequest = originalXMLHttpRequest
+    global.fetch = originalFetch
+    global.AbortController = originalAbortController
+    jest.restoreAllMocks()
+  })
+
+  it("should use XMLHttpRequest by default", () => {
+    Ajax.request("GET", "/test-endpoint", {}, null, 0, null, (response) => {
+      expect(response).toEqual({success: true})
+    })
+
+    expect(global.XMLHttpRequest).toHaveBeenCalled()
+  })
+
+  it("should use fetch when XMLHttpRequest is not availble", () => {
+    global.XMLHttpRequest = undefined // Simulate it being unavailable
+    Ajax.request("GET", "/test-endpoint", {}, null, 0, null, (response) => {
+      expect(response).toEqual({success: true})
+    })
+
+    expect(global.fetch).toHaveBeenCalledWith(
+      "/test-endpoint",
+      expect.objectContaining({
+        method: "GET",
+      })
+    )
+  })
+})
+
diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs
index ef2b37ac2..6766a85ad 100644
--- a/installer/test/phx_new_test.exs
+++ b/installer/test/phx_new_test.exs
@@ -141,8 +141,8 @@ defmodule Mix.Tasks.Phx.NewTest do
 
       assert_file("phx_blog/config/dev.exs", fn file ->
         assert file =~ "esbuild: {Esbuild,"
-        assert file =~ "lib/phx_blog_web/(controllers|live|components)/.*(ex|heex)"
-        assert file =~ "http: [ip: {127, 0, 0, 1}, port: 4000]"
+        assert file =~ "lib/phx_blog_web/(?:controllers|live|components|router)/?.*\\.(ex|heex)$"
+        assert file =~ "http: [ip: {127, 0, 0, 1}"
       end)
 
       # tailwind
@@ -815,7 +815,7 @@ defmodule Mix.Tasks.Phx.NewTest do
     in_tmp("new without defaults", fn ->
       Mix.Tasks.Phx.New.run([@app_name, "--inside-docker-env"])
       assert_file("phx_blog/config/dev.exs", fn file ->
-        assert file =~ "http: [ip: {0, 0, 0, 0}, port: 4000]"
+        assert file =~ "http: [ip: {0, 0, 0, 0}"
       end)
     end)
   end
diff --git a/installer/test/phx_new_umbrella_test.exs b/installer/test/phx_new_umbrella_test.exs
index 7d652c660..fd298a47c 100644
--- a/installer/test/phx_new_umbrella_test.exs
+++ b/installer/test/phx_new_umbrella_test.exs
@@ -71,7 +71,7 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
 
       assert_file(root_path(@app, "config/dev.exs"), fn file ->
         assert file =~ ~r[esbuild: {Esbuild]
-        assert file =~ "lib/#{@app}_web/(controllers|live|components)/.*(ex|heex)"
+        assert file =~ "lib/#{@app}_web/(?:controllers|live|components|router)/?.*\\.(ex|heex)$"
         assert file =~ "config :#{@app}_web, dev_routes: true"
       end)
 
diff --git a/priv/templates/phx.gen.auth/auth_test.exs b/priv/templates/phx.gen.auth/auth_test.exs
index f895f93f3..99e6d55ee 100644
--- a/priv/templates/phx.gen.auth/auth_test.exs
+++ b/priv/templates/phx.gen.auth/auth_test.exs
@@ -37,7 +37,7 @@ defmodule <%= inspect auth_module %>Test do
     test "keeps session when re-authenticating", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       conn =
         conn
-        |> assign(:current_scope, <%= inspect scope_config.scope.alias %>.for_<%= schema.singular %>(<%= schema.singular %>))
+        |> assign(:<%= scope_config.scope.assign_key %>, <%= inspect scope_config.scope.alias %>.for_<%= schema.singular %>(<%= schema.singular %>))
         |> put_session(:to_be_removed, "value")
         |> <%= inspect schema.alias %>Auth.log_in_<%= schema.singular %>(<%= schema.singular %>)
 
@@ -52,7 +52,7 @@ defmodule <%= inspect auth_module %>Test do
 
       conn =
         conn
-        |> assign(:current_scope, <%= inspect scope_config.scope.alias %>.for_<%= schema.singular %>(other_<%= schema.singular %>))
+        |> assign(:<%= scope_config.scope.assign_key %>, <%= inspect scope_config.scope.alias %>.for_<%= schema.singular %>(other_<%= schema.singular %>))
         |> put_session(:to_be_removed, "value")
         |> <%= inspect schema.alias %>Auth.log_in_<%= schema.singular %>(<%= schema.singular %>)
 
@@ -77,7 +77,7 @@ defmodule <%= inspect auth_module %>Test do
     test "redirects to settings when <%= schema.singular %> is already logged in", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       conn =
         conn
-        |> assign(:current_scope, <%= inspect scope_config.scope.alias %>.for_<%= schema.singular %>(<%= schema.singular %>))
+        |> assign(:<%= scope_config.scope.assign_key %>, <%= inspect scope_config.scope.alias %>.for_<%= schema.singular %>(<%= schema.singular %>))
         |> <%= inspect schema.alias %>Auth.log_in_<%= schema.singular %>(<%= schema.singular %>)
 
       assert redirected_to(conn) == "<%= schema.route_prefix %>/settings"
@@ -143,15 +143,15 @@ defmodule <%= inspect auth_module %>Test do
     end
   end
 
-  describe "fetch_current_scope_for_<%= schema.singular %>/2" do
+  describe "fetch_<%= scope_config.scope.assign_key %>_for_<%= schema.singular %>/2" do
     test "authenticates <%= schema.singular %> from session", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       <%= schema.singular %>_token = <%= inspect context.alias %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
 
       conn =
-        conn |> put_session(:<%= schema.singular %>_token, <%= schema.singular %>_token) |> <%= inspect schema.alias %>Auth.fetch_current_scope_for_<%= schema.singular %>([])
+        conn |> put_session(:<%= schema.singular %>_token, <%= schema.singular %>_token) |> <%= inspect schema.alias %>Auth.fetch_<%= scope_config.scope.assign_key %>_for_<%= schema.singular %>([])
 
-      assert conn.assigns.current_scope.<%= schema.singular %>.id == <%= schema.singular %>.id
-      assert conn.assigns.current_scope.<%= schema.singular %>.authenticated_at == <%= schema.singular %>.authenticated_at
+      assert conn.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>.id == <%= schema.singular %>.id
+      assert conn.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>.authenticated_at == <%= schema.singular %>.authenticated_at
       assert get_session(conn, :<%= schema.singular %>_token) == <%= schema.singular %>_token
     end
 
@@ -165,10 +165,10 @@ defmodule <%= inspect auth_module %>Test do
       conn =
         conn
         |> put_req_cookie(@remember_me_cookie, signed_token)
-        |> <%= inspect schema.alias %>Auth.fetch_current_scope_for_<%= schema.singular %>([])
+        |> <%= inspect schema.alias %>Auth.fetch_<%= scope_config.scope.assign_key %>_for_<%= schema.singular %>([])
 
-      assert conn.assigns.current_scope.<%= schema.singular %>.id == <%= schema.singular %>.id
-      assert conn.assigns.current_scope.<%= schema.singular %>.authenticated_at == <%= schema.singular %>.authenticated_at
+      assert conn.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>.id == <%= schema.singular %>.id
+      assert conn.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>.authenticated_at == <%= schema.singular %>.authenticated_at
       assert get_session(conn, :<%= schema.singular %>_token) == <%= schema.singular %>_token
       assert get_session(conn, :<%= schema.singular %>_remember_me)<%= if live? do %>
 
@@ -178,9 +178,9 @@ defmodule <%= inspect auth_module %>Test do
 
     test "does not authenticate if data is missing", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       _ = <%= inspect context.alias %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
-      conn = <%= inspect schema.alias %>Auth.fetch_current_scope_for_<%= schema.singular %>(conn, [])
+      conn = <%= inspect schema.alias %>Auth.fetch_<%= scope_config.scope.assign_key %>_for_<%= schema.singular %>(conn, [])
       refute get_session(conn, :<%= schema.singular %>_token)
-      refute conn.assigns.current_scope
+      refute conn.assigns.<%= scope_config.scope.assign_key %>
     end
 
     test "reissues a new token after a few days and refreshes cookie", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
@@ -198,10 +198,10 @@ defmodule <%= inspect auth_module %>Test do
         |> put_session(:<%= schema.singular %>_token, token)
         |> put_session(:<%= schema.singular %>_remember_me, true)
         |> put_req_cookie(@remember_me_cookie, signed_token)
-        |> <%= inspect schema.alias %>Auth.fetch_current_scope_for_<%= schema.singular %>([])
+        |> <%= inspect schema.alias %>Auth.fetch_<%= scope_config.scope.assign_key %>_for_<%= schema.singular %>([])
 
-      assert conn.assigns.current_scope.<%= schema.singular %>.id == <%= schema.singular %>.id
-      assert conn.assigns.current_scope.<%= schema.singular %>.authenticated_at == <%= schema.singular %>.authenticated_at
+      assert conn.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>.id == <%= schema.singular %>.id
+      assert conn.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>.authenticated_at == <%= schema.singular %>.authenticated_at
       assert new_token = get_session(conn, :<%= schema.singular %>_token)
       assert new_token != token
       assert %{value: new_signed_token, max_age: max_age} = conn.resp_cookies[@remember_me_cookie]
@@ -210,50 +210,50 @@ defmodule <%= inspect auth_module %>Test do
     end
   end
 
-  <%= if live? do %>describe "on_mount :mount_current_scope" do
+  <%= if live? do %>describe "on_mount :mount_<%= scope_config.scope.assign_key %>" do
     setup %{conn: conn} do
-      %{conn: <%= inspect schema.alias %>Auth.fetch_current_scope_for_<%= schema.singular %>(conn, [])}
+      %{conn: <%= inspect schema.alias %>Auth.fetch_<%= scope_config.scope.assign_key %>_for_<%= schema.singular %>(conn, [])}
     end
 
-    test "assigns current_scope based on a valid <%= schema.singular %>_token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+    test "assigns <%= scope_config.scope.assign_key %> based on a valid <%= schema.singular %>_token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       <%= schema.singular %>_token = <%= inspect context.alias %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
       session = conn |> put_session(:<%= schema.singular %>_token, <%= schema.singular %>_token) |> get_session()
 
       {:cont, updated_socket} =
-        <%= inspect schema.alias %>Auth.on_mount(:mount_current_scope, %{}, session, %LiveView.Socket{})
+        <%= inspect schema.alias %>Auth.on_mount(:mount_<%= scope_config.scope.assign_key %>, %{}, session, %LiveView.Socket{})
 
-      assert updated_socket.assigns.current_scope.<%= schema.singular %>.id == <%= schema.singular %>.id
+      assert updated_socket.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>.id == <%= schema.singular %>.id
     end
 
-    test "assigns nil to current_scope assign if there isn't a valid <%= schema.singular %>_token", %{conn: conn} do
+    test "assigns nil to <%= scope_config.scope.assign_key %> assign if there isn't a valid <%= schema.singular %>_token", %{conn: conn} do
       <%= schema.singular %>_token = "invalid_token"
       session = conn |> put_session(:<%= schema.singular %>_token, <%= schema.singular %>_token) |> get_session()
 
       {:cont, updated_socket} =
-        <%= inspect schema.alias %>Auth.on_mount(:mount_current_scope, %{}, session, %LiveView.Socket{})
+        <%= inspect schema.alias %>Auth.on_mount(:mount_<%= scope_config.scope.assign_key %>, %{}, session, %LiveView.Socket{})
 
-      assert updated_socket.assigns.current_scope == nil
+      assert updated_socket.assigns.<%= scope_config.scope.assign_key %> == nil
     end
 
-    test "assigns nil to current_scope assign if there isn't a <%= schema.singular %>_token", %{conn: conn} do
+    test "assigns nil to <%= scope_config.scope.assign_key %> assign if there isn't a <%= schema.singular %>_token", %{conn: conn} do
       session = conn |> get_session()
 
       {:cont, updated_socket} =
-        <%= inspect schema.alias %>Auth.on_mount(:mount_current_scope, %{}, session, %LiveView.Socket{})
+        <%= inspect schema.alias %>Auth.on_mount(:mount_<%= scope_config.scope.assign_key %>, %{}, session, %LiveView.Socket{})
 
-      assert updated_socket.assigns.current_scope == nil
+      assert updated_socket.assigns.<%= scope_config.scope.assign_key %> == nil
     end
   end
 
   describe "on_mount :require_authenticated" do
-    test "authenticates current_scope based on a valid <%= schema.singular %>_token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+    test "authenticates <%= scope_config.scope.assign_key %> based on a valid <%= schema.singular %>_token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       <%= schema.singular %>_token = <%= inspect context.alias %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
       session = conn |> put_session(:<%= schema.singular %>_token, <%= schema.singular %>_token) |> get_session()
 
       {:cont, updated_socket} =
         <%= inspect schema.alias %>Auth.on_mount(:require_authenticated, %{}, session, %LiveView.Socket{})
 
-      assert updated_socket.assigns.current_scope.<%= schema.singular %>.id == <%= schema.singular %>.id
+      assert updated_socket.assigns.<%= scope_config.scope.assign_key %>.<%= schema.singular %>.id == <%= schema.singular %>.id
     end
 
     test "redirects to login page if there isn't a valid <%= schema.singular %>_token", %{conn: conn} do
@@ -266,7 +266,7 @@ defmodule <%= inspect auth_module %>Test do
       }
 
       {:halt, updated_socket} = <%= inspect schema.alias %>Auth.on_mount(:require_authenticated, %{}, session, socket)
-      assert updated_socket.assigns.current_scope == nil
+      assert updated_socket.assigns.<%= scope_config.scope.assign_key %> == nil
     end
 
     test "redirects to login page if there isn't a <%= schema.singular %>_token", %{conn: conn} do
@@ -278,7 +278,7 @@ defmodule <%= inspect auth_module %>Test do
       }
 
       {:halt, updated_socket} = <%= inspect schema.alias %>Auth.on_mount(:require_authenticated, %{}, session, socket)
-      assert updated_socket.assigns.current_scope == nil
+      assert updated_socket.assigns.<%= scope_config.scope.assign_key %> == nil
     end
   end
 
@@ -317,7 +317,7 @@ defmodule <%= inspect auth_module %>Test do
       conn =
         conn
         |> fetch_flash()
-        |> assign(:current_scope, <%= inspect scope_config.scope.alias %>.for_<%= schema.singular %>(<%= schema.singular %>))
+        |> assign(:<%= scope_config.scope.assign_key %>, <%= inspect scope_config.scope.alias %>.for_<%= schema.singular %>(<%= schema.singular %>))
         |> <%= inspect schema.alias %>Auth.require_sudo_mode([])
 
       refute conn.halted
@@ -334,7 +334,7 @@ defmodule <%= inspect auth_module %>Test do
       conn =
         conn
         |> fetch_flash()
-        |> assign(:current_scope, Scope.for_<%= schema.singular %>(<%= schema.singular %>))
+        |> assign(:<%= scope_config.scope.assign_key %>, Scope.for_<%= schema.singular %>(<%= schema.singular %>))
         |> <%= inspect schema.alias %>Auth.require_sudo_mode([])
 
       assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
@@ -346,13 +346,13 @@ defmodule <%= inspect auth_module %>Test do
 
   describe "redirect_if_<%= schema.singular %>_is_authenticated/2" do
     setup %{conn: conn} do
-      %{conn: <%= inspect schema.alias %>Auth.fetch_current_scope_for_<%= schema.singular %>(conn, [])}
+      %{conn: <%= inspect schema.alias %>Auth.fetch_<%= scope_config.scope.assign_key %>_for_<%= schema.singular %>(conn, [])}
     end
 
     test "redirects if <%= schema.singular %> is authenticated", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       conn =
         conn
-        |> assign(:current_scope, <%= inspect scope_config.scope.alias %>.for_<%= schema.singular %>(<%= schema.singular %>))
+        |> assign(:<%= scope_config.scope.assign_key %>, <%= inspect scope_config.scope.alias %>.for_<%= schema.singular %>(<%= schema.singular %>))
         |> <%= inspect schema.alias %>Auth.redirect_if_<%= schema.singular %>_is_authenticated([])
 
       assert conn.halted
@@ -368,7 +368,7 @@ defmodule <%= inspect auth_module %>Test do
 
   describe "require_authenticated_<%= schema.singular %>/2" do
     setup %{conn: conn} do
-      %{conn: <%= inspect schema.alias %>Auth.fetch_current_scope_for_<%= schema.singular %>(conn, [])}
+      %{conn: <%= inspect schema.alias %>Auth.fetch_<%= scope_config.scope.assign_key %>_for_<%= schema.singular %>(conn, [])}
     end
 
     test "redirects if <%= schema.singular %> is not authenticated", %{conn: conn} do
@@ -410,7 +410,7 @@ defmodule <%= inspect auth_module %>Test do
     test "does not redirect if <%= schema.singular %> is authenticated", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       conn =
         conn
-        |> assign(:current_scope, <%= inspect scope_config.scope.alias %>.for_<%= schema.singular %>(<%= schema.singular %>))
+        |> assign(:<%= scope_config.scope.assign_key %>, <%= inspect scope_config.scope.alias %>.for_<%= schema.singular %>(<%= schema.singular %>))
         |> <%= inspect schema.alias %>Auth.require_authenticated_<%= schema.singular %>([])
 
       refute conn.halted
diff --git a/priv/templates/phx.gen.auth/login_live_test.exs b/priv/templates/phx.gen.auth/login_live_test.exs
index 5b6d90e69..2e64849e6 100644
--- a/priv/templates/phx.gen.auth/login_live_test.exs
+++ b/priv/templates/phx.gen.auth/login_live_test.exs
@@ -83,7 +83,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
       {:ok, _login_live, login_html} =
         lv
-        |> element(~s|main a:fl-contains("Sign up")|)
+        |> element("main a", "Sign up")
         |> render_click()
         |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/register")
 
diff --git a/priv/templates/phx.gen.auth/registration_live_test.exs b/priv/templates/phx.gen.auth/registration_live_test.exs
index 0c98f2033..104d82bd9 100644
--- a/priv/templates/phx.gen.auth/registration_live_test.exs
+++ b/priv/templates/phx.gen.auth/registration_live_test.exs
@@ -72,7 +72,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
       {:ok, _login_live, login_html} =
         lv
-        |> element(~s|main a:fl-contains("Log in")|)
+        |> element("main a", "Log in")
         |> render_click()
         |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log-in")
 
diff --git a/test/mix/tasks/phx.gen.auth/injector_test.exs b/test/mix/tasks/phx.gen.auth/injector_test.exs
index 122f11353..a5818cdd8 100644
--- a/test/mix/tasks/phx.gen.auth/injector_test.exs
+++ b/test/mix/tasks/phx.gen.auth/injector_test.exs
@@ -470,6 +470,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
     test "injects after :put_secure_browser_headers" do
       schema = Schema.new("Accounts.User", "users", [], [no_scope: true])
       context = Context.new("Accounts", schema, [])
+      binding = [schema: schema, context: context, scope_config: %{scope: %{assign_key: :current_scope}}]
 
       input = """
       defmodule DemoWeb.Router do
@@ -485,7 +486,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
       end
       """
 
-      {:ok, injected} = Injector.router_plug_inject(input, context)
+      {:ok, injected} = Injector.router_plug_inject(input, binding)
 
       assert injected ==
                """
@@ -507,6 +508,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
     test "injects after :put_secure_browser_headers even when it has additional options" do
       schema = Schema.new("Accounts.User", "users", [], [no_scope: true])
       context = Context.new("Accounts", schema, [])
+      binding = [schema: schema, context: context, scope_config: %{scope: %{assign_key: :current_scope}}]
 
       input = """
       defmodule DemoWeb.Router do
@@ -522,7 +524,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
       end
       """
 
-      {:ok, injected} = Injector.router_plug_inject(input, context)
+      {:ok, injected} = Injector.router_plug_inject(input, binding)
 
       assert injected ==
                """
@@ -544,6 +546,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
     test "respects windows line endings" do
       schema = Schema.new("Accounts.User", "users", [], [no_scope: true])
       context = Context.new("Accounts", schema, [])
+      binding = [schema: schema, context: context, scope_config: %{scope: %{assign_key: :current_scope}}]
 
       input = """
       defmodule DemoWeb.Router do\r
@@ -559,7 +562,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
       end\r
       """
 
-      {:ok, injected} = Injector.router_plug_inject(input, context)
+      {:ok, injected} = Injector.router_plug_inject(input, binding)
 
       assert injected ==
                """
@@ -581,6 +584,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
     test "errors when :put_secure_browser_headers_is_missing" do
       schema = Schema.new("Accounts.User", "users", [], [no_scope: true])
       context = Context.new("Accounts", schema, [])
+      binding = [schema: schema, context: context, scope_config: %{scope: %{assign_key: :current_scope}}]
 
       input = """
       defmodule DemoWeb.Router do
@@ -595,7 +599,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
       end
       """
 
-      assert {:error, :unable_to_inject} = Injector.router_plug_inject(input, context)
+      assert {:error, :unable_to_inject} = Injector.router_plug_inject(input, binding)
     end
   end
 
@@ -603,10 +607,11 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
     test "returns a string with the expected help text" do
       schema = Schema.new("Accounts.User", "users", [], [no_scope: true])
       context = Context.new("Accounts", schema, [])
+      binding = [schema: schema, context: context, scope_config: %{scope: %{assign_key: :current_scope}}]
 
       file_path = Path.expand("foo.ex")
 
-      assert Injector.router_plug_help_text(file_path, context) ==
+      assert Injector.router_plug_help_text(file_path, binding) ==
                """
                Add the :fetch_current_scope_for_user plug to the :browser pipeline in foo.ex:
 
@@ -617,11 +622,31 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
                    end
                """
     end
+
+    test "adheres to the --assign-key" do
+      schema = Schema.new("Accounts.User", "users", [], [no_scope: true])
+      context = Context.new("Accounts", schema, [])
+      binding = [schema: schema, context: context, scope_config: %{scope: %{assign_key: :current_user_scope}}]
+
+      file_path = Path.expand("foo.ex")
+
+      assert Injector.router_plug_help_text(file_path, binding) ==
+               """
+               Add the :fetch_current_user_scope_for_user plug to the :browser pipeline in foo.ex:
+
+                   pipeline :browser do
+                     ...
+                     plug :put_secure_browser_headers
+                     plug :fetch_current_user_scope_for_user
+                   end
+               """
+    end
   end
 
   describe "app_layout_menu_inject/2" do
     test "injects user menu at the bottom of nav section when it exists" do
       schema = Schema.new("Accounts.User", "users", [], [no_scope: true])
+      binding = [schema: schema, scope_config: %{scope: %{assign_key: :current_scope}}]
 
       template = """
       <!DOCTYPE html>
@@ -646,7 +671,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
       </html>
       """
 
-      {:ok, template_str} = Injector.app_layout_menu_inject(schema, template)
+      {:ok, template_str} = Injector.app_layout_menu_inject(binding, template)
 
       assert template_str ==
                """
@@ -695,6 +720,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
 
     test "injects user menu at the bottom of nav section when it exists with windows line endings" do
       schema = Schema.new("Accounts.User", "users", [], [no_scope: true])
+      binding = [schema: schema, scope_config: %{scope: %{assign_key: :current_scope}}]
 
       template = """
       <!DOCTYPE html>\r
@@ -719,7 +745,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
       </html>\r
       """
 
-      {:ok, template_str} = Injector.app_layout_menu_inject(schema, template)
+      {:ok, template_str} = Injector.app_layout_menu_inject(binding, template)
 
       assert template_str ==
                """
@@ -768,6 +794,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
 
     test "injects render user_menu after the opening body tag" do
       schema = Schema.new("Accounts.User", "users", [], [no_scope: true])
+      binding = [schema: schema, scope_config: %{scope: %{assign_key: :current_scope}}]
 
       template = """
       <!DOCTYPE html>
@@ -785,7 +812,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
       </html>
       """
 
-      {:ok, template_str} = Injector.app_layout_menu_inject(schema, template)
+      {:ok, template_str} = Injector.app_layout_menu_inject(binding, template)
 
       assert template_str ==
                """
@@ -827,6 +854,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
 
     test "works with windows line endings" do
       schema = Schema.new("Accounts.User", "users", [], [no_scope: true])
+      binding = [schema: schema, scope_config: %{scope: %{assign_key: :current_scope}}]
 
       template = """
       <!DOCTYPE html>\r
@@ -844,7 +872,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
       </html>\r
       """
 
-      {:ok, template_str} = Injector.app_layout_menu_inject(schema, template)
+      {:ok, template_str} = Injector.app_layout_menu_inject(binding, template)
 
       assert template_str ==
                """
@@ -886,7 +914,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
 
     test "returns :already_injected when render is already found in file" do
       schema = Schema.new("Accounts.User", "users", [], [no_scope: true])
-
+      binding = [schema: schema, scope_config: %{scope: %{assign_key: :current_scope}}]
       template = """
       <!DOCTYPE html>
       <html lang="en">
@@ -915,21 +943,23 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
       </html>
       """
 
-      assert :already_injected = Injector.app_layout_menu_inject(schema, template)
+      assert :already_injected = Injector.app_layout_menu_inject(binding, template)
     end
 
     test "returns {:error, :unable_to_inject} when the body tag isn't found" do
       schema = Schema.new("Accounts.User", "users", [], [no_scope: true])
-      assert {:error, :unable_to_inject} = Injector.app_layout_menu_inject(schema, "")
+      binding = [schema: schema, scope_config: %{scope: %{assign_key: :current_scope}}]
+      assert {:error, :unable_to_inject} = Injector.app_layout_menu_inject(binding, "")
     end
   end
 
   describe "app_layout_menu_help_text/2" do
     test "returns a string with the expected help text" do
       schema = Schema.new("Accounts.User", "users", [], [no_scope: true])
+      binding = [schema: schema, scope_config: %{scope: %{assign_key: :current_scope}}]
       file_path = Path.expand("foo.ex")
 
-      assert Injector.app_layout_menu_help_text(file_path, schema) =~
+      assert Injector.app_layout_menu_help_text(file_path, binding) =~
                "Add the following user menu"
     end
   end
diff --git a/test/mix/tasks/phx.gen.auth_test.exs b/test/mix/tasks/phx.gen.auth_test.exs
index 59bb70f98..c0d718d41 100644
--- a/test/mix/tasks/phx.gen.auth_test.exs
+++ b/test/mix/tasks/phx.gen.auth_test.exs
@@ -822,7 +822,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         assert_file("lib/my_app/accounts/user.ex", fn file ->
           assert file =~ "field :confirmed_at, :utc_datetime"
           assert file =~ "timestamps(type: :utc_datetime)"
-          assert file =~ "now = DateTime.utc_now() |> DateTime.truncate(:second)"
+          assert file =~ "now = DateTime.utc_now(:second)"
         end)
 
         assert_file("lib/my_app/accounts/user_token.ex", fn file ->
@@ -1311,6 +1311,10 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
             phx.gen.auth will create a new accounts_user scope.
 
+            Note that if you run `phx.gen.live` multiple times, the generated assign key for
+            the generated scopes can conflict with each other. You can pass `--assign-key` to customize
+            the assign key for the generated scope.
+
             Do you want to proceed with the generation?\
             """
 
diff --git a/test/mix/tasks/phx.gen.context_test.exs b/test/mix/tasks/phx.gen.context_test.exs
index fb55f5d8e..a018f4a42 100644
--- a/test/mix/tasks/phx.gen.context_test.exs
+++ b/test/mix/tasks/phx.gen.context_test.exs
@@ -494,7 +494,7 @@ defmodule Mix.Tasks.Phx.Gen.ContextTest do
             assert file =~ "def subscribe_posts(%Scope{} = scope)"
             assert file =~ "def get_post!(%Scope{} = scope, id)"
             assert file =~ "def list_posts(%Scope{} = scope)"
-            assert file =~ "def create_post(%Scope{} = scope, attrs \\\\ %{})"
+            assert file =~ "def create_post(%Scope{} = scope, attrs)"
             assert file =~ "def update_post(%Scope{} = scope, %Post{} = post, attrs)"
             assert file =~ "def delete_post(%Scope{} = scope, %Post{} = post)"
             assert file =~ "def change_post(%Scope{} = scope, %Post{} = post, attrs \\\\ %{})"
diff --git a/test/mix/tasks/phx.gen.schema_test.exs b/test/mix/tasks/phx.gen.schema_test.exs
index 7c546b780..20ba172e0 100644
--- a/test/mix/tasks/phx.gen.schema_test.exs
+++ b/test/mix/tasks/phx.gen.schema_test.exs
@@ -213,9 +213,7 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
 
   test "allows a custom migration dir", config do
     in_tmp_project(config.test, fn ->
-      Gen.Schema.run(
-        ~w(Blog.Post blog_posts title:string --migration-dir priv/custom_dir)
-      )
+      Gen.Schema.run(~w(Blog.Post blog_posts title:string --migration-dir priv/custom_dir))
 
       assert [migration] = Path.wildcard("priv/custom_dir/*_create_blog_posts.exs")
 
@@ -240,9 +238,7 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
 
   test "does not add maps to the required list", config do
     in_tmp_project(config.test, fn ->
-      Gen.Schema.run(
-        ~w(Blog.Post blog_posts title:string tags:map published_at:naive_datetime)
-      )
+      Gen.Schema.run(~w(Blog.Post blog_posts title:string tags:map published_at:naive_datetime))
 
       assert_file("lib/phoenix/blog/post.ex", fn file ->
         assert file =~ "cast(attrs, [:title, :tags, :published_at]"
@@ -283,9 +279,7 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
 
   test "generates unique indices", config do
     in_tmp_project(config.test, fn ->
-      Gen.Schema.run(
-        ~w(Blog.Post posts title:unique secret:redact unique_int:integer:unique)
-      )
+      Gen.Schema.run(~w(Blog.Post posts title:unique secret:redact unique_int:integer:unique))
 
       assert [migration] = Path.wildcard("priv/repo/migrations/*_create_posts.exs")
 
@@ -556,6 +550,48 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
         assert [_] = Path.wildcard("another_app/priv/repo/migrations/*_create_blog_posts.exs")
       end)
     end
+
+    test "generates scoped schema", config do
+      in_tmp_umbrella_project(config.test, fn ->
+        Application.put_env(:phoenix, :generators, context_app: {:another_app, "another_app"})
+
+        with_scope_env(
+          :another_app,
+          [
+            org: [
+              default: true,
+              module: MyApp.Accounts.Scope,
+              assign_key: :current_scope,
+              access_path: [:user, :org_id],
+              schema_key: :org_id,
+              schema_type: :id,
+              schema_table: :organizations
+            ]
+          ],
+          fn ->
+            Gen.Schema.run(~w(Blog.Post blog_posts title:string --scope org --binary-id))
+
+            assert_file("another_app/lib/another_app/blog/post.ex", fn file ->
+              assert file =~ "@primary_key {:id, :binary_id, autogenerate: true}"
+              assert file =~ "field :org_id, :id"
+            end)
+
+            assert [migration] =
+                     Path.wildcard("another_app/priv/repo/migrations/*_create_blog_posts.exs")
+
+            assert_file(migration, fn file ->
+              assert file =~ "create table(:blog_posts, primary_key: false) do"
+              assert file =~ "add :id, :binary_id, primary_key: true"
+
+              assert file =~
+                       "add :org_id, references(:organizations, type: :id, on_delete: :delete_all)"
+
+              assert file =~ "create index(:blog_posts, [:org_id])"
+            end)
+          end
+        )
+      end)
+    end
   end
 
   test "generates scoped schema", config do
@@ -589,6 +625,8 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
 
             assert file =~
                      "add :org_id, references(:organizations, type: :id, on_delete: :delete_all)"
+
+            assert file =~ "create index(:blog_posts, [:org_id])"
           end)
         end
       )
diff --git a/test/phoenix/code_reloader_test.exs b/test/phoenix/code_reloader_test.exs
index 36f2911e2..38426af4e 100644
--- a/test/phoenix/code_reloader_test.exs
+++ b/test/phoenix/code_reloader_test.exs
@@ -8,7 +8,7 @@ defmodule Phoenix.CodeReloaderTest do
   end
 
   def reload(_, _) do
-    {:error, "oops"}
+    {:error, "oops \e[31merror"}
   end
 
   @tag :capture_log
@@ -59,7 +59,7 @@ defmodule Phoenix.CodeReloaderTest do
 
     assert conn.state == :sent
     assert conn.status == 500
-    assert conn.resp_body =~ "oops"
+    assert conn.resp_body =~ "oops error"
     assert conn.resp_body =~ "CompileError"
     assert conn.resp_body =~ "Compilation error"
   end
diff --git a/test/phoenix/router/routing_test.exs b/test/phoenix/router/routing_test.exs
index 38801f7aa..81ab12663 100644
--- a/test/phoenix/router/routing_test.exs
+++ b/test/phoenix/router/routing_test.exs
@@ -63,6 +63,8 @@ defmodule Phoenix.Router.RoutingTest do
       pipe_through :noop
       get "/plug", SomePlug, []
       get "/users/:id/raise", UserController, :raise
+      pipe_through :halt
+      get "/info", UserController, :raise
     end
 
     get "/no_log", SomePlug, [], log: false
@@ -487,46 +489,59 @@ defmodule Phoenix.Router.RoutingTest do
     end
   end
 
-  test "route_info returns route string, path params, and more" do
-    assert Phoenix.Router.route_info(Router, "GET", "foo/bar/baz", nil) == %{
-             log: :debug,
-             path_params: %{"path" => ["foo", "bar", "baz"]},
-             pipe_through: [],
-             plug: Phoenix.Router.RoutingTest.UserController,
-             plug_opts: :not_found,
-             route: "/*path"
-           }
-
-    assert Phoenix.Router.route_info(Router, "GET", "users/1", nil) == %{
-             log: :debug,
-             path_params: %{"id" => "1"},
-             pipe_through: [],
-             plug: Phoenix.Router.RoutingTest.UserController,
-             plug_opts: :show,
-             route: "/users/:id",
-             access: :user
-           }
-
-    assert Phoenix.Router.route_info(Router, "GET", "/", "host") == %{
-             log: :debug,
-             path_params: %{},
-             pipe_through: [],
-             plug: Phoenix.Router.RoutingTest.UserController,
-             plug_opts: :index,
-             route: "/"
-           }
-
-    assert Phoenix.Router.route_info(Router, "POST", "/not-exists", "host") == :error
-  end
+  describe "route_info" do
+    test " returns route string, path params, and more" do
+      assert Phoenix.Router.route_info(Router, "GET", "foo/bar/baz", nil) == %{
+               log: :debug,
+               path_params: %{"path" => ["foo", "bar", "baz"]},
+               pipe_through: [],
+               plug: Phoenix.Router.RoutingTest.UserController,
+               plug_opts: :not_found,
+               route: "/*path"
+             }
+
+      assert Phoenix.Router.route_info(Router, "GET", "users/1", nil) == %{
+               log: :debug,
+               path_params: %{"id" => "1"},
+               pipe_through: [],
+               plug: Phoenix.Router.RoutingTest.UserController,
+               plug_opts: :show,
+               route: "/users/:id",
+               access: :user
+             }
 
-  test "route_info returns route string, path params and more for split path" do
-    assert Phoenix.Router.route_info(Router, "GET", ~w(foo bar baz), nil) == %{
-             log: :debug,
-             path_params: %{"path" => ["foo", "bar", "baz"]},
-             pipe_through: [],
-             plug: Phoenix.Router.RoutingTest.UserController,
-             plug_opts: :not_found,
-             route: "/*path"
-           }
+      assert Phoenix.Router.route_info(Router, "GET", "/", "host") == %{
+               log: :debug,
+               path_params: %{},
+               pipe_through: [],
+               plug: Phoenix.Router.RoutingTest.UserController,
+               plug_opts: :index,
+               route: "/"
+             }
+
+      assert Phoenix.Router.route_info(Router, "POST", "/not-exists", "host") == :error
+    end
+
+    test "returns route string, path params and more for split path" do
+      assert Phoenix.Router.route_info(Router, "GET", ~w(foo bar baz), nil) == %{
+               log: :debug,
+               path_params: %{"path" => ["foo", "bar", "baz"]},
+               pipe_through: [],
+               plug: Phoenix.Router.RoutingTest.UserController,
+               plug_opts: :not_found,
+               route: "/*path"
+             }
+    end
+
+    test "returns accumulated pipe_through metadata" do
+      assert Phoenix.Router.route_info(Router, "GET", "/info", nil) == %{
+               log: :info,
+               path_params: %{},
+               pipe_through: [:noop, :halt],
+               plug: Phoenix.Router.RoutingTest.UserController,
+               plug_opts: :raise,
+               route: "/info"
+             }
+    end
   end
 end