Surface UI is a server-side rendering component library for Phoenix LiveView that brings a Vue-like component syntax to the Elixir ecosystem. You declare components with typed props, slots, and event handlers — Surface's compiler transforms them into optimized LiveView code at compile time. The result is a structured, component-driven architecture that scales well as your UI grows. But Surface's compile-time magic means a bad component definition or a LiveView process crash can quietly break a section of your UI without affecting HTTP health probes. Vigilmon gives you the external monitoring layer to catch those failures: HTTP uptime checks, component endpoint validation, heartbeat monitors for data-feeding GenServers, and instant alerts to email or Slack.
What You'll Build
- A
/healthendpoint that validates HTTP, database, and PubSub (the backing layer for Surface LiveComponent state) - Vigilmon HTTP monitors for your Surface-rendered pages
- A heartbeat monitor for background workers that feed data to Surface components
- Alert channels (email and Slack)
- An uptime badge embeddable in your Surface layout
Prerequisites
- A Phoenix LiveView + Surface UI application (Surface 0.11+)
- A free Vigilmon account
Step 1: Add a Health Check Plug
Surface components render inside LiveView processes. Your health check should validate the Phoenix endpoint, Ecto, and PubSub — the full dependency chain for a live Surface component.
# 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 = %{
database: check_database(),
pubsub: check_pubsub()
}
degraded = Enum.any?(checks, fn {_, v} -> v != :ok end)
status = if degraded, do: 503, else: 200
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(%{
status: if(degraded, do: "degraded", else: "ok"),
checks: Map.new(checks, fn {k, v} ->
{k, if(v == :ok, do: "ok", else: "error")}
end)
}))
|> halt()
end
def call(conn, _opts), do: conn
defp check_database do
case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
{:ok, _} -> :ok
_ -> :error
end
end
defp check_pubsub do
topic = "health:check:#{System.unique_integer([:positive])}"
Phoenix.PubSub.subscribe(MyApp.PubSub, topic)
Phoenix.PubSub.broadcast(MyApp.PubSub, topic, :ping)
result =
receive do
:ping -> :ok
after
1_000 -> :error
end
Phoenix.PubSub.unsubscribe(MyApp.PubSub, topic)
result
end
end
Register the plug before the router:
# lib/my_app_web/endpoint.ex
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
plug MyAppWeb.Plugs.HealthCheck
plug MyAppWeb.Router
end
Test it:
curl -s http://localhost:4000/health | jq .
# {
# "status": "ok",
# "checks": { "database": "ok", "pubsub": "ok" }
# }
Step 2: Configure Vigilmon HTTP Monitors
Log in to Vigilmon → Monitors → New Monitor.
Monitor 1: Health Endpoint
| Field | Value |
|---|---|
| URL | https://yourapp.com/health |
| Method | GET |
| Expected status | 200 |
| Expected body | "status":"ok" |
| Check interval | 1 minute |
Monitor 2: Surface-Rendered Page
Surface components render their HTML through LiveView over a WebSocket upgrade. An HTTP monitor on your main route confirms that the initial dead render (the HTML Plug sends before WebSocket connects) succeeds:
| Field | Value |
|---|---|
| URL | https://yourapp.com/ |
| Method | GET |
| Expected status | 200 |
| Expected body | A stable string from your layout (e.g. your app name or a component's static outer tag) |
| Check interval | 1 minute |
Two monitors, two failure domains. The root check catches Cowboy/Plug failures; the health check catches the data-layer failures that would cause Surface components to mount in a broken state.
Step 3: Heartbeat Monitor for Component Data Feeds
Surface LiveComponents that display live data (dashboards, feeds, tables) typically subscribe to PubSub topics that a GenServer populates. If that GenServer stops running — but its supervisor doesn't restart it within the expected window — components freeze without surfacing any HTTP error.
# lib/my_app/dashboard_feed.ex
defmodule MyApp.DashboardFeed do
use GenServer
require Logger
@interval :timer.seconds(60)
def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
@impl true
def init(state) do
schedule_broadcast()
{:ok, state}
end
@impl true
def handle_info(:broadcast, state) do
case fetch_metrics() do
{:ok, metrics} ->
Phoenix.PubSub.broadcast(MyApp.PubSub, "dashboard:metrics", {:metrics, metrics})
ping_heartbeat()
{:error, reason} ->
Logger.error("DashboardFeed fetch failed: #{inspect(reason)}")
# No ping — Vigilmon alerts after the window expires
end
schedule_broadcast()
{:noreply, state}
end
defp schedule_broadcast, do: Process.send_after(self(), :broadcast, @interval)
defp fetch_metrics do
# Replace with your actual data fetching
{:ok, %{requests_per_second: 42, error_rate: 0.1}}
end
defp ping_heartbeat do
url = Application.get_env(:my_app, :vigilmon)[:dashboard_heartbeat_url]
if url, do: Req.get(url, receive_timeout: 5_000)
end
end
Register the server in your supervision tree:
# lib/my_app/application.ex
def start(_type, _args) do
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
{Phoenix.PubSub, name: MyApp.PubSub},
MyApp.DashboardFeed # ← add here
]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
Add config:
# config/runtime.exs
config :my_app, :vigilmon,
dashboard_heartbeat_url: System.get_env("VIGILMON_DASHBOARD_HEARTBEAT_URL")
In Vigilmon:
- Monitors → New Heartbeat Monitor
- Expected interval: 3 minutes (3× the 1-minute broadcast cycle)
- Copy the ping URL and set
VIGILMON_DASHBOARD_HEARTBEAT_URLin your environment
Step 4: Embed an Uptime Badge in Your Surface Layout
Add a live uptime indicator to your Surface layout using a lightweight component:
# lib/my_app_web/components/uptime_badge.ex
defmodule MyAppWeb.Components.UptimeBadge do
use Surface.Component
prop badge_url, :string, required: true
prop status_url, :string, required: true
prop label, :string, default: "Uptime"
def render(assigns) do
~F"""
<a href={@status_url} target="_blank" rel="noopener noreferrer" aria-label="Service uptime status">
<img
src={@badge_url}
alt={@label}
width="120"
height="20"
loading="lazy"
/>
</a>
"""
end
end
Use it in your root layout:
# lib/my_app_web/components/layouts/root.html.heex (or .sface)
<footer>
<MyAppWeb.Components.UptimeBadge
badge_url="https://vigilmon.online/api/badge/your-monitor-id.svg"
status_url="https://vigilmon.online/status/your-monitor-slug"
/>
</footer>
Because Surface renders server-side and LiveView persists the layout across navigations, the badge loads once and stays visible throughout the user's session.
Step 5: Alert Channels
In Vigilmon → Alert Channels:
- Alert Channels → Email → add your on-call address
- Attach to all monitors
Slack
- Create a Slack Incoming Webhook for
#alerts - Alert Channels → Webhook → paste the URL
- Payload template:
{
"text": "🔴 *{{ monitor.name }}* is DOWN\n{{ monitor.url }}\nStatus: {{ event.status }}\n<https://vigilmon.online|Open Vigilmon>"
}
Step 6: Verify Your Setup
# 1. Health endpoint returns 200 with all checks green
curl -s https://yourapp.com/health | jq .
# 2. Simulate a PubSub failure (stop the PubSub process)
# Expected: health returns 503 with "pubsub": "error"
# Expected: Vigilmon alerts within 2 minutes (catches frozen Surface components)
# 3. Stop DashboardFeed and wait for the heartbeat window to expire
# Expected: Vigilmon heartbeat alert fires before users notice stale data
# 4. Vigilmon → "Test Alert" → verify email and Slack delivery
Summary
| Monitor | What It Catches | |---|---| | HTTP health endpoint | Database failures, PubSub outages, bad deploys | | Surface-rendered page root | Plug/Cowboy failures, broken dead-render | | GenServer heartbeat | Frozen dashboard feeds, stalled PubSub broadcasts |
Next Steps
- Add a heartbeat for every GenServer that feeds a critical Surface component
- Use Vigilmon's response time graphs to correlate Surface component mount latency with data-fetch times
- Monitor the LiveView WebSocket upgrade path (
/live/websocket) to catch endpoint misconfigurations - Add multi-region monitors if you deploy across multiple Fly.io regions
Ready to get started? Sign up for Vigilmon free — have your first monitor running in under 5 minutes.