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:
- Sign up at vigilmon.online
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/api/health - Set check interval: 1 minute (paid) or 5 minutes (free)
- Save
React dashboard monitor:
- Click New Monitor → HTTP
- Enter
https://yourdomain.com(port 3000 or via reverse proxy) - Add a keyword check for content that should appear (e.g.,
Papercupsor your company name) - 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:
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/socket/websocket - Set expected status: 400 (Phoenix returns 400 to plain HTTP requests at the socket endpoint — this is healthy)
- 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:
- api.slack.com/apps → Create New App → From scratch
- Enable Incoming Webhooks → Add New Webhook
- Pick your
#alertsor#support-opschannel 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:
- Click New Monitor → Heartbeat
- Set expected interval matching the job schedule (e.g. 24 hours for daily digest, 5 minutes for queue processor)
- Copy the ping URL → set as environment variable
- 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:
- Go to Status Pages → New Status Page in Vigilmon
- Add all your monitors: API health, dashboard, WebSocket, each heartbeat
- 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:

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.