tutorial

How to Monitor Pow with Vigilmon

Add health checks, uptime monitoring, and alerts to Phoenix apps using Pow authentication — detect user registration flow failures, session store degradation, and password reset issues before they lock users out.

How to Monitor Pow with Vigilmon

Pow is the modular, extensible authentication and user management framework for Phoenix. It provides registration, session management, password reset, email confirmation, and token-based API authentication — the full authentication stack. Unlike some auth libraries, Pow integrates deeply with your database schema and Phoenix router, making it fast and flexible.

But authentication is a critical user path. When Pow degrades — database connectivity issues preventing login, session store failures, or email delivery problems blocking registration — the impact is immediate and widespread. Users can't log in, sign up, or reset their passwords.

This tutorial adds observability to Pow-based authentication with Vigilmon:

  • A health endpoint that verifies database connectivity and session infrastructure
  • Uptime monitoring for your authentication flows
  • Heartbeat monitoring for email confirmation and cleanup jobs
  • Slack alerts when authentication degrades

Step 1: Add a health check that covers Pow's dependencies

Pow depends on your database (for user storage and sessions) and optionally on email delivery (for confirmations and password resets). Include both in your health check.

# lib/my_app_web/plugs/health_check.ex
defmodule MyAppWeb.Plugs.HealthCheck do
  import Plug.Conn

  def init(opts), do: opts

  def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
    checks = run_checks()
    status = if all_ok?(checks), do: 200, else: 503

    conn
    |> put_resp_content_type("application/json")
    |> send_resp(status, Jason.encode!(%{status: status_label(status), checks: checks}))
    |> halt()
  end

  def call(conn, _opts), do: conn

  defp run_checks do
    %{
      database: check_database(),
      session_store: check_session_store(),
      memory: check_memory()
    }
  end

  defp check_database do
    # Pow stores users and sessions in Postgres by default
    case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
      {:ok, _} -> :ok
      {:error, _} -> :error
    end
  end

  defp check_session_store do
    # Verify Pow's ETS session store is accessible
    # Pow.Store.CredentialsCache uses a backend store — check it directly
    try do
      store_backend = Application.get_env(:my_app, :pow)[:cache_store_backend] ||
                      Pow.Store.Backend.EtsCache

      # A quick get returns nil for unknown keys but won't crash if the store is healthy
      store_backend.get([namespace: "health_check"], "probe_key")
      :ok
    rescue
      _ -> :error
    catch
      :exit, _ -> :error
    end
  end

  defp check_memory do
    case :memsup.get_system_memory_data() do
      [] -> :ok
      data ->
        total = Keyword.get(data, :total_memory, 1)
        free = Keyword.get(data, :free_memory, total)
        if (total - free) / total * 100 < 90, do: :ok, else: :error
    end
  end

  defp all_ok?(checks) do
    Enum.all?(checks, fn {_, v} -> v == :ok end)
  end

  defp status_label(200), do: "ok"
  defp status_label(_), do: "degraded"
end

Add a richer auth status endpoint:

# lib/my_app_web/controllers/auth_status_controller.ex
defmodule MyAppWeb.AuthStatusController do
  use MyAppWeb, :controller

  def index(conn, _params) do
    status = %{
      healthy: true,
      database: check_user_table(),
      service: "pow_auth"
    }

    json(conn, status)
  end

  defp check_user_table do
    try do
      # Verify the users table is accessible
      MyApp.Repo.aggregate(MyApp.Users.User, :count, :id)
      "ok"
    rescue
      _ -> "error"
    end
  end
end

Wire up the plug and route:

# lib/my_app_web/endpoint.ex
plug MyAppWeb.Plugs.HealthCheck

# lib/my_app_web/router.ex
# Place before Pow routes to avoid authentication pipeline
get "/auth/status", AuthStatusController, :index

Step 2: Set up HTTP monitoring in Vigilmon

Point Vigilmon at your health endpoint:

  1. Sign up at vigilmon.online
  2. Click New Monitor → HTTP
  3. Enter https://yourdomain.com/health
  4. Set check interval: 1 minute (paid) or 5 minutes (free)
  5. Save

For auth-specific monitoring:

  1. Click New Monitor → HTTP
  2. Enter https://yourdomain.com/auth/status
  3. Under Keyword Check, add "healthy":true
  4. Vigilmon alerts when auth status becomes unhealthy

Also add an HTTP monitor for your login page itself — a 200 on the login page confirms the full authentication routing is working:

  1. Click New Monitor → HTTP
  2. Enter https://yourdomain.com/session/new
  3. Under Keyword Check, add the text of a field on your login form (e.g. Sign in or the form action path)

Step 3: Heartbeat monitoring for PowEmailConfirmation jobs

If you use PowEmailConfirmation, unconfirmed users that never confirm their email accumulate in your database. Add a cleanup job with heartbeat monitoring:

# lib/my_app/workers/unconfirmed_user_cleanup_worker.ex
defmodule MyApp.Workers.UnconfirmedUserCleanupWorker do
  use GenServer

  require Logger

  @cleanup_interval :timer.hours(24)
  @confirmation_grace_period_days 7

  def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)

  @impl true
  def init(state) do
    schedule_cleanup()
    {:ok, state}
  end

  @impl true
  def handle_info(:cleanup, state) do
    case clean_unconfirmed_users() do
      {:ok, count} ->
        Logger.info("Removed #{count} unconfirmed users past grace period")
        ping_heartbeat()
      {:error, reason} ->
        Logger.error("Unconfirmed user cleanup failed: #{inspect(reason)}")
    end

    schedule_cleanup()
    {:noreply, state}
  end

  defp schedule_cleanup, do: Process.send_after(self(), :cleanup, @cleanup_interval)

  defp clean_unconfirmed_users do
    cutoff = DateTime.add(DateTime.utc_now(), -@confirmation_grace_period_days * 86_400, :second)

    {count, _} =
      MyApp.Repo.delete_all(
        from u in MyApp.Users.User,
          where: is_nil(u.email_confirmed_at) and u.inserted_at < ^cutoff
      )

    {:ok, count}
  rescue
    e -> {:error, Exception.message(e)}
  end

  defp ping_heartbeat do
    url = Application.get_env(:my_app, :vigilmon)[:user_cleanup_heartbeat_url]
    if url do
      case Req.get(url, receive_timeout: 5_000) do
        {:ok, _} -> :ok
        {:error, reason} -> Logger.warning("Heartbeat ping failed: #{inspect(reason)}")
      end
    end
  end
end

Add a heartbeat monitor for your password reset token cleanup too:

# lib/my_app/workers/reset_token_cleanup_worker.ex
defmodule MyApp.Workers.ResetTokenCleanupWorker do
  use GenServer

  require Logger

  @cleanup_interval :timer.hours(6)

  def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)

  @impl true
  def init(state) do
    schedule_cleanup()
    {:ok, state}
  end

  @impl true
  def handle_info(:cleanup, state) do
    case clean_expired_reset_tokens() do
      {:ok, count} ->
        Logger.info("Cleaned #{count} expired password reset tokens")
        ping_heartbeat()
      {:error, reason} ->
        Logger.error("Reset token cleanup failed: #{inspect(reason)}")
    end

    schedule_cleanup()
    {:noreply, state}
  end

  defp schedule_cleanup, do: Process.send_after(self(), :cleanup, @cleanup_interval)

  defp clean_expired_reset_tokens do
    now = DateTime.utc_now()

    # Pow stores reset tokens with a validity window; clear expired ones
    {count, _} =
      MyApp.Repo.delete_all(
        from u in MyApp.Users.User,
          where: not is_nil(u.reset_password_sent_at) and
                 u.reset_password_sent_at < datetime_add(^now, -24, "hour")
      )

    {:ok, count}
  rescue
    e -> {:error, Exception.message(e)}
  end

  defp ping_heartbeat do
    url = Application.get_env(:my_app, :vigilmon)[:reset_token_cleanup_heartbeat_url]
    if url, do: Req.get(url, receive_timeout: 5_000)
  end
end

Configure heartbeat URLs:

# config/runtime.exs
config :my_app, :vigilmon,
  user_cleanup_heartbeat_url: System.get_env("VIGILMON_USER_CLEANUP_HEARTBEAT_URL"),
  reset_token_cleanup_heartbeat_url: System.get_env("VIGILMON_RESET_TOKEN_CLEANUP_HEARTBEAT_URL")

In Vigilmon, create heartbeat monitors for each:

  1. Click New Monitor → Heartbeat
  2. For user cleanup: set interval to 25 hours
  3. For reset token cleanup: set interval to 7 hours
  4. Copy ping URLs and set as environment variables

Step 4: Monitor the login page as a smoke test

Add a comprehensive smoke test monitor for your login form to verify Pow routing end-to-end:

# lib/my_app_web/controllers/smoke_test_controller.ex
defmodule MyAppWeb.SmokeTestController do
  use MyAppWeb, :controller

  def index(conn, _params) do
    # Verify the router and Pow pipeline are working
    json(conn, %{
      healthy: true,
      pow_routes: true,
      service: "authentication"
    })
  end
end

# lib/my_app_web/router.ex
# Outside of authenticated pipeline
scope "/", MyAppWeb do
  pipe_through :browser
  get "/smoke", SmokeTestController, :index
end

Add a Vigilmon HTTP monitor for /smoke with keyword "pow_routes":true.


Step 5: Alerts via Slack

In Vigilmon, go to Notifications → New Channel → Slack and paste your Slack webhook URL.

Enable Slack alerts on all Pow-related monitors. Authentication downtime should page you immediately:

🔴 DOWN: yourdomain.com/health
Status: 503 — session_store check failed
Detected from: EU-West, US-East
1 minute ago

🔴 DOWN: yourdomain.com/auth/status
Keyword check failed: "healthy":true not found

Step 6: Status page

  1. Go to Status Pages → New Status Page in Vigilmon
  2. Add monitors for health, auth status, login page, and cleanup heartbeats
  3. Label auth clearly: users checking the status page should see "Authentication" as a named service
  4. Share the URL in your app's error pages so users know where to check

What you've built

| What | How | |------|-----| | Database + session store health | Health plug with check_database and check_session_store | | HTTP uptime monitoring | Vigilmon HTTP monitor → /health | | Auth service keyword alert | Vigilmon keyword check on /auth/status | | Login page smoke test | Keyword monitor on login page content | | Unconfirmed user cleanup heartbeat | Heartbeat ping after 24h cleanup run | | Reset token cleanup heartbeat | Heartbeat ping after 6h cleanup run | | Slack downtime alerts | Vigilmon Slack notification channel | | Status page | Vigilmon public status page labeled "Authentication" |

Pow handles your users. Vigilmon ensures the authentication infrastructure stays reachable when they need it.


Next steps

  • Add user count metrics to your health response to detect unexpected drops that might indicate a migration failure
  • Use Vigilmon's response time history to correlate login page latency spikes with database connection pool saturation
  • Add a heartbeat monitor for your PowPersistentSession cleanup if you use persistent sessions
  • Monitor email delivery separately if you use PowEmailConfirmation — a broken SMTP connection silently blocks all new registrations

Get started free at vigilmon.online.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →