RamalLive

A live inspector for Ramal branches: see every alternative state of your data side by side, and merge, discard or approve them with a click.

Hex.pm HexDocs

The inspector with four open branches, each moving herd R7 to a different paddock, shown as columns of before and after rows

Ramal gives an Ash application Git-style branches of its data: a branch is a process that holds a few changes on top of your database, and nothing reaches the database until the branch is merged. That is easy to use from code and hard to see. RamalLive is the page that shows it: one column per branch, with what that branch would change.

What you see

It shows the open and proposed branches of one Ash domain as columns:

Who it is for

There is no JavaScript, no Tailwind and no asset pipeline. The page is a LiveView with inline CSS (slate for the base line, copper for branches, light and dark themes), mounted by your router inside your own live_session, so it uses your LiveView socket and your JavaScript. The decision is in ADR 0015.

Setup

Requires Phoenix 1.7+, Phoenix LiveView 1.1+ and a running Phoenix.PubSub.

# mix.exs
{:ramal, "~> 0.9.0"},
{:ramal_live, "~> 0.9.0"}

# application.ex, after the PubSub and Ramal.Supervisor, in every node that runs branches
{RamalLive.Broadcaster, pubsub: MyApp.PubSub}

# router.ex, behind your own authorization
import RamalLive.Router

scope "/admin" do
  pipe_through [:browser, :require_admin]

  live_session :admin, on_mount: [{MyAppWeb.UserAuth, :ensure_admin}] do
    ramal_inspector "/ramal", domain: MyApp.Farm, pubsub: MyApp.PubSub, actor: {MyAppWeb.Inspector, :actor}
  end
end

Options of ramal_inspector/2: :domain, :pubsub and :actor (all required), and :owner and :debounce (optional). :actor is {module, function}; the page calls module.function(socket) and uses the result as the actor that is recorded as decided_by and replays a merge, or refuses to act when it returns nil. A router without it does not compile. For a development tool with no users, return a constant (def actor(_socket), do: :dev).

:owner is also {module, function} and off by default. When given, the page calls module.function(actor) with what :actor returned and shows only the branches created with that owner (Ramal.branch(domain, owner: ...)), so each signed-in user sees their own and forks they make belong to them. When the actor or the owner is nil it shows nothing. It is a view, not access control (see below). :debounce is how many milliseconds the page waits to coalesce a burst of writes before it reads the branch again (default 50).

defmodule MyAppWeb.Inspector do
  def actor(socket), do: socket.assigns.current_user
  def owner(user), do: user && user.id
end

ramal_inspector "/ramal", domain: MyApp.Farm, pubsub: MyApp.PubSub,
  actor: {MyAppWeb.Inspector, :actor}, owner: {MyAppWeb.Inspector, :owner}

When to use it

Things to know

Try it

You do not need an app of your own to see the inspector. A runnable demo, the one the screenshot above and the animation below come from, lives in examples/farm_demo.exs. It needs Elixir 1.18+, Docker (for Postgres) and a clone of the repository. It is a single file that installs its own dependencies with Mix.install (so the first start compiles for a few minutes), runs a Phoenix Playground app on a seeded farm in Postgres, and mounts the inspector with the ramal_inspector line above.

docker compose up -d db                 # from the repository root: Postgres on port 5435
elixir ramal_live/examples/farm_demo.exs

Open http://localhost:4000 and press Explore 4 alternatives. Here is what happens. Four branches open one after the other, each moves herd R7 out of paddock L07 into a different paddock, and the columns fill in as the writes happen. Merge one: the database now has R7 in its new paddock, and merging any other column is refused as stale, because that branch read R7 where it used to be. Reset discards every branch and seeds the farm again.

The columns filling in as the four branches write

The demo uses a ramal_dev database on the server of the test database, creates it if it is missing, and drops and recreates its three tables on every start, so do not point it at data you want to keep. RAMAL_PG_HOST, RAMAL_PG_PORT, RAMAL_PG_USER and RAMAL_PG_PASSWORD change the server and PORT changes the HTTP port. Branches are kept in ETS, so they do not survive a restart. The page is the demo's own LiveView with the inspector in an iframe, so one URL shows both the button and the columns.

Development

cd ramal_live
mix deps.get
mix ci          # format, compile with warnings as errors, credo strict, tests

The tests run on a test endpoint with Phoenix.LiveViewTest and an ETS-backed domain, so no database is needed.