# `Phoenix.Token` beyond `mix phx.gen.auth`

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`.

```elixir
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:

```elixir
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:

```elixir
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:

```elixir
# 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.

[`Phoenix.Token` docs](https://hexdocs.pm/phoenix/Phoenix.Token.html)


---

Created by: almirsarajcic
Date: April 15, 2026
URL: https://elixirdrops.net/d/tx1mHVTh
