Skip to content

Commit 0afe007

Browse files
alcoclaude
andauthored
Add Phoenix LiveDashboard on a separate port (#4119)
## Summary - Adds Phoenix LiveDashboard served on a dedicated Bandit instance, configured via `ELECTRIC_LIVE_DASHBOARD_PORT` - Dashboard is opt-in: not started unless the env var is set - Fully isolated from the main Plug-based HTTP API > **⚠️ Security note:** The dashboard endpoint is **completely unauthenticated** and exposes internal system state (VM metrics, process info, ETS tables, etc.). In production, operators **must** ensure the dashboard port is firewalled or otherwise restricted to trusted networks only. ## Test plan - [x] Set `ELECTRIC_LIVE_DASHBOARD_PORT=4000` and verify dashboard loads at `http://localhost:4000` - [x] Verify main API on `ELECTRIC_PORT` is unaffected - [x] Verify dashboard does not start when env var is unset 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 902970b commit 0afe007

12 files changed

Lines changed: 167 additions & 6 deletions

File tree

.changeset/nice-taxis-shake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@core/sync-service': patch
3+
---
4+
5+
Add optional PhoenixLiveDashboard with configurable port to listen on.

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Real-time sync for Postgres.
3232
- [What is Electric?](#what-is-electric)
3333
- [Getting Started](#getting-started)
3434
- [HTTP API Docs](#http-api-docs)
35+
- [Phoenix LiveDashboard](#phoenix-livedashboard)
3536
- [Developing Electric](#developing-electric)
3637
- [Mac setup](#mac-setup)
3738
- [Contributing](#contributing)
@@ -98,6 +99,20 @@ Again, see the [Quickstart](https://electric-sql.com/docs/quickstart) and the [D
9899

99100
The HTTP API is defined in an [OpenAPI spec](https://swagger.io/specification/) in [website/electric-api.yaml](./website/electric-api.yaml).
100101

102+
## Phoenix LiveDashboard
103+
104+
Electric includes an optional [Phoenix LiveDashboard](https://github.com/phoenixframework/phoenix_live_dashboard) for real-time monitoring of the running system (VM metrics, process info, ETS tables, etc.).
105+
106+
To enable it, set the `ELECTRIC_LIVE_DASHBOARD_PORT` environment variable:
107+
108+
```sh
109+
ELECTRIC_LIVE_DASHBOARD_PORT=4000
110+
```
111+
112+
The dashboard will be available at `http://localhost:4000` (or whichever port you choose). When the variable is not set, the dashboard is not started.
113+
114+
> **WARNING: The LiveDashboard endpoint is completely unauthenticated.** Anyone with network access to the port can view internal system state. In production, you **must** restrict access to this port using firewall rules, network policies, or similar controls. Do not expose it to the public internet.
115+
101116
## Developing Electric
102117

103118
We use [asdf](https://asdf-vm.com/) to install Elixir, Erlang, and Node.js. Versions are defined in [.tool-versions](.tool-versions).
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Used by "mix format"
22
[
33
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
4-
import_deps: [:plug, :stream_data]
4+
import_deps: [:plug, :stream_data, :phoenix]
55
]

packages/sync-service/config/runtime.exs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ replication_stream_id =
150150
)
151151

152152
prometheus_port = env!("ELECTRIC_PROMETHEUS_PORT", :integer, nil)
153+
live_dashboard_port = env!("ELECTRIC_LIVE_DASHBOARD_PORT", :integer, nil)
153154

154155
call_home_telemetry_url =
155156
env!(
@@ -248,6 +249,7 @@ config :electric,
248249
env!("ELECTRIC_TELEMETRY_LONG_MESSAGE_QUEUE_DISABLE_THRESHOLD", :integer, nil),
249250
telemetry_statsd_host: statsd_host,
250251
prometheus_port: prometheus_port,
252+
live_dashboard_port: live_dashboard_port,
251253
db_pool_size: env!("ELECTRIC_DB_POOL_SIZE", :integer, nil),
252254
replication_stream_id: replication_stream_id,
253255
replication_slot_temporary?: env!("CLEANUP_REPLICATION_SLOTS_ON_SHUTDOWN", :boolean, nil),
@@ -395,3 +397,31 @@ if Electric.telemetry_enabled?() do
395397
config :opentelemetry, processors: []
396398
end
397399
end
400+
401+
# Phoenix LiveDashboard Endpoint Configuration
402+
#
403+
# WARNING: The dashboard is completely unauthenticated and exposes internal
404+
# system state (VM metrics, process info, ETS tables, etc.). In production,
405+
# ensure the dashboard port is firewalled or otherwise restricted to trusted
406+
# networks only.
407+
if live_dashboard_port do
408+
dashboard_ip =
409+
if env!("ELECTRIC_LISTEN_ON_IPV6", :boolean, false),
410+
do: {0, 0, 0, 0, 0, 0, 0, 0},
411+
else: {0, 0, 0, 0}
412+
413+
config :electric, Electric.LiveDashboard.Endpoint,
414+
adapter: Bandit.PhoenixAdapter,
415+
http: [
416+
port: live_dashboard_port,
417+
ip: dashboard_ip
418+
],
419+
server: true,
420+
render_errors: [
421+
formats: [html: Electric.LiveDashboard.ErrorView],
422+
layout: false
423+
],
424+
live_view: [signing_salt: "r5zw+GcXjt3wP3Z/snFRqQ5uH2cm8Vb7ldc8t0POZdo="],
425+
secret_key_base: Base.encode64(:crypto.strong_rand_bytes(48)),
426+
pubsub_server: Electric.PubSub
427+
end

packages/sync-service/lib/electric/application.ex

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ defmodule Electric.Application do
5353
application_telemetry(config),
5454
[{Electric.StackSupervisor, Keyword.put(config, :name, Electric.StackSupervisor)}],
5555
api_server_children(config),
56-
prometheus_endpoint(Electric.Config.get_env(:prometheus_port))
56+
prometheus_endpoint(Electric.Config.get_env(:prometheus_port)),
57+
live_dashboard_endpoint(Electric.Config.get_env(:live_dashboard_port))
5758
])
5859
end
5960

@@ -270,6 +271,15 @@ defmodule Electric.Application do
270271
]
271272
end
272273

274+
defp live_dashboard_endpoint(nil), do: []
275+
276+
defp live_dashboard_endpoint(_port) do
277+
[
278+
{Phoenix.PubSub, name: Electric.PubSub},
279+
Electric.LiveDashboard.Endpoint
280+
]
281+
end
282+
273283
@doc false
274284
# REQUIRED (but undocumented) public API for Phoenix.Sync
275285
def api_server do

packages/sync-service/lib/electric/config.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ defmodule Electric.Config do
121121
shape_db_synchronous:
122122
Electric.ShapeCache.ShapeStatus.ShapeDb.Connection.default!(:synchronous),
123123
shape_db_cache_size: Electric.ShapeCache.ShapeStatus.ShapeDb.Connection.default!(:cache_size),
124-
exclude_spans: MapSet.new()
124+
exclude_spans: MapSet.new(),
125+
live_dashboard_port: nil
125126
]
126127

127128
@installation_id_key "electric_installation_id"
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
defmodule Electric.LiveDashboard.Endpoint do
2+
@moduledoc """
3+
Phoenix Endpoint for serving LiveDashboard on a separate port.
4+
This runs independently from the main Plug-based HTTP server.
5+
6+
**WARNING: This endpoint is completely unauthenticated.** It exposes internal
7+
system state (VM metrics, process info, etc.). In production, ensure the
8+
dashboard port is not publicly accessible — use firewall rules or network
9+
policies to restrict access.
10+
"""
11+
12+
use Phoenix.Endpoint, otp_app: :electric
13+
14+
# LiveView socket for dashboard interactions
15+
socket "/live", Phoenix.LiveView.Socket,
16+
websocket: true,
17+
longpoll: true
18+
19+
# Serve static assets for LiveDashboard
20+
plug Plug.Static,
21+
at: "/",
22+
from: :phoenix_live_dashboard,
23+
gzip: false,
24+
only: ~w(assets fonts images priv)
25+
26+
# Session configuration for LiveView
27+
plug Plug.Session,
28+
store: :cookie,
29+
key: "_live_dashboard_key",
30+
signing_salt: "abc43s8Z",
31+
same_site: "Lax"
32+
33+
# Parse request body for LiveView
34+
plug Plug.Parsers,
35+
parsers: [:urlencoded, :multipart, :json],
36+
pass: ["*/*"],
37+
json_decoder: Jason
38+
39+
plug Plug.MethodOverride
40+
plug Plug.Head
41+
42+
# Route all requests to the LiveDashboard router
43+
plug Electric.LiveDashboard.Router
44+
end
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
defmodule Electric.LiveDashboard.ErrorView do
2+
@moduledoc """
3+
Error view for the LiveDashboard endpoint.
4+
Handles rendering of HTTP errors.
5+
"""
6+
7+
def render(template, _assigns) do
8+
Phoenix.Controller.status_message_from_template(template)
9+
end
10+
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
defmodule Electric.LiveDashboard.FaviconController do
2+
@moduledoc """
3+
Simple controller to handle favicon requests.
4+
Returns a 204 No Content response to avoid 500 errors.
5+
"""
6+
7+
use Phoenix.Controller, formats: [:html]
8+
9+
def show(conn, _params) do
10+
send_resp(conn, 204, "")
11+
end
12+
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
defmodule Electric.LiveDashboard.Router do
2+
@moduledoc """
3+
Phoenix Router for LiveDashboard.
4+
Mounts the dashboard at the root path.
5+
"""
6+
7+
use Phoenix.Router
8+
import Phoenix.LiveDashboard.Router
9+
10+
pipeline :browser do
11+
plug :accepts, ["html", "json"]
12+
plug :fetch_session
13+
plug :put_root_layout, html: {Phoenix.LiveDashboard.LayoutView, :root}
14+
plug :protect_from_forgery
15+
plug :put_secure_browser_headers
16+
end
17+
18+
scope "/" do
19+
pipe_through :browser
20+
21+
# Handle favicon requests gracefully
22+
get "/favicon.ico", Electric.LiveDashboard.FaviconController, :show
23+
24+
live_dashboard "/", ecto_repos: []
25+
end
26+
end

0 commit comments

Comments
 (0)