Operate Shared Canvases

Copy Markdown View Source

Shared mode is an operational choice. It changes where accepted canvas state lives, how participants find the coordinator, how state is recovered, and what the application must supervise.

It does not change how canvas commands work.

Use this guide after the basic shared-canvas setup is working.

One-Node Backend

For a single BEAM node, use LivePanels.Canvas.Shared.Local:

children = [
  {Phoenix.PubSub, name: MyApp.PubSub},
  LivePanels.Application
]
defmodule MyApp.CanvasLayouts do
  use LivePanels.Canvas.LayoutStore.Ecto,
    repo: MyApp.Repo,
    schema: MyApp.LivePanelsCanvasLayout
end

use LivePanels.Canvas,
  pubsub: MyApp.PubSub,
  session: [
    mode: :shared,
    id: {:param, "room_id"},
    backend:
      {LivePanels.Canvas.Shared.Local,
       layout_store: MyApp.CanvasLayouts}
  ]

The local backend uses local Registry, DynamicSupervisor, participant tracking, and the configured layout store. It is the default backend shape for development and one-node production.

You can also add backend child specs yourself:

children =
  [
    {Phoenix.PubSub, name: MyApp.PubSub},
    LivePanels.Application
  ] ++ LivePanels.Canvas.Shared.child_specs({LivePanels.Canvas.Shared.Local, layout_store: MyApp.CanvasLayouts})

Clustered Backend

For clustered coordinator lookup and supervision, configure a distributed adapter such as LivePanels.Canvas.Shared.Horde:

use LivePanels.Canvas,
  pubsub: MyApp.PubSub,
  session: [
    mode: :shared,
    id: {:param, "room_id"},
    backend:
      {LivePanels.Canvas.Shared.Horde,
       process_registry: MyApp.LivePanelsRegistry,
       dynamic_supervisor: MyApp.LivePanelsSupervisor,
       participant_tracker_supervisor: MyApp.LivePanelsParticipantTrackerSupervisor,
       layout_store: MyApp.CanvasLayouts}
  ]

The application still owns Horde membership, node discovery, deployment topology, database choice, and release checks. LivePanels only needs the backend pieces required to find or start the shared coordinator and participant tracker.

Persistent Recovery

A shared coordinator can recover only what the layout store can load.

For production, use LivePanels.Canvas.LayoutStore.Ecto or another durable LivePanels.Canvas.LayoutStore:

defmodule MyApp.CanvasLayouts do
  use LivePanels.Canvas.LayoutStore.Ecto,
    repo: MyApp.Repo,
    schema: MyApp.LivePanelsCanvasLayout
end

Store the versioned layout payload unchanged. It may contain item topology, vector state, and graph connectors between persistent items.

For PostgreSQL, the schema can store the payload as an array of maps:

defmodule MyApp.LivePanelsCanvasLayout do
  use Ecto.Schema

  schema "livepanels_canvas_layouts" do
    field :canvas_id, :string
    field :layout, {:array, :map}, default: []

    timestamps(type: :utc_datetime_usec)
  end
end
create table(:livepanels_canvas_layouts) do
  add :canvas_id, :string, null: false
  add :layout, {:array, :map}, null: false, default: []

  timestamps(type: :utc_datetime_usec)
end

create unique_index(:livepanels_canvas_layouts, [:canvas_id])

Use one shared layout store per shared canvas id. Do not let each participant save an independent version of the same shared room.

Check Backend Health

Run LivePanels.Canvas.Shared.backend_health_check!/1 in an application test, release check, or smoke test:

LivePanels.Canvas.Shared.backend_health_check!(
  pubsub: MyApp.PubSub,
  backend:
    {LivePanels.Canvas.Shared.Local,
     layout_store: MyApp.CanvasLayouts},
  canvas_id: "backend-health-check-room"
)

The backend health check exercises the shared backend with a generated canvas. It checks coordinator startup, participant tracking, command application, PubSub fanout, and layout persistence wiring.

Use LivePanels.Canvas.Shared.backend_health_check/1 when you want {:ok, report} or {:error, reason} instead of an exception.

Recovery Behavior

Expected recovery behavior:

  • if the coordinator dies, participant LiveViews try to ensure or reconnect to the coordinator for the same canvas id;
  • if durable layout exists, the restarted coordinator can restore the shared canvas from the layout store;
  • if a participant disconnects, the participant tracker eventually emits a leave diff;
  • if a participant held an interaction lease, the lease sweep can release it;
  • if persistence fails, LivePanels logs and emits telemetry instead of crashing the canvas.

Applications should treat layout persistence failure as operationally important even when the current in-memory canvas keeps running.

Telemetry

Attach handlers to LivePanels telemetry events:

:telemetry.attach_many(
  "my-app-livepanels-shared",
  [
    [:livepanels, :session, :started],
    [:livepanels, :session, :participant, :joined],
    [:livepanels, :session, :participant, :left],
    [:livepanels, :session, :recovery],
    [:livepanels, :broadcast, :state],
    [:livepanels, :broadcast, :delta],
    [:livepanels, :broadcast, :profile],
    [:livepanels, :layout, :persisted],
    [:livepanels, :layout, :persistence_failed]
  ],
  &MyApp.LivePanelsTelemetry.handle_event/4,
  nil
)

Useful operational signals:

  • coordinator starts by canvas id and backend;
  • participant join and leave counts;
  • recovery status and reason;
  • broadcast payload size and fanout time;
  • layout persistence success and failure.

The public telemetry catalog is available through LivePanels.Telemetry.

Participant Session Metadata

Applications can pass participant metadata through the LiveView session under LivePanels.Canvas.Shared.Session.session_key/0.

Use this for viewer id, role, capabilities, or region id when shared runtime policy should know who is connected. Do not put secrets or large domain records in participant metadata.

Shared participants without a session role enter as :viewer. Pass "role" => "editor" in the participant session for browsers that should mutate canvas state.

Keep authorization in the application route, mount logic, and canvas policy. Shared mode assumes the user is allowed to join the canvas you mounted. Canvas policy runs in the coordinator command path; configure its timeout to bound command acceptance checks.

Known Limits

Plan for these boundaries:

  • in-progress drawing drafts broadcast throttled intent, but are not canonical state until committed;
  • application UI state is not automatically shared;
  • durable recovery depends on the configured layout store;
  • clustered behavior depends on the application's distributed registry/supervisor setup;
  • product-level conflict resolution belongs in the application or policy layer.

If your app needs domain-level collaboration beyond canvas state, model that domain data separately and use LivePanels commands only for the canvas effects.