Repository: phoenixframework/phoenix
Pull request #5985 (2024-11-26 16:20:06)

run endpoint config warmup before starting socket
There was a slight time window in between starting the socket listener and actually setting the config in persistent_term that could rely in a crash on incoming requests. This commit fixes this by calling the warmup function as a child in the endpoint supervisor, after the config is started, but before the socket listener is started.

Fixes https://github.com/phoenixframework/phoenix/issues/5981.


Patch:
diff --git a/installer/templates/phx_assets/app.js b/installer/templates/phx_assets/app.js
index 0d9f8cdd1..87ddc5c7e 100644
--- a/installer/templates/phx_assets/app.js
+++ b/installer/templates/phx_assets/app.js
@@ -22,8 +22,8 @@ import "phoenix_html"
 <%= @live_comment %>import {LiveSocket} from "phoenix_live_view"
 <%= @live_comment %>import topbar from "../vendor/topbar"
 
-<%= @live_comment %>let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
-<%= @live_comment %>let liveSocket = new LiveSocket("/live", Socket, {
+<%= @live_comment %>const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
+<%= @live_comment %>const liveSocket = new LiveSocket("/live", Socket, {
 <%= @live_comment %>  longPollFallbackMs: 2500,
 <%= @live_comment %>  params: {_csrf_token: csrfToken}
 <%= @live_comment %>})
@@ -41,4 +41,39 @@ import "phoenix_html"
 // >> liveSocket.enableLatencySim(1000)  // enabled for duration of browser session
 // >> liveSocket.disableLatencySim()
 <%= @live_comment %>window.liveSocket = liveSocket
+
+// The lines below enable quality of life phoenix_live_reload
+// development features:
+//
+//     1. stream server logs to the browser console
+//     2. click on elements to jump to their definitions in your code editor
+//
+<%= @live_comment %>if (process.env.NODE_ENV === "development") {
+<%= @live_comment %>  window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
+<%= @live_comment %>    // Enable server log streaming to client.
+<%= @live_comment %>    // Disable with reloader.disableServerLogs()
+<%= @live_comment %>    reloader.enableServerLogs()
+<%= @live_comment %>
+<%= @live_comment %>    // Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
+<%= @live_comment %>    //
+<%= @live_comment %>    //   * click with "c" key pressed to open at caller location
+<%= @live_comment %>    //   * click with "d" key pressed to open at function component definition location
+<%= @live_comment %>    let keyDown
+<%= @live_comment %>    window.addEventListener("keydown", e => keyDown = e.key)
+<%= @live_comment %>    window.addEventListener("keyup", e => keyDown = null)
+<%= @live_comment %>    window.addEventListener("click", e => {
+<%= @live_comment %>      if(keyDown === "c"){
+<%= @live_comment %>        e.preventDefault()
+<%= @live_comment %>        e.stopImmediatePropagation()
+<%= @live_comment %>        reloader.openEditorAtCaller(e.target)
+<%= @live_comment %>      } else if(keyDown === "d"){
+<%= @live_comment %>        e.preventDefault()
+<%= @live_comment %>        e.stopImmediatePropagation()
+<%= @live_comment %>        reloader.openEditorAtDef(e.target)
+<%= @live_comment %>      }
+<%= @live_comment %>    }, true)
+<%= @live_comment %>
+<%= @live_comment %>    window.liveReloader = reloader
+<%= @live_comment %>  })
+<%= @live_comment %>}
 <% end %>
diff --git a/installer/templates/phx_single/README.md b/installer/templates/phx_single/README.md
index 5c96eb63a..304d84424 100644
--- a/installer/templates/phx_single/README.md
+++ b/installer/templates/phx_single/README.md
@@ -2,8 +2,8 @@
 
 To start your Phoenix server:
 
-  * Run `mix setup` to install and setup dependencies
-  * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
+* Run `mix setup` to install and setup dependencies
+* Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
 
 Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
 
@@ -11,8 +11,8 @@ Ready to run in production? Please [check our deployment guides](https://hexdocs
 
 ## Learn more
 
-  * Official website: https://www.phoenixframework.org/
-  * Guides: https://hexdocs.pm/phoenix/overview.html
-  * Docs: https://hexdocs.pm/phoenix
-  * Forum: https://elixirforum.com/c/phoenix-forum
-  * Source: https://github.com/phoenixframework/phoenix
+* Official website: https://www.phoenixframework.org/
+* Guides: https://hexdocs.pm/phoenix/overview.html
+* Docs: https://hexdocs.pm/phoenix
+* Forum: https://elixirforum.com/c/phoenix-forum
+* Source: https://github.com/phoenixframework/phoenix
diff --git a/installer/templates/phx_single/config/dev.exs b/installer/templates/phx_single/config/dev.exs
index 4dc897f64..3df396383 100644
--- a/installer/templates/phx_single/config/dev.exs
+++ b/installer/templates/phx_single/config/dev.exs
@@ -45,6 +45,7 @@ config :<%= @app_name %>, <%= @endpoint_module %>,
 # Watch static and templates for browser reloading.
 config :<%= @app_name %>, <%= @endpoint_module %>,
   live_reload: [
+    web_console_logger: true,
     patterns: [
       ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",<%= if @gettext do %>
       ~r"priv/gettext/.*(po)$",<% end %>
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/README.md b/installer/templates/phx_umbrella/apps/app_name_web/README.md
index fdb87dfcf..3d9845a58 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/README.md
+++ b/installer/templates/phx_umbrella/apps/app_name_web/README.md
@@ -2,8 +2,8 @@
 
 To start your Phoenix server:
 
-  * Run `mix setup` to install and setup dependencies
-  * Start Phoenix endpoint with `mix phx.server`
+* Run `mix setup` to install and setup dependencies
+* Start Phoenix endpoint with `mix phx.server`
 
 Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
 
@@ -11,8 +11,8 @@ Ready to run in production? Please [check our deployment guides](https://hexdocs
 
 ## Learn more
 
-  * Official website: https://www.phoenixframework.org/
-  * Guides: https://hexdocs.pm/phoenix/overview.html
-  * Docs: https://hexdocs.pm/phoenix
-  * Forum: https://elixirforum.com/c/phoenix-forum
-  * Source: https://github.com/phoenixframework/phoenix
+* Official website: https://www.phoenixframework.org/
+* Guides: https://hexdocs.pm/phoenix/overview.html
+* Docs: https://hexdocs.pm/phoenix
+* Forum: https://elixirforum.com/c/phoenix-forum
+* Source: https://github.com/phoenixframework/phoenix
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 bcc1304b4..d73204be7 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
@@ -45,6 +45,7 @@ config :<%= @web_app_name %>, <%= @endpoint_module %>,
 # Watch static and templates for browser reloading.
 config :<%= @web_app_name %>, <%= @endpoint_module %>,
   live_reload: [
+    web_console_logger: true,
     patterns: [
       ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",<%= if @gettext do %>
       ~r"priv/gettext/.*(po)$",<% end %>
diff --git a/installer/templates/phx_web/components/core_components.ex b/installer/templates/phx_web/components/core_components.ex
index d639407e8..c604ef553 100644
--- a/installer/templates/phx_web/components/core_components.ex
+++ b/installer/templates/phx_web/components/core_components.ex
@@ -76,7 +76,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
 
   def flash_group(assigns) do
     ~H"""
-    <div id={@id}>
+    <div id={@id} aria-live="polite">
       <.flash kind={:info} title=<%= maybe_heex_attr_gettext.("Success!", @gettext) %> flash={@flash} />
       <.flash kind={:error} title=<%= maybe_heex_attr_gettext.("Error!", @gettext) %> flash={@flash} />
       <.flash
diff --git a/installer/templates/phx_web/components/layouts/root.html.heex b/installer/templates/phx_web/components/layouts/root.html.heex
index 12f7f1d87..fdbd4aa4e 100644
--- a/installer/templates/phx_web/components/layouts/root.html.heex
+++ b/installer/templates/phx_web/components/layouts/root.html.heex
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html lang="en" class="[scrollbar-gutter:stable]">
+<html lang="en">
   <head>
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
diff --git a/installer/templates/phx_web/endpoint.ex b/installer/templates/phx_web/endpoint.ex
index f14b4d97b..4b37bf5bc 100644
--- a/installer/templates/phx_web/endpoint.ex
+++ b/installer/templates/phx_web/endpoint.ex
@@ -17,12 +17,13 @@ defmodule <%= @endpoint_module %> do
 
   # Serve at "/" the static files from "priv/static" directory.
   #
-  # You should set gzip to true if you are running phx.digest
-  # when deploying your static files in production.
+  # When code reloading is disabled (e.g., in production),
+  # the `gzip` option is enabled to serve compressed
+  # static files generated by running `phx.digest`.
   plug Plug.Static,
     at: "/",
     from: :<%= @web_app_name %>,
-    gzip: false,
+    gzip: not code_reloading?,
     only: <%= @web_namespace %>.static_paths()
 
   # Code reloading can be explicitly enabled under the
diff --git a/lib/mix/tasks/phx.gen.context.ex b/lib/mix/tasks/phx.gen.context.ex
index a95172075..1dfad60e3 100644
--- a/lib/mix/tasks/phx.gen.context.ex
+++ b/lib/mix/tasks/phx.gen.context.ex
@@ -89,7 +89,8 @@ defmodule Mix.Tasks.Phx.Gen.Context do
     merge_with_existing_context: :boolean,
     prefix: :string,
     live: :boolean,
-    compile: :boolean
+    compile: :boolean,
+    primary_key: :string
   ]
 
   @default_opts [schema: true, context: true]
diff --git a/lib/mix/tasks/phx.gen.schema.ex b/lib/mix/tasks/phx.gen.schema.ex
index 7d2da6e63..68f3c82dd 100644
--- a/lib/mix/tasks/phx.gen.schema.ex
+++ b/lib/mix/tasks/phx.gen.schema.ex
@@ -86,6 +86,13 @@ defmodule Mix.Tasks.Phx.Gen.Schema do
   Generated migration can use `binary_id` for schema's primary key
   and its references with option `--binary-id`.
 
+  ## primary_key
+
+  By default, the primary key in the table is called `id`. This option
+  allows to change the name of the primary key column. For example:
+
+      $ mix phx.gen.schema Blog.post posts --primary-key post_id
+
   ## repo
 
   Generated migration can use `repo` to set the migration repository
@@ -146,7 +153,8 @@ defmodule Mix.Tasks.Phx.Gen.Schema do
   alias Mix.Phoenix.Schema
 
   @switches [migration: :boolean, binary_id: :boolean, table: :string, web: :string,
-    context_app: :string, prefix: :string, repo: :string, migration_dir: :string]
+    context_app: :string, prefix: :string, repo: :string, migration_dir: :string,
+    primary_key: :string]
 
   @doc false
   def run(args) do
diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex
index 597f175d9..da2ffdb83 100644
--- a/lib/phoenix/endpoint/supervisor.ex
+++ b/lib/phoenix/endpoint/supervisor.ex
@@ -13,7 +13,6 @@ defmodule Phoenix.Endpoint.Supervisor do
     with {:ok, pid} = ok <- Supervisor.start_link(__MODULE__, {otp_app, mod, opts}, name: mod) do
       # We don't use the defaults in the checks below
       conf = Keyword.merge(Application.get_env(otp_app, mod, []), opts)
-      warmup(mod)
       log_access_url(mod, conf)
       browser_open(mod, conf)
 
@@ -83,6 +82,7 @@ defmodule Phoenix.Endpoint.Supervisor do
 
     children =
       config_children(mod, secret_conf, default_conf) ++
+        warmup_children(mod) ++
         pubsub_children(mod, conf) ++
         socket_children(mod, conf, :child_spec) ++
         server_children(mod, conf, server?) ++
@@ -156,6 +156,10 @@ defmodule Phoenix.Endpoint.Supervisor do
     [{Phoenix.Config, args}]
   end
 
+  defp warmup_children(mod) do
+    [%{id: :warmup, start: {__MODULE__, :warmup, [mod]}}]
+  end
+
   defp server_children(mod, config, server?) do
     cond do
       server? ->
@@ -346,6 +350,11 @@ defmodule Phoenix.Endpoint.Supervisor do
     rescue
       e -> Logger.error("Could not warm up static assets: #{Exception.message(e)}")
     end
+
+    # To prevent a race condition where the socket listener is already started
+    # but the config not warmed up, we run warmup/1 as a child in the supervision
+    # tree. As we don't actually want to start a process, we return :ignore here.
+    :ignore
   end
 
   defp warmup_persistent(endpoint) do
diff --git a/priv/templates/phx.gen.auth/confirmation_instructions_live.ex b/priv/templates/phx.gen.auth/confirmation_instructions_live.ex
index ac8d9b298..bf54a9933 100644
--- a/priv/templates/phx.gen.auth/confirmation_instructions_live.ex
+++ b/priv/templates/phx.gen.auth/confirmation_instructions_live.ex
@@ -12,7 +12,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       </.header>
 
       <.simple_form for={@form} id="resend_confirmation_form" phx-submit="send_instructions">
-        <.input field={@form[:email]} type="email" placeholder="Email" required />
+        <.input
+          field={@form[:email]}
+          type="email"
+          placeholder="Email"
+          autocomplete="username"
+          required
+        />
         <:actions>
           <.button phx-disable-with="Sending..." class="w-full">
             Resend confirmation instructions
diff --git a/priv/templates/phx.gen.auth/confirmation_new.html.heex b/priv/templates/phx.gen.auth/confirmation_new.html.heex
index 97b952445..7edb1088c 100644
--- a/priv/templates/phx.gen.auth/confirmation_new.html.heex
+++ b/priv/templates/phx.gen.auth/confirmation_new.html.heex
@@ -5,7 +5,7 @@
   </.header>
 
   <.simple_form :let={f} for={@conn.params["<%= schema.singular %>"]} as={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/confirm"}>
-    <.input field={f[:email]} type="email" placeholder="Email" required />
+    <.input field={f[:email]} type="email" placeholder="Email" autocomplete="username" required />
     <:actions>
       <.button phx-disable-with="Sending..." class="w-full">
         Resend confirmation instructions
diff --git a/priv/templates/phx.gen.auth/forgot_password_live.ex b/priv/templates/phx.gen.auth/forgot_password_live.ex
index dc4a03a59..9c30c3c90 100644
--- a/priv/templates/phx.gen.auth/forgot_password_live.ex
+++ b/priv/templates/phx.gen.auth/forgot_password_live.ex
@@ -12,7 +12,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       </.header>
 
       <.simple_form for={@form} id="reset_password_form" phx-submit="send_email">
-        <.input field={@form[:email]} type="email" placeholder="Email" required />
+        <.input
+          field={@form[:email]}
+          type="email"
+          placeholder="Email"
+          autocomplete="username"
+          required
+        />
         <:actions>
           <.button phx-disable-with="Sending..." class="w-full">
             Send password reset instructions
diff --git a/priv/templates/phx.gen.auth/login_live.ex b/priv/templates/phx.gen.auth/login_live.ex
index e9957f607..77bb16d79 100644
--- a/priv/templates/phx.gen.auth/login_live.ex
+++ b/priv/templates/phx.gen.auth/login_live.ex
@@ -16,8 +16,14 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       </.header>
 
       <.simple_form for={@form} id="login_form" action={~p"<%= schema.route_prefix %>/log-in"} phx-update="ignore">
-        <.input field={@form[:email]} type="email" label="Email" required />
-        <.input field={@form[:password]} type="password" label="Password" required />
+        <.input field={@form[:email]} type="email" label="Email" autocomplete="username" required />
+        <.input
+          field={@form[:password]}
+          type="password"
+          label="Password"
+          autocomplete="current-password"
+          required
+        />
 
         <:actions>
           <.input field={@form[:remember_me]} type="checkbox" label="Keep me logged in" />
diff --git a/priv/templates/phx.gen.auth/registration_live.ex b/priv/templates/phx.gen.auth/registration_live.ex
index f5c5ef830..31cb8ad8e 100644
--- a/priv/templates/phx.gen.auth/registration_live.ex
+++ b/priv/templates/phx.gen.auth/registration_live.ex
@@ -31,8 +31,14 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
           Oops, something went wrong! Please check the errors below.
         </.error>
 
-        <.input field={@form[:email]} type="email" label="Email" required />
-        <.input field={@form[:password]} type="password" label="Password" required />
+        <.input field={@form[:email]} type="email" label="Email" autocomplete="username" required />
+        <.input
+          field={@form[:password]}
+          type="password"
+          label="Password"
+          autocomplete="new-password"
+          required
+        />
 
         <:actions>
           <.button phx-disable-with="Creating account..." class="w-full">Create an account</.button>
diff --git a/priv/templates/phx.gen.auth/registration_new.html.heex b/priv/templates/phx.gen.auth/registration_new.html.heex
index ae1c5fbb8..cff2b3819 100644
--- a/priv/templates/phx.gen.auth/registration_new.html.heex
+++ b/priv/templates/phx.gen.auth/registration_new.html.heex
@@ -15,8 +15,14 @@
       Oops, something went wrong! Please check the errors below.
     </.error>
 
-    <.input field={f[:email]} type="email" label="Email" required />
-    <.input field={f[:password]} type="password" label="Password" required />
+    <.input field={f[:email]} type="email" label="Email" autocomplete="username" required />
+    <.input
+      field={f[:password]}
+      type="password"
+      label="Password"
+      autocomplete="new-password"
+      required
+    />
 
     <:actions>
       <.button phx-disable-with="Creating account..." class="w-full">Create an account</.button>
diff --git a/priv/templates/phx.gen.auth/reset_password_edit.html.heex b/priv/templates/phx.gen.auth/reset_password_edit.html.heex
index 29615463d..729b497c4 100644
--- a/priv/templates/phx.gen.auth/reset_password_edit.html.heex
+++ b/priv/templates/phx.gen.auth/reset_password_edit.html.heex
@@ -8,11 +8,18 @@
       Oops, something went wrong! Please check the errors below.
     </.error>
 
-    <.input field={f[:password]} type="password" label="New Password" required />
+    <.input
+      field={f[:password]}
+      type="password"
+      label="New Password"
+      autocomplete="new-password"
+      required
+    />
     <.input
       field={f[:password_confirmation]}
       type="password"
       label="Confirm new password"
+      autocomplete="new-password"
       required
     />
     <:actions>
diff --git a/priv/templates/phx.gen.auth/reset_password_live.ex b/priv/templates/phx.gen.auth/reset_password_live.ex
index 8d07fd8a8..02ae26850 100644
--- a/priv/templates/phx.gen.auth/reset_password_live.ex
+++ b/priv/templates/phx.gen.auth/reset_password_live.ex
@@ -18,11 +18,18 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
           Oops, something went wrong! Please check the errors below.
         </.error>
 
-        <.input field={@form[:password]} type="password" label="New password" required />
+        <.input
+          field={@form[:password]}
+          type="password"
+          label="New password"
+          autocomplete="new-password"
+          required
+        />
         <.input
           field={@form[:password_confirmation]}
           type="password"
           label="Confirm new password"
+          autocomplete="new-password"
           required
         />
         <:actions>
diff --git a/priv/templates/phx.gen.auth/reset_password_new.html.heex b/priv/templates/phx.gen.auth/reset_password_new.html.heex
index 3d2e40b0d..6f254d1e8 100644
--- a/priv/templates/phx.gen.auth/reset_password_new.html.heex
+++ b/priv/templates/phx.gen.auth/reset_password_new.html.heex
@@ -5,7 +5,7 @@
   </.header>
 
   <.simple_form :let={f} for={@conn.params["<%= schema.singular %>"]} as={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/reset-password"}>
-    <.input field={f[:email]} type="email" placeholder="Email" required />
+    <.input field={f[:email]} type="email" placeholder="Email" autocomplete="username" required />
     <:actions>
       <.button phx-disable-with="Sending..." class="w-full">
         Send password reset instructions
diff --git a/priv/templates/phx.gen.auth/session_new.html.heex b/priv/templates/phx.gen.auth/session_new.html.heex
index abddb592e..1e7abc4fc 100644
--- a/priv/templates/phx.gen.auth/session_new.html.heex
+++ b/priv/templates/phx.gen.auth/session_new.html.heex
@@ -13,8 +13,14 @@
   <.simple_form :let={f} for={@conn.params["<%= schema.singular %>"]} as={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/log-in"}>
     <.error :if={@error_message}><%%= @error_message %></.error>
 
-    <.input field={f[:email]} type="email" label="Email" required />
-    <.input field={f[:password]} type="password" label="Password" required />
+    <.input field={f[:email]} type="email" label="Email" autocomplete="username" required />
+    <.input
+      field={f[:password]}
+      type="password"
+      label="Password"
+      autocomplete="current-password"
+      required
+    />
 
     <:actions :let={f}>
       <.input field={f[:remember_me]} type="checkbox" label="Keep me logged in" />
diff --git a/priv/templates/phx.gen.auth/settings_edit.html.heex b/priv/templates/phx.gen.auth/settings_edit.html.heex
index bebc45cbf..e741536a9 100644
--- a/priv/templates/phx.gen.auth/settings_edit.html.heex
+++ b/priv/templates/phx.gen.auth/settings_edit.html.heex
@@ -12,12 +12,13 @@
 
       <input type="hidden" name="action" value="update_email" />
 
-      <.input field={f[:email]} type="email" label="Email" required />
+      <.input field={f[:email]} type="email" label="Email" autocomplete="username" required />
       <.input
         field={f[:current_password]}
         name="current_password"
         type="password"
         label="Current Password"
+        autocomplete="current-password"
         required
         id="current_password_for_email"
       />
@@ -39,11 +40,18 @@
 
       <input type="hidden" name="action" value="update_password" />
 
-      <.input field={f[:password]} type="password" label="New password" required />
+      <.input
+        field={f[:password]}
+        type="password"
+        label="New password"
+        autocomplete="new-password"
+        required
+      />
       <.input
         field={f[:password_confirmation]}
         type="password"
         label="Confirm new password"
+        autocomplete="new-password"
         required
       />
 
@@ -53,6 +61,7 @@
         type="password"
         label="Current password"
         id="current_password_for_password"
+        autocomplete="current-password"
         required
       />
       <:actions>
diff --git a/priv/templates/phx.gen.auth/settings_live.ex b/priv/templates/phx.gen.auth/settings_live.ex
index a1deeb080..7dc7c999b 100644
--- a/priv/templates/phx.gen.auth/settings_live.ex
+++ b/priv/templates/phx.gen.auth/settings_live.ex
@@ -18,7 +18,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
           phx-submit="update_email"
           phx-change="validate_email"
         >
-          <.input field={@email_form[:email]} type="email" label="Email" required />
+          <.input
+            field={@email_form[:email]}
+            type="email"
+            label="Email"
+            autocomplete="username"
+            required
+          />
           <.input
             field={@email_form[:current_password]}
             name="current_password"
@@ -26,6 +32,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
             type="password"
             label="Current password"
             value={@email_form_current_password}
+            autocomplete="current-password"
             required
           />
           <:actions>
@@ -47,13 +54,21 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
             name={@password_form[:email].name}
             type="hidden"
             id="hidden_<%= schema.singular %>_email"
+            autocomplete="username"
             value={@current_email}
           />
-          <.input field={@password_form[:password]} type="password" label="New password" required />
+          <.input
+            field={@password_form[:password]}
+            type="password"
+            label="New password"
+            autocomplete="new-password"
+            required
+          />
           <.input
             field={@password_form[:password_confirmation]}
             type="password"
             label="Confirm new password"
+            autocomplete="new-password"
           />
           <.input
             field={@password_form[:current_password]}
@@ -62,6 +77,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
             label="Current password"
             id="current_password_for_password"
             value={@current_password}
+            autocomplete="current-password"
             required
           />
           <:actions>
diff --git a/priv/templates/phx.gen.html/index.html.heex b/priv/templates/phx.gen.html/index.html.heex
index 772a71077..c1f89108a 100644
--- a/priv/templates/phx.gen.html/index.html.heex
+++ b/priv/templates/phx.gen.html/index.html.heex
@@ -1,7 +1,7 @@
 <.header>
   Listing <%= schema.human_plural %>
   <:actions>
-    <.button phx-click={JS.dispatch("click", to: {:inner, "a"})}> 
+    <.button phx-click={JS.dispatch("click", to: {:inner, "a"})}>
       <.link href={~p"<%= schema.route_prefix %>/new"}>
         New <%= schema.human_singular %>
       </.link>
diff --git a/priv/templates/phx.gen.live/core_components.ex b/priv/templates/phx.gen.live/core_components.ex
index d639407e8..c604ef553 100644
--- a/priv/templates/phx.gen.live/core_components.ex
+++ b/priv/templates/phx.gen.live/core_components.ex
@@ -76,7 +76,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
 
   def flash_group(assigns) do
     ~H"""
-    <div id={@id}>
+    <div id={@id} aria-live="polite">
       <.flash kind={:info} title=<%= maybe_heex_attr_gettext.("Success!", @gettext) %> flash={@flash} />
       <.flash kind={:error} title=<%= maybe_heex_attr_gettext.("Error!", @gettext) %> flash={@flash} />
       <.flash
diff --git a/priv/templates/phx.gen.live/index.ex b/priv/templates/phx.gen.live/index.ex
index ff23665ab..36f2e11cc 100644
--- a/priv/templates/phx.gen.live/index.ex
+++ b/priv/templates/phx.gen.live/index.ex
@@ -31,7 +31,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       </:action>
       <:action :let={{id, <%= schema.singular %>}}>
         <.link
-          phx-click={JS.push("delete", value: %{id: <%= schema.singular %>.id}) |> hide("##{id}")}
+          phx-click={JS.push("delete", value: %{id: <%= schema.singular %>.<%= schema.opts[:primary_key] || :id %>}) |> hide("##{id}")}
           data-confirm="Are you sure?"
         >
           Delete
@@ -45,7 +45,8 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   def mount(_params, _session, socket) do
     {:ok,
      socket
-     |> assign(:page_title, "Listing <%= schema.human_plural %>")
+     |> assign(:page_title, "Listing <%= schema.human_plural %>")<%= if schema.opts[:primary_key] do %>
+     |> stream_configure(:<%= schema.collection %>, dom_id: &"<%= schema.table %>-#{&1.<%= schema.opts[:primary_key] %>}")<% end %>
      |> stream(:<%= schema.collection %>, <%= inspect context.alias %>.list_<%= schema.plural %>())}
   end
 
diff --git a/priv/templates/phx.gen.live/show.ex b/priv/templates/phx.gen.live/show.ex
index 140016c74..6973b730b 100644
--- a/priv/templates/phx.gen.live/show.ex
+++ b/priv/templates/phx.gen.live/show.ex
@@ -7,7 +7,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   def render(assigns) do
     ~H"""
     <.header>
-      <%= schema.human_singular %> <%%= @<%= schema.singular %>.id %>
+      <%= schema.human_singular %> <%%= @<%= schema.singular %>.<%= schema.opts[:primary_key] || :id %> %>
       <:subtitle>This is a <%= schema.singular %> record from your database.</:subtitle>
       <:actions>
         <.button phx-click={JS.dispatch("click", to: {:inner, "a"})}>
diff --git a/priv/templates/phx.gen.schema/migration.exs b/priv/templates/phx.gen.schema/migration.exs
index ed6c032fe..7b3fcc3a1 100644
--- a/priv/templates/phx.gen.schema/migration.exs
+++ b/priv/templates/phx.gen.schema/migration.exs
@@ -2,9 +2,10 @@ defmodule <%= inspect schema.repo %>.Migrations.Create<%= Macro.camelize(schema.
   use <%= inspect schema.migration_module %>
 
   def change do
-    create table(:<%= schema.table %><%= if schema.binary_id do %>, primary_key: false<% end %><%= if schema.prefix do %>, prefix: :<%= schema.prefix %><% end %>) do
-<%= if schema.binary_id do %>      add :id, :binary_id, primary_key: true
-<% end %><%= for {k, v} <- schema.attrs do %>      add <%= inspect k %>, <%= inspect Mix.Phoenix.Schema.type_for_migration(v) %><%= schema.migration_defaults[k] %>
+    create table(:<%= schema.table %><%= if schema.binary_id || schema.opts[:primary_key] do %>, primary_key: false<% end %><%= if schema.prefix do %>, prefix: :<%= schema.prefix %><% end %>) do
+<%= if schema.binary_id do %>      add :<%= schema.opts[:primary_key] || :id %>, :binary_id, primary_key: true
+<% else %><%= if schema.opts[:primary_key] do %>      add :<%= schema.opts[:primary_key] %>, :id, primary_key: true
+<% end %><% end %><%= for {k, v} <- schema.attrs do %>      add <%= inspect k %>, <%= inspect Mix.Phoenix.Schema.type_for_migration(v) %><%= schema.migration_defaults[k] %>
 <% end %><%= for {_, i, _, s} <- schema.assocs do %>      add <%= inspect(i) %>, references(<%= inspect(s) %>, on_delete: :nothing<%= if schema.binary_id do %>, type: :binary_id<% end %>)
 <% end %>
       timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}" %>)
diff --git a/priv/templates/phx.gen.schema/schema.ex b/priv/templates/phx.gen.schema/schema.ex
index 94c40751d..082b3f43d 100644
--- a/priv/templates/phx.gen.schema/schema.ex
+++ b/priv/templates/phx.gen.schema/schema.ex
@@ -2,9 +2,11 @@ defmodule <%= inspect schema.module %> do
   use Ecto.Schema
   import Ecto.Changeset
 <%= if schema.prefix do %>
-  @schema_prefix :<%= schema.prefix %><% end %><%= if schema.binary_id do %>
-  @primary_key {:id, :binary_id, autogenerate: true}
-  @foreign_key_type :binary_id<% end %>
+  @schema_prefix :<%= schema.prefix %><% end %><%= if schema.opts[:primary_key] do %>
+  @derive {Phoenix.Param, key: :<%= schema.opts[:primary_key] %>}<% end %><%= if schema.binary_id do %>
+  @primary_key {:<%= schema.opts[:primary_key] || :id %>, :binary_id, autogenerate: true}
+  @foreign_key_type :binary_id<% else %><%= if schema.opts[:primary_key] do %>
+  @primary_key {:<%= schema.opts[:primary_key] %>, :id, autogenerate: true}<% end %><% end %>
   schema <%= inspect schema.table %> do
 <%= Mix.Phoenix.Schema.format_fields_for_schema(schema) %>
 <%= for {_, k, _, _} <- schema.assocs do %>    field <%= inspect k %>, <%= if schema.binary_id do %>:binary_id<% else %>:id<% end %>


Test patch:
diff --git a/priv/templates/phx.gen.context/test_cases.exs b/priv/templates/phx.gen.context/test_cases.exs
index 3931af5c6..a6f9cfff9 100644
--- a/priv/templates/phx.gen.context/test_cases.exs
+++ b/priv/templates/phx.gen.context/test_cases.exs
@@ -13,7 +13,7 @@
 
     test "get_<%= schema.singular %>!/1 returns the <%= schema.singular %> with given id" do
       <%= schema.singular %> = <%= schema.singular %>_fixture()
-      assert <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.id) == <%= schema.singular %>
+      assert <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.<%= schema.opts[:primary_key] || :id %>) == <%= schema.singular %>
     end
 
     test "create_<%= schema.singular %>/1 with valid data creates a <%= schema.singular %>" do
@@ -38,13 +38,13 @@
     test "update_<%= schema.singular %>/2 with invalid data returns error changeset" do
       <%= schema.singular %> = <%= schema.singular %>_fixture()
       assert {:error, %Ecto.Changeset{}} = <%= inspect context.alias %>.update_<%= schema.singular %>(<%= schema.singular %>, @invalid_attrs)
-      assert <%= schema.singular %> == <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.id)
+      assert <%= schema.singular %> == <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.<%= schema.opts[:primary_key] || :id %>)
     end
 
     test "delete_<%= schema.singular %>/1 deletes the <%= schema.singular %>" do
       <%= schema.singular %> = <%= schema.singular %>_fixture()
       assert {:ok, %<%= inspect schema.alias %>{}} = <%= inspect context.alias %>.delete_<%= schema.singular %>(<%= schema.singular %>)
-      assert_raise Ecto.NoResultsError, fn -> <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.id) end
+      assert_raise Ecto.NoResultsError, fn -> <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.<%= schema.opts[:primary_key] || :id %>) end
     end
 
     test "change_<%= schema.singular %>/1 returns a <%= schema.singular %> changeset" do
diff --git a/priv/templates/phx.gen.live/live_test.exs b/priv/templates/phx.gen.live/live_test.exs
index 7fce16ea0..dd90d352d 100644
--- a/priv/templates/phx.gen.live/live_test.exs
+++ b/priv/templates/phx.gen.live/live_test.exs
@@ -54,7 +54,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
       assert {:ok, form_live, _html} =
                index_live
-               |> element("#<%= schema.plural %>-#{<%= schema.singular %>.id} a", "Edit")
+               |> element("#<%= schema.plural %>-#{<%= schema.singular %>.<%= schema.opts[:primary_key] || :id %>} a", "Edit")
                |> render_click()
                |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}/edit")
 
@@ -78,8 +78,8 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     test "deletes <%= schema.singular %> in listing", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       {:ok, index_live, _html} = live(conn, ~p"<%= schema.route_prefix %>")
 
-      assert index_live |> element("#<%= schema.plural %>-#{<%= schema.singular %>.id} a", "Delete") |> render_click()
-      refute has_element?(index_live, "#<%= schema.plural %>-#{<%= schema.singular %>.id}")
+      assert index_live |> element("#<%= schema.plural %>-#{<%= schema.singular %>.<%= schema.opts[:primary_key] || :id %>} a", "Delete") |> render_click()
+      refute has_element?(index_live, "#<%= schema.plural %>-#{<%= schema.singular %>.<%= schema.opts[:primary_key] || :id %>}")
     end
   end
 
diff --git a/test/mix/tasks/phx.gen.schema_test.exs b/test/mix/tasks/phx.gen.schema_test.exs
index e7f62cd9d..f808e8784 100644
--- a/test/mix/tasks/phx.gen.schema_test.exs
+++ b/test/mix/tasks/phx.gen.schema_test.exs
@@ -303,6 +303,25 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
     end
   end
 
+  test "generates migration with custom primary key", config do
+    in_tmp_project config.test, fn ->
+      Gen.Schema.run(~w(Blog.Post posts title user_id:references:users --binary-id --primary-key post_id))
+
+      assert_file "lib/phoenix/blog/post.ex", fn file ->
+        assert file =~ "@derive {Phoenix.Param, key: :post_id}"
+        assert file =~ "@primary_key {:post_id, :binary_id, autogenerate: true}"
+        assert file =~ "field :user_id, :binary_id"
+      end
+
+      assert [migration] = Path.wildcard("priv/repo/migrations/*_create_posts.exs")
+      assert_file migration, fn file ->
+        assert file =~ "create table(:posts, primary_key: false) do"
+        assert file =~ "add :post_id, :binary_id, primary_key: true"
+        assert file =~ "add :user_id, references(:users, on_delete: :nothing, type: :binary_id)"
+      end
+    end
+  end
+
   test "generates schema and migration with prefix", config do
     in_tmp_project config.test, fn ->
       Gen.Schema.run(~w(Blog.Post posts title --prefix cms))