Deploying the LiveView IDE for non-localhost (remote) access

June 12, 2026 · View on GitHub

This guide covers running the Beamtalk LiveView IDE past localhost — the production auth/authz topology decided in ADR 0091. For the security model and trust path, see docs/security/threat-model.md.

Local development is unchanged. beamtalk repl --web and just web <ws> on localhost stay zero-config (cookie handshake, no OIDC). Everything here is opt-in and applies only when you expose the IDE beyond the local machine.

Installing the IDE

The IDE ships on its own release lane (separate from the beamtalk-<version> toolchain bundle — it runs as its own BEAM node and attaches over distribution). Three ways to get it, from most turnkey to most hands-on:

  1. Docker image (recommended for remote/operator deployments) — ghcr.io/jamesc/beamtalk-ide:<version> bundles Elixir + OTP + the built assets; the host needs only Docker. See Run with Docker.

  2. Release archivebeamtalk-ide-<version>-<platform>.tar.gz from the GitHub Release is a self-contained, ERTS-embedded build that runs with no Elixir/Mix on the host:

    tar -xzf beamtalk-ide-<version>-<platform>.tar.gz
    cd beamtalk-ide-<version>
    
    # Workspace on this host: resolve its node + cookie by id, like `just web`
    # (generates an ephemeral SECRET_KEY_BASE if unset — fine for a local trial)
    bin/server <workspace-id>
    
    # Remote workspace / explicit config: provision everything via env
    PHX_SERVER=true SECRET_KEY_BASE="$(openssl rand -base64 48)" \
    PHX_HOST=ide.example.com \
    BT_WORKSPACE_NODE=... BT_WORKSPACE_COOKIE=... \
      bin/bt_attach start          # OIDC env per step 1 below
    

    Available for linux-x86_64, macos-arm64, macos-x86_64. (Windows is a browser-only client — there is no IDE-host build for it.)

  3. From source (contributors) — just web <ws> / just web-remote <ws> from a checkout; see editors/liveview/README.md.

All three run the same release and read the same configuration (below). The rest of this guide configures the IDE for non-localhost access; it applies identically however you installed it.

The shape

   UNTRUSTED NETWORK              TRUSTED HOST / PRIVATE NETWORK
┌─────────┐  HTTPS + OIDC    ┌──────────────┐  internal dist    ┌─────────────┐
│ Browser │ ───────────────▶ │   Phoenix    │ ────────────────▶ │  Workspace  │
│ (user)  │  session cookie  │  LiveView    │  (loopback /      │  BEAM node  │
└─────────┘  (HttpOnly,      │  front       │   private iface,  │  (eval, RCE)│
             Secure, Strict)  └──────────────┘   never public)   └─────────────┘
   The ONLY untrusted hop is browser → Phoenix.   Cookie is an infra secret;
   Phoenix is the only node on the dist mesh.     it never reaches a browser.

The browser's trust boundary terminates at Phoenix (HTTPS + OIDC). Phoenix — and only Phoenix — speaks Erlang distribution to the workspace, over a link kept off untrusted networks by deployment (no overlay or TLS-dist dependency is imposed).

1. Authentication (OIDC)

Configure OIDC once at boot via ~/.beamtalk/ide.toml (every key has a BT_OIDC_* env override; the client secret comes from env or a chmod 600 file, never inline):

# ~/.beamtalk/ide.toml
[oidc]
issuer            = "https://idp.example.com"     # env: BT_OIDC_ISSUER
client_id         = "beamtalk-ide"                # env: BT_OIDC_CLIENT_ID
redirect_uri      = "https://ide.example.com/oidc/callback"
groups_claim      = "groups"
client_secret_env = "BT_OIDC_CLIENT_SECRET"       # or client_secret_file = "/run/secrets/oidc"

[oidc.roles]                                       # IdP group -> role (fail-closed)
owner    = ["beamtalk-owners", "platform-admins"]  # execute + read + admin
observer = ["beamtalk-observers"]                  # read-only, no eval
# unmatched group -> DENY

Boot fails closed if OIDC is requested but a required key — or the role map — is missing (it will not start with broken auth). Roles may also be supplied by env (BT_OIDC_ROLES_OWNER, BT_OIDC_ROLES_OBSERVER, space/comma-separated) for a 12-factor deployment with no file.

Owner = full RCE (equivalent to the ADR 0058 authenticated user). Only Observer is non-executing. Hand out Observer freely; hand out Owner as you would shell access.

2. Transport posture — keep distribution internal

Run Phoenix and the workspace on the same host. Distribution stays on loopback and never leaves the machine; the only network-facing service is Phoenix's HTTPS port.

The workspace launcher pins ERL_EPMD_ADDRESS=127.0.0.1 for an epmd it starts, and warns at launch if it detects a pre-existing epmd already bound to a non-loopback interface (epmd is a shared per-user daemon — loopback is not automatic). If you see that warning, stop the stray epmd (epmd -kill when no other Erlang nodes need it) and restart the workspace.

Same trusted private network / VPC

Bind distribution and epmd to the private interface, never 0.0.0.0:

export ERL_EPMD_ADDRESS=10.0.0.5        # the private-interface address
beamtalk workspace create ws --background --persistent

This is a standard private-cluster posture — no overlay or TLS-dist required.

Untrusted split (discouraged)

If you must run Phoenix and the workspace across an untrusted network, you own securing that hop. In order of preference: keep them co-located instead; an SSH tunnel; a network overlay (Tailscale/WireGuard); or reintroduce TLS distribution (inet_tls_dist, removed in PR #1401 — not in the tree). The platform does not depend on any of these.

The dist cookie is shared between the two trusted hosts, not a user credential. It is read from ~/.beamtalk/workspaces/{id}/cookie (mode 0600) and provisioned to Phoenix via env — BT_WORKSPACE_NODE and BT_WORKSPACE_COOKIE — through your secret-management path. It is never sent to a browser (browser ↔ Phoenix is HTTPS only; the cookie is used solely for the Phoenix ↔ workspace dist link).

Run it

export SECRET_KEY_BASE="$(cd editors/liveview && mix phx.gen.secret)"
export PHX_HOST=ide.example.com          # behind your TLS terminator
just web-remote ws                       # discovers node+cookie, starts prod Phoenix

just web-remote discovers the node + cookie, exports them as the infra secret, and starts Phoenix in prod mode (server on :8443 by default; put a TLS terminator in front, or add https: to the endpoint). OIDC must be configured (step 1) or boot fails closed.

Run with Docker

The published image (ghcr.io/jamesc/beamtalk-ide) is the turnkey option — it bundles Elixir + OTP + the built assets, so the host needs only Docker. It runs the same ERTS-embedded release as just dist-liveview, configured by the same env. Pin a version tag (it tracks the toolchain version) rather than latest:

docker run --network host \
  -e SECRET_KEY_BASE="$(openssl rand -base64 48)" \
  -e PHX_HOST=ide.example.com \
  -e BT_WORKSPACE_NODE="beamtalk_workspace_ws@localhost" \
  -e BT_WORKSPACE_COOKIE="$(cat ~/.beamtalk/workspaces/ws/cookie)" \
  -e BT_OIDC_ISSUER=https://idp.example.com \
  -e BT_OIDC_CLIENT_ID=beamtalk-ide \
  -e BT_OIDC_CLIENT_SECRET="$BT_OIDC_CLIENT_SECRET" \
  ghcr.io/jamesc/beamtalk-ide:0.4.0

--network host co-locates the container with the workspace so the Phoenix ↔ workspace Erlang distribution link stays on loopback (ADR 0091 Decision 5); the only network-facing service is Phoenix's HTTP(S) port. Do not publish epmd/dist ports — keep distribution off untrusted networks (co-located or a private interface). The cookie is an infrastructure secret passed via env; it is never sent to a browser. Boot fails closed if OIDC is requested but misconfigured.

Rotate with beamtalk workspace rotate-cookie (future CLI work, ADR 0020), coordinated with a Phoenix reconnect: dist connections are long-lived, so rotation needs a brief reconnect window (re-provision BT_WORKSPACE_COOKIE and restart/reconnect Phoenix), not a live re-key.

Caveat (ADR 0058): rotating the cookie does not evict code already hot-loaded into the workspace. A fully compromised workspace must be stopped and restarted, not merely re-keyed.

Checklist

  • HTTPS terminates at Phoenix; PHX_HOST set; SECRET_KEY_BASE set.
  • OIDC [oidc] + [oidc.roles] configured; secret from env/file, not inline.
  • Workspace + Phoenix co-located (or dist bound to a private iface, never 0.0.0.0).
  • No promiscuous-epmd warning at workspace launch.
  • BT_WORKSPACE_COOKIE delivered via secret management; never in a browser surface.
  • Observer handed out for read-only audiences; Owner treated as shell access.