One authentication flow

Dstar events and streams are ordinary HTTP requests. Put the page routes and component dispatcher behind one Phoenix authentication pipeline, and that same gate protects the initial render and every later interaction.

Demo

Browser

Sends the signed session cookie with each request; Datastar actions also carry the CSRF signal.

One Phoenix pipeline

auth gate

Fetch the session, load the current scope, require a user, and reject before dispatch.

Dstar.Page

Only authenticated conns reach mount, event, stream, or component callbacks.

Request Purpose After the same gate
GET /settings Initial HTML mount/2 → render/1
POST /settings/_event/:event Browser event authorize/2 → handle_event/3
POST /settings Long-lived stream authorize/2 → handle_connect/2
POST /ds/:module/:event Shared component event component handle_event/3

How it works

  • Every Dstar request crosses the same Phoenix authentication pipeline.
  • The signed session cookie is already available because every interaction is HTTP.
  • mount/2 runs only on GET; it is not protection for direct event or stream POSTs.
  • authorize/2 handles page- or resource-specific checks before SSE starts.
  • CSRF protection is separate from authentication and still belongs in the browser pipeline.

A dstar "/settings", SettingsPage line expands to three routes: a GET, an event POST, and a stream POST. Phoenix runs every one through the surrounding pipelines. The browser sends the same signed session cookie, so your existing fetch_current_scope and require_authenticated_user plugs are the only authentication path you need. Put dstar_components/2 in that scope too when its handlers require a user.

illustrative router.ex
# router.ex
pipeline :browser do
  plug :accepts, ["html"]
  plug :fetch_session
  plug :fetch_current_scope
  plug Dstar.Plugs.RenameCsrfParam
  plug :protect_from_forgery
end

pipeline :require_authenticated_user do
  plug :require_authenticated_user
end

scope "/", MyAppWeb do
  pipe_through [:browser, :require_authenticated_user]

  dstar "/settings", SettingsPage
  dstar_components "/ds", [SettingsForm]
end

Authentication is not authorization

The pipeline answers “who is this?” once, consistently. A record-level rule still has to answer “may this user change this account?” Use authorize/2 for page event and stream POSTs. It runs after signals are parsed but before the response becomes SSE, so it can still return a normal 401 or 403. On GET, load only an authorized resource in mount/2. Component handlers must do their resource check before calling start/1.

illustrative settings_page.ex
# settings_page.ex — resource authorization, not a second login flow
@impl Dstar.Page
def authorize(conn, {:event, "save:" <> account_id}) do
  case Accounts.fetch_for_scope(conn.assigns.current_scope, account_id) do
    {:ok, account} -> assign(conn, :account, account)

    :error ->
      conn
      |> send_resp(403, "Forbidden")
      |> halt()
  end
end

def authorize(conn, {:event, _event}), do: conn
def authorize(conn, {:stream, _params}), do: conn

If this were LiveView

LiveView uses the same session token, but it has two request lifecycles to guard. The Plug protects the initial HTTP request; an on_mount hook protects the connected LiveView process. Dstar has no authenticated socket process, so every request simply re-enters the Plug pipeline.

Dstar

illustrative router.ex
# router.ex
pipeline :browser do
  plug :accepts, ["html"]
  plug :fetch_session
  plug :fetch_current_scope
  plug Dstar.Plugs.RenameCsrfParam
  plug :protect_from_forgery
end

pipeline :require_authenticated_user do
  plug :require_authenticated_user
end

scope "/", MyAppWeb do
  pipe_through [:browser, :require_authenticated_user]

  dstar "/settings", SettingsPage
  dstar_components "/ds", [SettingsForm]
end

LiveView

illustrative
# Illustrative generated-style LiveView routing.
scope "/", MyAppWeb do
  pipe_through [:browser, :require_authenticated_user]

  live_session :require_authenticated_user,
    on_mount: [{MyAppWeb.UserAuth, :require_authenticated}] do
    live "/settings", SettingsLive, :edit
  end
end