We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
`Phoenix.Token` beyond `mix phx.gen.auth`
almirsarajcic
If you’ve used mix phx.gen.auth, you’ve already seen Phoenix.Token at work — it’s what powers the email verification and password reset links in the generated code. But most developers never reach for it directly, and it solves a whole class of problems that have nothing to do with authentication.
The clearest example: unsubscribe links.
Every email your app sends needs one. The naive approach stores a token in the database, looks it up on click, then deletes it. Phoenix.Token makes that unnecessary — the token itself is the proof, signed with your app’s secret_key_base.
defmodule MyApp.Emails do
# Generate a signed unsubscribe URL — no database record needed
def unsubscribe_url(user) do
token = Phoenix.Token.sign(MyAppWeb.Endpoint, "unsubscribe", user.id)
MyAppWeb.Endpoint.url() <> "/unsubscribe?token=#{token}"
end
end
defmodule MyAppWeb.UnsubscribeController do
use MyAppWeb, :controller
# max_age: :infinity — unsubscribe links should never expire
def show(conn, %{"token" => token}) do
case Phoenix.Token.verify(conn, "unsubscribe", token, max_age: :infinity) do
{:ok, user_id} -> render(conn, :confirm, user_id: user_id)
{:error, _} -> render(conn, :invalid)
end
end
def delete(conn, %{"token" => token}) do
case Phoenix.Token.verify(conn, "unsubscribe", token, max_age: :infinity) do
{:ok, user_id} ->
Accounts.unsubscribe(user_id)
render(conn, :success)
{:error, _} ->
render(conn, :invalid)
end
end
end
The salt ("unsubscribe") scopes the token — it can’t be used to trigger anything else in your app, even if someone figures out the structure.
Other places this pattern fits:
Invite links — encode the inviting org and the invitee’s email directly in the token. No pending invitation row required until they actually accept:
token = Phoenix.Token.sign(MyAppWeb.Endpoint, "org invite", %{
org_id: org.id,
email: invitee_email,
role: :member
})
One-time download links — sign a file path with a short max_age. The link expires without any scheduled cleanup job:
token = Phoenix.Token.sign(MyAppWeb.Endpoint, "download", file.id)
# max_age: 600 — expires in 10 minutes
Phoenix.Token.verify(MyAppWeb.Endpoint, "download", token, max_age: 600)
Cross-app tokens — two Phoenix apps that share the same secret_key_base can verify each other’s tokens. Simple internal service auth without a full OAuth setup:
# App A signs
token = Phoenix.Token.sign(AppAWeb.Endpoint, "internal", %{service: "worker", job_id: 42})
# App B verifies (same secret_key_base configured in both)
Phoenix.Token.verify(AppBWeb.Endpoint, "internal", token, max_age: 60)
The common thread: anywhere you’d normally create a database row just to hold a temporary token, Phoenix.Token is likely a better fit. No cleanup jobs, no expiry columns, no lookup queries — the token carries everything and expires on its own.
copied to clipboard