tutorial

Monitoring Your Papercups Customer Support Platform with Vigilmon

Add uptime monitoring, health checks, and alerts to your self-hosted Papercups live chat and customer support platform — covering API server, Phoenix web, PostgreSQL, WebSocket chat, and background workers.

Monitoring Your Papercups Customer Support Platform with Vigilmon

Papercups is an open-source alternative to Intercom and Drift: a self-hosted Elixir/Phoenix live chat platform that keeps your customer conversation data under your own control.

That self-hosted ownership comes with a trade-off — you're responsible for keeping it running. When Papercups goes down, your support team goes dark and customers see a broken chat widget. Unlike SaaS tools that absorb outages silently, you hear about it from your customers first.

This tutorial adds external monitoring to your Papercups deployment:

  • Health check endpoints for the API server and React dashboard
  • HTTP uptime monitoring with Vigilmon
  • Heartbeat monitoring for background email digest and reply automation workers
  • WebSocket and Slack integration health checks
  • Slack alerts and a public status page

Architecture overview

A typical Papercups deployment has several components to monitor:

| Component | Default port | What breaks when it's down | |-----------|-------------|---------------------------| | Phoenix API server | 4000 | All API calls, widget backend | | React dashboard | 3000 | Agent support interface | | PostgreSQL | 5432 | All data, conversation history | | WebSocket endpoint | 4000 | Real-time chat for customers | | Slack integration webhook | external | Slack reply routing | | Email digest worker | internal | Scheduled email summaries | | Reply automation worker | internal | Auto-reply rules | | Chat widget CDN | external | Widget load for visitors |


Step 1: Add a health check endpoint

Papercups exposes a basic /ping route. Extend it with a deeper health check by adding a custom controller:

# lib/papercups_web/controllers/health_controller.ex
defmodule PapercupsWeb.HealthController do
  use PapercupsWeb, :controller

  def index(conn, _params) do
    checks = %{
      database: check_database(),
      api: :ok
    }

    status = if Enum.all?(checks, fn {_, v} -> v == :ok end), do: 200, else: 503

    conn
    |> put_status(status)
    |> json(%{status: status_label(status), checks: checks})
  end

  defp check_database do
    case Ecto.Adapters.SQL.query(Papercups.Repo, "SELECT 1", []) do
      {:ok, _} -> :ok
      {:error, _} -> :error
    end
  end

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

Add the route:

# lib/papercups_web/router.ex
scope "/api", PapercupsWeb do
  pipe_through :api

  get "/health", HealthController, :index
  # ... existing routes
end

Test it:

curl http://localhost:4000/api/health
# {"status":"ok","checks":{"database":"ok","api":"ok"}}

A 503 response with "database":"error" tells you exactly where the failure is — which beats a generic "something is wrong" at 3 AM.


Step 2: Set up HTTP monitoring in Vigilmon

Point Vigilmon at both the API and dashboard:

API server monitor:

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

React dashboard monitor:

  1. Click New Monitor → HTTP
  2. Enter https://yourdomain.com (port 3000 or via reverse proxy)
  3. Add a keyword check for content that should appear (e.g., Papercups or your company name)
  4. Save

Vigilmon checks from multiple geographic regions. If your Papercups is behind a reverse proxy (nginx, Caddy) these checks also catch proxy configuration failures — a common cause of "API is up but the dashboard is broken" incidents.


Step 3: Monitor WebSocket chat connections

Papercups relies on Phoenix Channels for real-time chat. The WebSocket endpoint is at /socket/websocket. Add a dedicated monitor:

  1. Click New Monitor → HTTP
  2. Enter https://yourdomain.com/socket/websocket
  3. Set expected status: 400 (Phoenix returns 400 to plain HTTP requests at the socket endpoint — this is healthy)
  4. Save

A non-400 response (502, 503, connection refused) indicates the Phoenix server itself is down or the WebSocket upgrade path is broken. This matters because customers on your site see a broken chat widget instantly, but your main /health check might still return 200 if the route is registered separately.


Step 4: Alerts via Slack

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

To create a webhook in Slack:

  1. api.slack.com/appsCreate New App → From scratch
  2. Enable Incoming WebhooksAdd New Webhook
  3. Pick your #alerts or #support-ops channel and copy the URL

Enable the Slack channel on both your monitors. When Papercups goes down, you'll get:

🔴 DOWN: yourdomain.com/api/health
Status: 503 Service Unavailable
Detected from: EU-West, US-East
3 minutes ago

And when it recovers:

✅ RECOVERED: yourdomain.com/api/health
Downtime: 8 minutes

This is particularly useful for the support team channel — they get notified of outages at the same time your on-call engineer does.


Step 5: Heartbeat monitoring for background workers

Papercups runs several background processes that can fail silently — the supervisor stays happy but jobs stop running:

Email digest worker: sends scheduled email summaries to customers Reply automation worker: processes auto-reply rules Conversation queue processor: handles incoming message routing

Add heartbeat pings to each worker:

# lib/papercups/workers/email_digest_worker.ex
defmodule Papercups.Workers.EmailDigestWorker do
  use Oban.Worker, queue: :mailers

  require Logger

  @impl Oban.Worker
  def perform(%Oban.Job{}) do
    with :ok <- generate_digests(),
         :ok <- send_digests() do
      ping_heartbeat(:email_digest)
      :ok
    else
      {:error, reason} ->
        Logger.error("EmailDigestWorker failed: #{inspect(reason)}")
        {:error, reason}
    end
  end

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

  defp generate_digests, do: :ok  # existing logic
  defp send_digests, do: :ok       # existing logic
end

Configure heartbeat URLs:

# config/runtime.exs
config :papercups, :vigilmon_heartbeats,
  email_digest: System.get_env("VIGILMON_HEARTBEAT_EMAIL_DIGEST"),
  reply_automation: System.get_env("VIGILMON_HEARTBEAT_REPLY_AUTOMATION"),
  conversation_queue: System.get_env("VIGILMON_HEARTBEAT_CONVERSATION_QUEUE")

In Vigilmon, create a Heartbeat Monitor for each worker:

  1. Click New Monitor → Heartbeat
  2. Set expected interval matching the job schedule (e.g. 24 hours for daily digest, 5 minutes for queue processor)
  3. Copy the ping URL → set as environment variable
  4. Enable Slack alerts on each heartbeat monitor

If the digest worker crashes and Oban exhausts retries, the missing heartbeat triggers an alert within one interval window — long before users notice tomorrow's digests never arrived.


Step 6: Monitor Slack integration health

Papercups can route customer conversations to Slack. This depends on an outbound webhook to Slack's API. Add a connectivity check:

# lib/papercups/integrations/slack_health.ex
defmodule Papercups.Integrations.SlackHealth do
  require Logger

  @slack_api "https://slack.com/api/api.test"

  def check do
    token = Application.get_env(:papercups, :slack_token)
    if token do
      case Req.get(@slack_api,
        headers: [{"Authorization", "Bearer #{token}"}],
        receive_timeout: 10_000
      ) do
        {:ok, %{status: 200, body: %{"ok" => true}}} -> :ok
        {:ok, %{body: %{"error" => error}}} ->
          Logger.error("Slack API check failed: #{error}")
          :error
        {:error, reason} ->
          Logger.error("Slack API unreachable: #{inspect(reason)}")
          :error
      end
    else
      :ok  # no Slack integration configured
    end
  end
end

Include this in your health check:

defp run_checks do
  %{
    database: check_database(),
    slack: Papercups.Integrations.SlackHealth.check(),
    api: :ok
  }
end

When Slack's API has an incident or your token expires, the health check returns 503 before your agents realize their Slack replies aren't routing.


Step 7: Status page and badge

Status page:

  1. Go to Status Pages → New Status Page in Vigilmon
  2. Add all your monitors: API health, dashboard, WebSocket, each heartbeat
  3. Copy the public URL (e.g. https://status.yourdomain.com)

Share the status page URL in your support team's #general channel topic. When agents report "something seems off," checking the status page is faster than asking ops.

README badge:

![Uptime](https://vigilmon.online/badge/your-monitor-slug)

What you've built

| What | How | |------|-----| | API health check | Custom Phoenix controller with DB connectivity | | Dashboard uptime | HTTP monitor with keyword check | | WebSocket health | HTTP monitor on /socket/websocket (expects 400) | | Slack alerts | Vigilmon Slack notification channel | | Email digest monitoring | Oban heartbeat ping on job success | | Reply automation monitoring | Oban heartbeat ping on job success | | Slack integration health | Outbound API test in health check | | Status page | Vigilmon public status page |

Your BEAM VM keeps processes alive. Vigilmon keeps external eyes on what your customers actually experience.


Next steps

  • Add response time history in Vigilmon to track PostgreSQL query latency trends
  • Monitor your chat widget CDN endpoint separately — a CDN outage breaks the widget even if your server is healthy
  • Set up multi-region monitoring to catch routing failures that affect some users but not others
  • Add a heartbeat for each active Oban queue, not just the email workers

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 →