Repository: electric-sql/electric
Pull request #2193: chore (examples): cloud deployment of the phoenix liveview example
Author: login: kevin-dp
Created: 2024-12-19T15:56:13Z | Merged: 2025-05-20T13:07:10Z

This PR contains the necessary SST config for deploying the Phoenix liveview example to the cloud.


Diff:
diff --git a/.github/workflows/deploy_examples.yml b/.github/workflows/deploy_examples.yml
index dbe28a792f..dfd51ccc22 100644
--- a/.github/workflows/deploy_examples.yml
+++ b/.github/workflows/deploy_examples.yml
@@ -33,6 +33,7 @@ jobs:
             nextjs/**
             todo-app/**
             proxy-auth/**
+            phoenix-liveview/**
             tanstack/**
             remix/**
             react/**
@@ -78,6 +79,7 @@ jobs:
       nextjs: ${{ steps.deploy.outputs.nextjs }}
       todo-app: ${{ steps.deploy.outputs.todo-app }}
       proxy-auth: ${{ steps.deploy.outputs.proxy-auth }}
+      phoenix-liveview: ${{ steps.deploy.outputs.phoenix-liveview }}
       tanstack: ${{ steps.deploy.outputs.tanstack }}
       remix: ${{ steps.deploy.outputs.remix }}
       react: ${{ steps.deploy.outputs.react }}
@@ -96,6 +98,7 @@ jobs:
       ELECTRIC_ADMIN_API: ${{ secrets.ELECTRIC_ADMIN_API }}
       ELECTRIC_TEAM_ID: ${{ secrets.ELECTRIC_TEAM_ID }}
       ELECTRIC_ADMIN_API_AUTH_TOKEN: ${{ secrets.ELECTRIC_ADMIN_API_AUTH_TOKEN }}
+      SECRET_KEY_BASE: ${{ secrets.LIVEVIEW_EXAMPLE_SECRET_KEY_BASE }}
 
     steps:
       - uses: actions/checkout@v4
@@ -159,6 +162,7 @@ jobs:
               nextjs: "${{ needs.deploy.outputs.nextjs }}",
               "todo-app": "${{ needs.deploy.outputs.todo-app }}",
               "proxy-auth": "${{ needs.deploy.outputs.proxy-auth }}",
+              "phoenix-liveview": "${{ needs.deploy.outputs.phoenix-liveview }}",
               "tanstack": "${{ needs.deploy.outputs.tanstack }}",
               "remix": "${{ needs.deploy.outputs.remix }}",
               "react": "${{ needs.deploy.outputs.react }}"
diff --git a/.github/workflows/teardown_examples_pr_stack.yml b/.github/workflows/teardown_examples_pr_stack.yml
index 1bc2a5a9ae..b7ed2abbff 100644
--- a/.github/workflows/teardown_examples_pr_stack.yml
+++ b/.github/workflows/teardown_examples_pr_stack.yml
@@ -24,7 +24,10 @@ jobs:
             "nextjs",
             "todo-app",
             "proxy-auth",
+            "phoenix-liveview",
+            "tanstack",
             "remix",
+            "react",
           ]
 
     env:
diff --git a/examples/phoenix-liveview/Dockerfile b/examples/phoenix-liveview/Dockerfile
new file mode 100644
index 0000000000..4622ad7dde
--- /dev/null
+++ b/examples/phoenix-liveview/Dockerfile
@@ -0,0 +1,67 @@
+ARG ELIXIR_VERSION=1.17.2
+ARG OTP_VERSION=27.0.1
+ARG DEBIAN_VERSION=bookworm-20240722-slim
+
+ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
+ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
+
+FROM ${BUILDER_IMAGE} AS builder
+LABEL maintainer="info@electric-sql.com"
+
+RUN apt-get update -y && \
+    apt-get install -y build-essential git curl && \
+    apt-get clean && \
+    rm -f /var/lib/apt/lists/*_*
+
+RUN mix local.hex --force && mix local.rebar --force
+
+ENV MIX_ENV=prod
+
+WORKDIR /app
+
+COPY mix.* /app/
+COPY config/config.exs /app/config/
+COPY config/prod.exs /app/config/
+RUN mix deps.get --only $MIX_ENV --check-locked
+RUN mix deps.compile
+RUN mix assets.setup
+
+COPY config/*runtime.exs /app/config/
+
+COPY lib /app/lib/
+RUN mix compile
+
+COPY assets /app/assets/
+COPY priv /app/priv/
+RUN mix assets.deploy
+
+COPY rel /app/rel/
+
+RUN mix release
+
+FROM ${RUNNER_IMAGE} AS runner_setup
+
+RUN apt-get update -y && \
+    apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates curl && \
+    apt-get clean && \
+    rm -f /var/lib/apt/lists/*_*
+
+# Set the locale
+RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
+
+ENV LANG=en_US.UTF-8
+ENV LANGUAGE=en_US:en
+ENV LC_ALL=en_US.UTF-8
+
+WORKDIR "/app"
+RUN chown nobody /app
+
+FROM runner_setup AS runner
+
+ARG RELEASE_NAME=electric_phoenix_example
+
+COPY --from=builder /app/_build/prod/rel/${RELEASE_NAME} ./
+RUN mv /app/bin/${RELEASE_NAME} /app/bin/entrypoint
+
+CMD ["/app/bin/server"]
+
diff --git a/examples/phoenix-liveview/config/dev.exs b/examples/phoenix-liveview/config/dev.exs
index e6b7645ec8..28980eb89a 100644
--- a/examples/phoenix-liveview/config/dev.exs
+++ b/examples/phoenix-liveview/config/dev.exs
@@ -83,6 +83,10 @@ config :phoenix_live_view,
   # Enable helpful, but potentially expensive runtime checks
   enable_expensive_runtime_checks: true
 
-config :electric_phoenix, Electric.Client,
-  base_url: System.get_env("ELECTRIC_URL", "http://localhost:3000/"),
-  params: System.get_env("ELECTRIC_CLIENT_PARAMS", "{}") |> :json.decode()
+config :phoenix_sync,
+  url:
+    System.get_env("ELECTRIC_URL", "http://localhost:3000/"),
+  mode: :http,
+  credentials: [
+    secret: System.get_env("ELECTRIC_SECRET") || "my-dev-secret",
+  ]
diff --git a/examples/phoenix-liveview/config/runtime.exs b/examples/phoenix-liveview/config/runtime.exs
index 58cd6f5be2..515e1bbf33 100644
--- a/examples/phoenix-liveview/config/runtime.exs
+++ b/examples/phoenix-liveview/config/runtime.exs
@@ -31,10 +31,13 @@ if config_env() == :prod do
   maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
 
   config :electric_phoenix_example, Electric.PhoenixExample.Repo,
-    # ssl: true,
+    ssl: true,
     url: database_url,
     pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
-    socket_options: maybe_ipv6
+    socket_options: maybe_ipv6,
+    ssl_opts: [
+      verify: :verify_none
+    ]
 
   # The secret key base is used to sign/encrypt cookies and other secrets.
   # A default value is used in config/dev.exs and config/test.exs but you
@@ -97,8 +100,18 @@ if config_env() == :prod do
   #
   # Check `Plug.SSL` for all available options in `force_ssl`.
 
-  config :electric_phoenix, Electric.Client,
-    base_url:
+  IO.puts("DEBUG INFO:")
+  dbg(System.get_env("ELECTRIC_URL"))
+  dbg(System.get_env("ELECTRIC_SECRET"))
+  dbg(System.get_env("ELECTRIC_SOURCE_ID"))
+
+  config :phoenix_sync,
+    env: config_env(),
+    url:
       System.get_env("ELECTRIC_URL") || raise("ELECTRIC_URL environment variable not set"),
-    params: System.get_env("ELECTRIC_CLIENT_PARAMS", "{}") |> :json.decode()
+    mode: :http,
+    credentials: [
+      secret: System.get_env("ELECTRIC_SECRET") || raise("ELECTRIC_SECRET environment variable not set"),
+      source_id: System.get_env("ELECTRIC_SOURCE_ID") || raise("ELECTRIC_SOURCE_ID environment variable not set")
+    ]
 end
diff --git a/examples/phoenix-liveview/db/migrations/001-create_todos.sql b/examples/phoenix-liveview/db/migrations/001-create_todos.sql
new file mode 100644
index 0000000000..15105cd262
--- /dev/null
+++ b/examples/phoenix-liveview/db/migrations/001-create_todos.sql
@@ -0,0 +1,7 @@
+CREATE TABLE todos (
+  id BIGSERIAL PRIMARY KEY,
+  text VARCHAR(255) NOT NULL,
+  completed BOOLEAN NOT NULL DEFAULT FALSE,
+  inserted_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
+  updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL
+);
diff --git a/examples/phoenix-liveview/lib/electric_phoenix_example/application.ex b/examples/phoenix-liveview/lib/electric_phoenix_example/application.ex
index 7f55af4689..25f0571a05 100644
--- a/examples/phoenix-liveview/lib/electric_phoenix_example/application.ex
+++ b/examples/phoenix-liveview/lib/electric_phoenix_example/application.ex
@@ -16,7 +16,7 @@ defmodule Electric.PhoenixExample.Application do
       # Start a worker by calling: Electric.PhoenixExample.Worker.start_link(arg)
       # {Electric.PhoenixExample.Worker, arg},
       # Start to serve requests, typically the last entry
-      Electric.PhoenixExampleWeb.Endpoint
+      {Electric.PhoenixExampleWeb.Endpoint, phoenix_sync: Phoenix.Sync.plug_opts()}
     ]
 
     # See https://hexdocs.pm/elixir/Supervisor.html
diff --git a/examples/phoenix-liveview/lib/electric_phoenix_example/release.ex b/examples/phoenix-liveview/lib/electric_phoenix_example/release.ex
new file mode 100644
index 0000000000..8e0b7176bf
--- /dev/null
+++ b/examples/phoenix-liveview/lib/electric_phoenix_example/release.ex
@@ -0,0 +1,28 @@
+defmodule Electric.PhoenixExample.Release do
+  @moduledoc """
+  Used for executing DB release tasks when run in production without Mix
+  installed.
+  """
+  @app :electric_phoenix_example
+
+  def migrate do
+    load_app()
+
+    for repo <- repos() do
+      {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
+    end
+  end
+
+  def rollback(repo, version) do
+    load_app()
+    {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
+  end
+
+  defp repos do
+    Application.fetch_env!(@app, :ecto_repos)
+  end
+
+  defp load_app do
+    Application.load(@app)
+  end
+end
diff --git a/examples/phoenix-liveview/lib/electric_phoenix_example_web/live/todo_live/index.ex b/examples/phoenix-liveview/lib/electric_phoenix_example_web/live/todo_live/index.ex
index 1c1b0da807..29c9ca7024 100644
--- a/examples/phoenix-liveview/lib/electric_phoenix_example_web/live/todo_live/index.ex
+++ b/examples/phoenix-liveview/lib/electric_phoenix_example_web/live/todo_live/index.ex
@@ -10,7 +10,7 @@ defmodule Electric.PhoenixExampleWeb.TodoLive.Index do
      socket
      |> assign(:electric_live, false)
      |> assign(:animate_insert, false)
-     |> Electric.Phoenix.LiveView.electric_stream(:todos, Todos.Todo)}
+     |> Phoenix.Sync.LiveView.sync_stream(:todos, Todos.Todo)}
   end
 
   @impl true
@@ -23,19 +23,19 @@ defmodule Electric.PhoenixExampleWeb.TodoLive.Index do
   # - `:loaded` is sent when the initial fetch has completed
   # - `:live` is sent when the client is in `live` mode and waiting for the
   #         latest updates from the server
-  def handle_info({:electric, {:todos, :loaded}}, socket) do
+  def handle_info({:sync, {:todos, :loaded}}, socket) do
     {:noreply, socket}
   end
 
   # here we use the `:live` state to turn on animations for new Todos
-  def handle_info({:electric, {:todos, :live}}, socket) do
+  def handle_info({:sync, {:todos, :live}}, socket) do
     {:noreply, socket |> assign(:electric_live, true) |> assign(:animate_insert, true)}
   end
 
   # Forward all events from the Electric sync stream to the component.
   # This is **required** for the integration.
-  def handle_info({:electric, event}, socket) do
-    {:noreply, Electric.Phoenix.LiveView.electric_stream_update(socket, event, at: 0)}
+  def handle_info({:sync, event}, socket) do
+    {:noreply, Phoenix.Sync.LiveView.sync_stream_update(socket, event, at: 0)}
   end
 
   @impl true
diff --git a/examples/phoenix-liveview/mix.exs b/examples/phoenix-liveview/mix.exs
index 3339de05b3..0a7a53cb87 100644
--- a/examples/phoenix-liveview/mix.exs
+++ b/examples/phoenix-liveview/mix.exs
@@ -56,8 +56,8 @@ defmodule Electric.PhoenixExample.MixProject do
       {:jason, "~> 1.2"},
       {:dns_cluster, "~> 0.1.1"},
       {:bandit, "~> 1.5"},
-      {:electric_phoenix, "~> 0.2.0"},
-      {:electric_client, ">= 0.2.6-pre-1", override: true}
+      {:phoenix_sync, "~> 0.4.1"},
+      {:electric_client, ">= 0.5.0-beta-1", override: true}
     ]
   end
 
diff --git a/examples/phoenix-liveview/mix.lock b/examples/phoenix-liveview/mix.lock
index 20812602cd..6717a2f30b 100644
--- a/examples/phoenix-liveview/mix.lock
+++ b/examples/phoenix-liveview/mix.lock
@@ -1,42 +1,43 @@
 %{
-  "bandit": {:hex, :bandit, "1.6.1", "9e01b93d72ddc21d8c576a704949e86ee6cde7d11270a1d3073787876527a48f", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5a904bf010ea24b67979835e0507688e31ac873d4ffc8ed0e5413e8d77455031"},
-  "castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"},
+  "bandit": {:hex, :bandit, "1.6.11", "2fbadd60c95310eefb4ba7f1e58810aa8956e18c664a3b2029d57edb7d28d410", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "543f3f06b4721619a1220bed743aa77bf7ecc9c093ba9fab9229ff6b99eacc65"},
+  "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"},
   "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
   "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
   "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
   "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
   "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
-  "electric_client": {:hex, :electric_client, "0.2.6-pre-1", "47b15a100742a6383d3f79913f151af7903922f851c21ccfffb1b362df3c3831", [:mix], [{:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:gen_stage, "~> 1.2", [hex: :gen_stage, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "c9bd26850bd28b5f10a44621a6e347bfb6085519188cead848cb072c8967091b"},
+  "electric_client": {:hex, :electric_client, "0.5.0-beta-1", "6031f02014705be7a0e95dd82b85851f2a3328e630c7bd4e782b182de8349a41", [:mix], [{:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:electric, "~> 1.0.0", [hex: :electric, repo: "hexpm", optional: true]}, {:gen_stage, "~> 1.2", [hex: :gen_stage, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "bc1719907df1ecc08c9c9d7849deca899adb5a70a9e7db62d1c9ebcdd1b80033"},
   "electric_phoenix": {:hex, :electric_phoenix, "0.2.0", "9ebf9a636992597651a0b385573baa31b7933f413a1d65322374f348ad92e712", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:electric_client, "~> 0.2.1", [hex: :electric_client, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d871c16169492a448c6e866595fa0c15d8710d6be62a36c5a3a08b9d2b4225b9"},
   "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
   "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
   "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
   "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
-  "floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
+  "floki": {:hex, :floki, "0.37.1", "d7aaee758c8a5b4a7495799a4260754fec5530d95b9c383c03b27359dea117cf", [:mix], [], "hexpm", "673d040cb594d31318d514590246b6dd587ed341d3b67e17c1c0eb8ce7ca6f04"},
   "gettext": {:hex, :gettext, "0.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"},
   "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
-  "hpax": {:hex, :hpax, "1.0.1", "c857057f89e8bd71d97d9042e009df2a42705d6d690d54eca84c8b29af0787b0", [:mix], [], "hexpm", "4e2d5a4f76ae1e3048f35ae7adb1641c36265510a2d4638157fbcb53dda38445"},
+  "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
   "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
   "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
-  "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
+  "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
   "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
   "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
-  "phoenix": {:hex, :phoenix, "1.7.18", "5310c21443514be44ed93c422e15870aef254cf1b3619e4f91538e7529d2b2e4", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "1797fcc82108442a66f2c77a643a62980f342bfeb63d6c9a515ab8294870004e"},
+  "phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"},
   "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"},
-  "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"},
+  "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"},
   "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
-  "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.1", "5389a30658176c0de816636ce276567478bffd063c082515a6e8368b8fc9a0db", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c0f517e6f290f10dbb94343ac22e0109437fb1fa6f0696e7c73967b789c1c285"},
+  "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.11", "76f6416cb31e2e49601c62b79edc9e6fbd0706368bba9f49c4bd13934b785a28", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "522b8c164d1a0009d30fd3364538d17684cb6f8e6a6931f511f82d891c634cdd"},
   "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
+  "phoenix_sync": {:hex, :phoenix_sync, "0.4.1", "886037b70cc525b888109425003e70bcb6e616c2a1bdfba60d37d1a44c49c448", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:electric, "1.0.1", [hex: :electric, repo: "hexpm", optional: true]}, {:electric_client, ">= 0.5.0-beta-1", [hex: :electric_client, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d5a13d7e1b1de13e1f1225892ea7749098c61c00458219fe409055380549555d"},
   "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
-  "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
-  "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
-  "postgrex": {:hex, :postgrex, "0.19.3", "a0bda6e3bc75ec07fca5b0a89bffd242ca209a4822a9533e7d3e84ee80707e19", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d31c28053655b78f47f948c85bb1cf86a9c1f8ead346ba1aa0d0df017fa05b61"},
-  "req": {:hex, :req, "0.5.8", "50d8d65279d6e343a5e46980ac2a70e97136182950833a1968b371e753f6a662", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d7fc5898a566477e174f26887821a3c5082b243885520ee4b45555f5d53f40ef"},
+  "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"},
+  "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
+  "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"},
+  "req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"},
   "tailwind": {:hex, :tailwind, "0.2.3", "277f08145d407de49650d0a4685dc062174bdd1ae7731c5f1da86163a24dfcdb", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "8e45e7a34a676a7747d04f7913a96c770c85e6be810a1d7f91e713d3a3655b5d"},
   "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
   "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"},
   "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
-  "thousand_island": {:hex, :thousand_island, "1.3.7", "1da7598c0f4f5f50562c097a3f8af308ded48cd35139f0e6f17d9443e4d0c9c5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0139335079953de41d381a6134d8b618d53d084f558c734f2662d1a72818dd12"},
+  "thousand_island": {:hex, :thousand_island, "1.3.13", "d598c609172275f7b1648c9f6eddf900e42312b09bfc2f2020358f926ee00d39", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5a34bdf24ae2f965ddf7ba1a416f3111cfe7df50de8d66f6310e01fc2e80b02a"},
   "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
   "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
 }
diff --git a/examples/phoenix-liveview/package.json b/examples/phoenix-liveview/package.json
new file mode 100644
index 0000000000..6218b2dfa5
--- /dev/null
+++ b/examples/phoenix-liveview/package.json
@@ -0,0 +1,15 @@
+{
+  "name": "phoenix-liveview",
+  "version": "0.0.0",
+  "dependencies": {
+    "camelcase": "^8.0.0"
+  },
+  "devDependencies": {
+    "@databases/pg-migrations": "^5.0.3",
+    "sst": "3.6.35",
+    "typescript": "5.7.2"
+  },
+  "scripts": {
+    "deploy": "sst deploy --stage production"
+  }
+}
\ No newline at end of file
diff --git a/examples/phoenix-liveview/rel/overlays/bin/migrate b/examples/phoenix-liveview/rel/overlays/bin/migrate
new file mode 100755
index 0000000000..cf99fc0160
--- /dev/null
+++ b/examples/phoenix-liveview/rel/overlays/bin/migrate
@@ -0,0 +1,5 @@
+#!/bin/sh
+set -eu
+
+cd -P -- "$(dirname -- "$0")"
+exec ./entrypoint eval Electric.PhoenixExample.Release.migrate
diff --git a/examples/phoenix-liveview/rel/overlays/bin/server b/examples/phoenix-liveview/rel/overlays/bin/server
new file mode 100755
index 0000000000..050783e96f
--- /dev/null
+++ b/examples/phoenix-liveview/rel/overlays/bin/server
@@ -0,0 +1,5 @@
+#!/bin/sh
+set -eu
+
+cd -P -- "$(dirname -- "$0")"
+PHX_SERVER=true exec ./entrypoint start
diff --git a/examples/phoenix-liveview/sst-env.d.ts b/examples/phoenix-liveview/sst-env.d.ts
new file mode 100644
index 0000000000..e4229cc1ad
--- /dev/null
+++ b/examples/phoenix-liveview/sst-env.d.ts
@@ -0,0 +1,18 @@
+/* This file is auto-generated by SST. Do not edit. */
+/* tslint:disable */
+/* eslint-disable */
+/* deno-fmt-ignore-file */
+import "sst"
+export {}
+declare module "sst" {
+  export interface Resource {
+    "electricRegionVpc2EuWest1": {
+      "type": "sst.aws.Vpc"
+    }
+    "phoenixLiveviewServiceEuWest1": {
+      "service": string
+      "type": "sst.aws.Service"
+      "url": string
+    }
+  }
+}
diff --git a/examples/phoenix-liveview/sst.config.ts b/examples/phoenix-liveview/sst.config.ts
new file mode 100644
index 0000000000..b9d1049137
--- /dev/null
+++ b/examples/phoenix-liveview/sst.config.ts
@@ -0,0 +1,78 @@
+/// <reference path="./.sst/platform/config.d.ts" />
+
+import { createDatabaseForCloudElectric } from "../.shared/lib/database"
+import { getSharedCluster, isProduction } from "../.shared/lib/infra"
+
+export default $config({
+  app(input) {
+    return {
+      name: "phoenix-liveview",
+      removal:
+        input?.stage.toLocaleLowerCase() === `production` ? `retain` : `remove`,
+      home: "aws",
+      providers: {
+        cloudflare: `5.42.0`,
+        aws: {
+          version: `6.66.2`,
+          profile: process.env.CI ? undefined : `marketing`,
+        },
+        neon: `0.6.3`,
+        command: `1.0.1`,
+      },
+    }
+  },
+  async run() {
+    if (!process.env.SECRET_KEY_BASE) {
+      throw new Error(`Env variable SECRET_KEY_BASE must be set`)
+    }
+
+    if (!process.env.ELECTRIC_API) {
+      throw new Error(`ELECTRIC_API environment variable is required`)
+    }
+
+    const dbName = isProduction()
+      ? `phoenix-liveview-app`
+      : `phoenix-liveview-app-${$app.stage}`
+
+    const { pooledDatabaseUri, sourceId, sourceSecret } =
+      createDatabaseForCloudElectric({
+        dbName,
+        migrationsDirectory: `./db/migrations`,
+      })
+
+    const domainName = `liveview-app-backend${isProduction() ? `` : `-stage-${$app.stage}`}.examples.electric-sql.com`
+
+    const cluster = getSharedCluster(`phoenix-liveview-app-${$app.stage}`)
+    const service = cluster.addService(
+      `phoenix-liveview-app-${$app.stage}-service`,
+      {
+        loadBalancer: {
+          ports: [
+            { listen: `443/https`, forward: `4000/http` },
+            { listen: `80/http`, forward: `4000/http` },
+          ],
+          domain: {
+            name: domainName,
+            dns: sst.cloudflare.dns(),
+          },
+        },
+        environment: {
+          DATABASE_URL: pooledDatabaseUri,
+          ELECTRIC_URL: process.env.ELECTRIC_API,
+          SECRET_KEY_BASE: process.env.SECRET_KEY_BASE,
+          PHX_HOST: domainName,
+          ELECTRIC_SOURCE_ID: sourceId,
+          ELECTRIC_SECRET: sourceSecret,
+        },
+        image: {
+          context: `.`,
+          dockerfile: `Dockerfile`,
+        },
+      }
+    )
+
+    return {
+      website: service.url,
+    }
+  },
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9ca814c98a..60f484f568 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -623,6 +623,22 @@ importers:
         specifier: ^5.3.4
         version: 5.4.10(@types/node@20.17.6)(terser@5.36.0)
 
+  examples/phoenix-liveview:
+    dependencies:
+      camelcase:
+        specifier: ^8.0.0
+        version: 8.0.0
+    devDependencies:
+      '@databases/pg-migrations':
+        specifier: ^5.0.3
+        version: 5.0.3(typescript@5.7.2)
+      sst:
+        specifier: 3.6.35
+        version: 3.6.35
+      typescript:
+        specifier: 5.7.2
+        version: 5.7.2
+
   examples/proxy-auth:
     dependencies:
       '@electric-sql/client':
@@ -1461,13 +1477,13 @@ importers:
         version: 9.15.0
       vitepress:
         specifier: ^1.3.1
-        version: 1.5.0(@algolia/client-search@5.13.0)(@types/node@20.17.6)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)(terser@5.36.0)(typescript@5.6.3)
+        version: 1.5.0(@algolia/client-search@5.13.0)(@types/node@20.17.6)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)(terser@5.36.0)(typescript@5.7.2)
       vitepress-plugin-tabs:
         specifier: ^0.5.0
-        version: 0.5.0(vitepress@1.5.0(@algolia/client-search@5.13.0)(@types/node@20.17.6)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)(terser@5.36.0)(typescript@5.6.3))(vue@3.5.12(typescript@5.6.3))
+        version: 0.5.0(vitepress@1.5.0(@algolia/client-search@5.13.0)(@types/node@20.17.6)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)(terser@5.36.0)(typescript@5.7.2))(vue@3.5.12(typescript@5.7.2))
       vue-tweet:
         specifier: ^2.3.1
-        version: 2.3.1(vue@3.5.12(typescript@5.6.3))
+        version: 2.3.1(vue@3.5.12(typescript@5.7.2))
       yaml:
         specifier: ^2.5.0
         version: 2.6.0
@@ -9502,6 +9518,11 @@ packages:
     engines: {node: '>=14.17'}
     hasBin: true
 
+  typescript@5.7.2:
+    resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+
   uc.micro@2.1.0:
     resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
 
@@ -11245,6 +11266,13 @@ snapshots:
     transitivePeerDependencies:
       - typescript
 
+  '@databases/pg-config@3.2.0(typescript@5.7.2)':
+    dependencies:
+      cosmiconfig: 8.3.6(typescript@5.7.2)
+      funtypes: 4.2.0
+    transitivePeerDependencies:
+      - typescript
+
   '@databases/pg-connection-string@1.0.0': {}
 
   '@databases/pg-data-type-id@3.0.0': {}
@@ -11267,6 +11295,22 @@ snapshots:
       - pg-native
       - typescript
 
+  '@databases/pg-migrations@5.0.3(typescript@5.7.2)':
+    dependencies:
+      '@databases/migrations-base': 3.0.1
+      '@databases/pg': 5.5.0(typescript@5.7.2)
+      '@databases/pg-config': 3.2.0(typescript@5.7.2)
+      assert-never: 1.3.0
+      chalk: 4.1.2
+      interrogator: 2.0.1
+      is-interactive: 1.0.0
+      parameter-reducers: 2.1.0
+      semver: 7.6.3
+      sucrase: 3.35.0
+    transitivePeerDependencies:
+      - pg-native
+      - typescript
+
   '@databases/pg@5.5.0(typescript@5.6.3)':
     dependencies:
       '@babel/code-frame': 7.26.2
@@ -11286,6 +11330,25 @@ snapshots:
       - pg-native
       - typescript
 
+  '@databases/pg@5.5.0(typescript@5.7.2)':
+    dependencies:
+      '@babel/code-frame': 7.26.2
+      '@databases/escape-identifier': 1.0.3
+      '@databases/pg-config': 3.2.0(typescript@5.7.2)
+      '@databases/pg-connection-string': 1.0.0
+      '@databases/pg-data-type-id': 3.0.0
+      '@databases/pg-errors': 1.0.0
+      '@databases/push-to-async-iterable': 3.0.0
+      '@databases/shared': 3.1.0
+      '@databases/split-sql-query': 1.0.4(@databases/sql@3.3.0)
+      '@databases/sql': 3.3.0
+      assert-never: 1.3.0
+      pg: 8.13.1
+      pg-cursor: 2.12.1(pg@8.13.1)
+    transitivePeerDependencies:
+      - pg-native
+      - typescript
+
   '@databases/push-to-async-iterable@3.0.0':
     dependencies:
       '@databases/queue': 1.0.1
@@ -14141,10 +14204,10 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@vitejs/plugin-vue@5.1.4(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))':
+  '@vitejs/plugin-vue@5.1.4(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0))(vue@3.5.12(typescript@5.7.2))':
     dependencies:
       vite: 5.4.10(@types/node@20.17.6)(terser@5.36.0)
-      vue: 3.5.12(typescript@5.6.3)
+      vue: 3.5.12(typescript@5.7.2)
 
   '@vitest/expect@2.1.4':
     dependencies:
@@ -14258,29 +14321,29 @@ snapshots:
       '@vue/shared': 3.5.12
       csstype: 3.1.3
 
-  '@vue/server-renderer@3.5.12(vue@3.5.12(typescript@5.6.3))':
+  '@vue/server-renderer@3.5.12(vue@3.5.12(typescript@5.7.2))':
     dependencies:
       '@vue/compiler-ssr': 3.5.12
       '@vue/shared': 3.5.12
-      vue: 3.5.12(typescript@5.6.3)
+      vue: 3.5.12(typescript@5.7.2)
 
   '@vue/shared@3.5.12': {}
 
-  '@vueuse/core@11.2.0(vue@3.5.12(typescript@5.6.3))':
+  '@vueuse/core@11.2.0(vue@3.5.12(typescript@5.7.2))':
     dependencies:
       '@types/web-bluetooth': 0.0.20
       '@vueuse/metadata': 11.2.0
-      '@vueuse/shared': 11.2.0(vue@3.5.12(typescript@5.6.3))
-      vue-demi: 0.14.10(vue@3.5.12(typescript@5.6.3))
+      '@vueuse/shared': 11.2.0(vue@3.5.12(typescript@5.7.2))
+      vue-demi: 0.14.10(vue@3.5.12(typescript@5.7.2))
     transitivePeerDependencies:
       - '@vue/composition-api'
       - vue
 
-  '@vueuse/integrations@11.2.0(focus-trap@7.6.0)(vue@3.5.12(typescript@5.6.3))':
+  '@vueuse/integrations@11.2.0(focus-trap@7.6.0)(vue@3.5.12(typescript@5.7.2))':
     dependencies:
-      '@vueuse/core': 11.2.0(vue@3.5.12(typescript@5.6.3))
-      '@vueuse/shared': 11.2.0(vue@3.5.12(typescript@5.6.3))
-      vue-demi: 0.14.10(vue@3.5.12(typescript@5.6.3))
+      '@vueuse/core': 11.2.0(vue@3.5.12(typescript@5.7.2))
+      '@vueuse/shared': 11.2.0(vue@3.5.12(typescript@5.7.2))
+      vue-demi: 0.14.10(vue@3.5.12(typescript@5.7.2))
     optionalDependencies:
       focus-trap: 7.6.0
     transitivePeerDependencies:
@@ -14289,9 +14352,9 @@ snapshots:
 
   '@vueuse/metadata@11.2.0': {}
 
-  '@vueuse/shared@11.2.0(vue@3.5.12(typescript@5.6.3))':
+  '@vueuse/shared@11.2.0(vue@3.5.12(typescript@5.7.2))':
     dependencies:
-      vue-demi: 0.14.10(vue@3.5.12(typescript@5.6.3))
+      vue-demi: 0.14.10(vue@3.5.12(typescript@5.7.2))
     transitivePeerDependencies:
       - '@vue/composition-api'
       - vue
@@ -14864,6 +14927,15 @@ snapshots:
     optionalDependencies:
       typescript: 5.6.3
 
+  cosmiconfig@8.3.6(typescript@5.7.2):
+    dependencies:
+      import-fresh: 3.3.0
+      js-yaml: 4.1.0
+      parse-json: 5.2.0
+      path-type: 4.0.0
+    optionalDependencies:
+      typescript: 5.7.2
+
   crelt@1.0.6: {}
 
   cross-spawn@7.0.3:
@@ -19396,6 +19468,8 @@ snapshots:
 
   typescript@5.6.3: {}
 
+  typescript@5.7.2: {}
+
   uc.micro@2.1.0: {}
 
   ufo@1.5.4: {}
@@ -19738,12 +19812,12 @@ snapshots:
       fsevents: 2.3.3
       terser: 5.36.0
 
-  vitepress-plugin-tabs@0.5.0(vitepress@1.5.0(@algolia/client-search@5.13.0)(@types/node@20.17.6)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)(terser@5.36.0)(typescript@5.6.3))(vue@3.5.12(typescript@5.6.3)):
+  vitepress-plugin-tabs@0.5.0(vitepress@1.5.0(@algolia/client-search@5.13.0)(@types/node@20.17.6)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)(terser@5.36.0)(typescript@5.7.2))(vue@3.5.12(typescript@5.7.2)):
     dependencies:
-      vitepress: 1.5.0(@algolia/client-search@5.13.0)(@types/node@20.17.6)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)(terser@5.36.0)(typescript@5.6.3)
-      vue: 3.5.12(typescript@5.6.3)
+      vitepress: 1.5.0(@algolia/client-search@5.13.0)(@types/node@20.17.6)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)(terser@5.36.0)(typescript@5.7.2)
+      vue: 3.5.12(typescript@5.7.2)
 
-  vitepress@1.5.0(@algolia/client-search@5.13.0)(@types/node@20.17.6)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)(terser@5.36.0)(typescript@5.6.3):
+  vitepress@1.5.0(@algolia/client-search@5.13.0)(@types/node@20.17.6)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)(terser@5.36.0)(typescript@5.7.2):
     dependencies:
       '@docsearch/css': 3.7.0
       '@docsearch/js': 3.7.0(@algolia/client-search@5.13.0)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)
@@ -19752,17 +19826,17 @@ snapshots:
       '@shikijs/transformers': 1.22.2
       '@shikijs/types': 1.22.2
       '@types/markdown-it': 14.1.2
-      '@vitejs/plugin-vue': 5.1.4(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))
+      '@vitejs/plugin-vue': 5.1.4(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0))(vue@3.5.12(typescript@5.7.2))
       '@vue/devtools-api': 7.6.3
       '@vue/shared': 3.5.12
-      '@vueuse/core': 11.2.0(vue@3.5.12(typescript@5.6.3))
-      '@vueuse/integrations': 11.2.0(focus-trap@7.6.0)(vue@3.5.12(typescript@5.6.3))
+      '@vueuse/core': 11.2.0(vue@3.5.12(typescript@5.7.2))
+      '@vueuse/integrations': 11.2.0(focus-trap@7.6.0)(vue@3.5.12(typescript@5.7.2))
       focus-trap: 7.6.0
       mark.js: 8.11.1
       minisearch: 7.1.0
       shiki: 1.22.2
       vite: 5.4.10(@types/node@20.17.6)(terser@5.36.0)
-      vue: 3.5.12(typescript@5.6.3)
+      vue: 3.5.12(typescript@5.7.2)
     optionalDependencies:
       postcss: 8.4.47
     transitivePeerDependencies:
@@ -19865,23 +19939,23 @@ snapshots:
       - supports-color
       - terser
 
-  vue-demi@0.14.10(vue@3.5.12(typescript@5.6.3)):
+  vue-demi@0.14.10(vue@3.5.12(typescript@5.7.2)):
     dependencies:
-      vue: 3.5.12(typescript@5.6.3)
+      vue: 3.5.12(typescript@5.7.2)
 
-  vue-tweet@2.3.1(vue@3.5.12(typescript@5.6.3)):
+  vue-tweet@2.3.1(vue@3.5.12(typescript@5.7.2)):
     dependencies:
-      vue: 3.5.12(typescript@5.6.3)
+      vue: 3.5.12(typescript@5.7.2)
 
-  vue@3.5.12(typescript@5.6.3):
+  vue@3.5.12(typescript@5.7.2):
     dependencies:
       '@vue/compiler-dom': 3.5.12
       '@vue/compiler-sfc': 3.5.12
       '@vue/runtime-dom': 3.5.12
-      '@vue/server-renderer': 3.5.12(vue@3.5.12(typescript@5.6.3))
+      '@vue/server-renderer': 3.5.12(vue@3.5.12(typescript@5.7.2))
       '@vue/shared': 3.5.12
     optionalDependencies:
-      typescript: 5.6.3
+      typescript: 5.7.2
 
   w3c-keyname@2.2.8: {}
 


Reviews:
user: login: icehaunter
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2024-12-19T16:09:26Z
user: login: magnetised
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T11:19:15Z
user: login: magnetised
body: Looks great -- thanks for doing that. Think the sst code could do with some cleanup though
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T16:13:34Z
user: login: kevin-dp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T16:40:45Z

Review comments:
diff_hunk: @@ -0,0 +1,165 @@
+/// <reference path="./.sst/platform/config.d.ts" />
+
+import { execSync } from "child_process"
+import camelcase from "camelcase"
+
+const isProduction = (stage) => stage.toLowerCase() === `production`
+
+const regionName = `eu-west-1`
+
+export default $config({
+  app(input) {
+    return {
+      name: "phoenix-liveview",
+      removal: input?.stage === "production" ? "retain" : "remove",
+      protect: ["production"].includes(input?.stage),
+      home: "aws",
+      providers: {
+        cloudflare: `5.42.0`,
+        aws: { version: `6.57.0`, region: regionName },
+        postgresql: `3.14.0`,
+      },
+    }
+  },
+  async run() {
+    if (!process.env.SECRET_KEY_BASE) {
+      throw new Error(
+        `Env variable SECRET_KEY_BASE must be set`
+      )
+    }
+
+    if (!process.env.ELECTRIC_API || !process.env.ELECTRIC_ADMIN_API)
+      throw new Error(
+        `Env variables ELECTRIC_API and ELECTRIC_ADMIN_API must be set`
+      )
+
+    if (
+      !process.env.EXAMPLES_DATABASE_HOST ||
+      !process.env.EXAMPLES_DATABASE_PASSWORD
+    ) {
+      throw new Error(
+        `Env variables EXAMPLES_DATABASE_HOST and EXAMPLES_DATABASE_PASSWORD must be set`
+      )
+    }
+
+    const provider = new postgresql.Provider(`neon`, {
+      host: process.env.EXAMPLES_DATABASE_HOST,
+      database: `neondb`,
+      username: `neondb_owner`,
+      password: process.env.EXAMPLES_DATABASE_PASSWORD,
+    })
+
+    const pg = new postgresql.Database(`liveview`, {}, { provider })
+
+    const pgBaseUri = $interpolate`postgresql://${provider.username}:${provider.password}@${provider.host}/${pg.name}`
+    const pgUriForElectric = $interpolate`${pgBaseUri}?sslmode=require`
+    const electricInfo = pgUriForElectric.apply((uri) => {
+      return addDatabaseToElectric(uri, `eu-west-1`)
+    })
+
+    const domainName = `phoenix-liveview${isProduction($app.stage) ? `` : `-stage-${$app.stage}`}.examples.electric-sql.com`
+
+    // Run the server on ECS
+    const vpcName = camelcase(`electric-region-vpc-2-${regionName}`)
+    const vpc = new sst.aws.Vpc(
+      vpcName,
+      {
+        nat: $app.stage === `production` ? `managed` : `ec2`,
+      },
+      { provider }
+    )
+
+    const cluster = new sst.aws.Cluster(
+      camelcase(`phoenix-liveview-cluster-${regionName}`),
+      { forceUpgrade: `v2`, vpc },
+      { provider }
+    )
+
+    const liveviewService = cluster.addService(
+      camelcase(`phoenix-liveview-service-${regionName}`),
+      {
+        loadBalancer: {
+          public: true,
+          domain: {
+            name: domainName,
+            dns: sst.cloudflare.dns(),
+          },
+          ports: [
+            { listen: `443/https`, forward: `4000/http` },
+            { listen: `80/http`, forward: `4000/http` },
+          ],
+        },
+        cpu: `0.25 vCPU`,
+        memory: `0.5 GB`,
+        // Uncomment the line below if you're trying to deploy from a Mac
+        //architecture: `arm64`,
+        transform: {
+          target: {
+            deregistrationDelay: 0,
+          },
+          service: {
+            waitForSteadyState: true,
+          },
+        },
+        environment: {
+          DATABASE_URL: $interpolate`${pgBaseUri}?ssl=true`,
+          ELECTRIC_URL: `https://api-dev-production.electric-sql.com`,
+          SECRET_KEY_BASE: process.env.SECRET_KEY_BASE,
+          PHX_HOST: domainName,
+          ELECTRIC_CLIENT_PARAMS: $interpolate`{ "database_id": "${electricInfo.id}", "token": "${electricInfo.token}" }`,
path: examples/phoenix-liveview/sst.config.ts
user: login: magnetised
body: this needs to be `source_id`
created_at: 2025-01-21T11:19:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,176 @@
+/// <reference path="./.sst/platform/config.d.ts" />
+
+import { createDatabaseForCloudElectric } from "../.shared/lib/database"
+import { getSharedCluster, isProduction } from "../.shared/lib/infra"
+
+export default $config({
+  app(input) {
+    return {
+      name: "phoenix-liveview",
+      removal:
+        input?.stage.toLocaleLowerCase() === `production` ? `retain` : `remove`,
+      home: "aws",
+      providers: {
+        cloudflare: `5.42.0`,
+        aws: {
+          version: `6.66.2`,
+          profile: process.env.CI ? undefined : `marketing`,
+        },
+        neon: `0.6.3`,
+        command: `1.0.1`,
+      },
+    }
+  },
+  async run() {
+    if (!process.env.SECRET_KEY_BASE) {
+      throw new Error(`Env variable SECRET_KEY_BASE must be set`)
+    }
+
+    if (!process.env.ELECTRIC_API) {
+      throw new Error(`ELECTRIC_API environment variable is required`)
+    }
+
+    const dbName = isProduction()
+      ? `phoenix-liveview-app`
+      : `phoenix-liveview-app-${$app.stage}`
+
+    const { pooledDatabaseUri, sourceId, sourceSecret } =
+      createDatabaseForCloudElectric({
+        dbName,
+        migrationsDirectory: `./db/migrations`,
+      })
+
+    const domainName = `liveview-app-backend${isProduction() ? `` : `-stage-${$app.stage}`}.examples.electric-sql.com`
+
+    const cluster = getSharedCluster(`phoenix-liveview-app-${$app.stage}`)
+    const service = cluster.addService(
+      `phoenix-liveview-app-${$app.stage}-service`,
+      {
+        loadBalancer: {
+          ports: [
+            { listen: `443/https`, forward: `4000/http` },
+            { listen: `80/http`, forward: `4000/http` },
+          ],
+          domain: {
+            name: domainName,
+            dns: sst.cloudflare.dns(),
+          },
+        },
+        environment: {
+          DATABASE_URL: pooledDatabaseUri,
+          ELECTRIC_URL: process.env.ELECTRIC_API,
+          SECRET_KEY_BASE: process.env.SECRET_KEY_BASE,
+          PHX_HOST: domainName,
+          ELECTRIC_SOURCE_ID: sourceId,
+          ELECTRIC_SECRET: sourceSecret,
+          ELECTRIC_LOG_LEVEL: `debug`,
+        },
+        image: {
+          context: `.`,
+          dockerfile: `Dockerfile`,
+        },
+      }
+    )
+
+    return {
+      website: service.url,
+    }
+
+    /*
+    if (!process.env.ELECTRIC_API || !process.env.ELECTRIC_ADMIN_API)
path: examples/phoenix-liveview/sst.config.ts
user: login: magnetised
body: any reason to keep all this commented code?
created_at: 2025-05-19T16:08:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,176 @@
+/// <reference path="./.sst/platform/config.d.ts" />
+
+import { createDatabaseForCloudElectric } from "../.shared/lib/database"
+import { getSharedCluster, isProduction } from "../.shared/lib/infra"
+
+export default $config({
+  app(input) {
+    return {
+      name: "phoenix-liveview",
+      removal:
+        input?.stage.toLocaleLowerCase() === `production` ? `retain` : `remove`,
+      home: "aws",
+      providers: {
+        cloudflare: `5.42.0`,
+        aws: {
+          version: `6.66.2`,
+          profile: process.env.CI ? undefined : `marketing`,
+        },
+        neon: `0.6.3`,
+        command: `1.0.1`,
+      },
+    }
+  },
+  async run() {
+    if (!process.env.SECRET_KEY_BASE) {
+      throw new Error(`Env variable SECRET_KEY_BASE must be set`)
+    }
+
+    if (!process.env.ELECTRIC_API) {
+      throw new Error(`ELECTRIC_API environment variable is required`)
+    }
+
+    const dbName = isProduction()
+      ? `phoenix-liveview-app`
+      : `phoenix-liveview-app-${$app.stage}`
+
+    const { pooledDatabaseUri, sourceId, sourceSecret } =
+      createDatabaseForCloudElectric({
+        dbName,
+        migrationsDirectory: `./db/migrations`,
path: examples/phoenix-liveview/sst.config.ts
user: login: magnetised
body: weird having the whole phoenix/ecto migration machinery and then just using this... but understand that this is maybe all we need
created_at: 2025-05-19T16:12:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,176 @@
+/// <reference path="./.sst/platform/config.d.ts" />
+
+import { createDatabaseForCloudElectric } from "../.shared/lib/database"
+import { getSharedCluster, isProduction } from "../.shared/lib/infra"
+
+export default $config({
+  app(input) {
+    return {
+      name: "phoenix-liveview",
+      removal:
+        input?.stage.toLocaleLowerCase() === `production` ? `retain` : `remove`,
+      home: "aws",
+      providers: {
+        cloudflare: `5.42.0`,
+        aws: {
+          version: `6.66.2`,
+          profile: process.env.CI ? undefined : `marketing`,
+        },
+        neon: `0.6.3`,
+        command: `1.0.1`,
+      },
+    }
+  },
+  async run() {
+    if (!process.env.SECRET_KEY_BASE) {
+      throw new Error(`Env variable SECRET_KEY_BASE must be set`)
+    }
+
+    if (!process.env.ELECTRIC_API) {
+      throw new Error(`ELECTRIC_API environment variable is required`)
+    }
+
+    const dbName = isProduction()
+      ? `phoenix-liveview-app`
+      : `phoenix-liveview-app-${$app.stage}`
+
+    const { pooledDatabaseUri, sourceId, sourceSecret } =
+      createDatabaseForCloudElectric({
+        dbName,
+        migrationsDirectory: `./db/migrations`,
+      })
+
+    const domainName = `liveview-app-backend${isProduction() ? `` : `-stage-${$app.stage}`}.examples.electric-sql.com`
+
+    const cluster = getSharedCluster(`phoenix-liveview-app-${$app.stage}`)
+    const service = cluster.addService(
+      `phoenix-liveview-app-${$app.stage}-service`,
+      {
+        loadBalancer: {
+          ports: [
+            { listen: `443/https`, forward: `4000/http` },
+            { listen: `80/http`, forward: `4000/http` },
+          ],
+          domain: {
+            name: domainName,
+            dns: sst.cloudflare.dns(),
+          },
+        },
+        environment: {
+          DATABASE_URL: pooledDatabaseUri,
+          ELECTRIC_URL: process.env.ELECTRIC_API,
+          SECRET_KEY_BASE: process.env.SECRET_KEY_BASE,
+          PHX_HOST: domainName,
+          ELECTRIC_SOURCE_ID: sourceId,
+          ELECTRIC_SECRET: sourceSecret,
+          ELECTRIC_LOG_LEVEL: `debug`,
path: examples/phoenix-liveview/sst.config.ts
user: login: magnetised
body: wouldn't it be better to lower the log level in prod? 
created_at: 2025-05-19T16:12:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,176 @@
+/// <reference path="./.sst/platform/config.d.ts" />
+
+import { createDatabaseForCloudElectric } from "../.shared/lib/database"
+import { getSharedCluster, isProduction } from "../.shared/lib/infra"
+
+export default $config({
+  app(input) {
+    return {
+      name: "phoenix-liveview",
+      removal:
+        input?.stage.toLocaleLowerCase() === `production` ? `retain` : `remove`,
+      home: "aws",
+      providers: {
+        cloudflare: `5.42.0`,
+        aws: {
+          version: `6.66.2`,
+          profile: process.env.CI ? undefined : `marketing`,
+        },
+        neon: `0.6.3`,
+        command: `1.0.1`,
+      },
+    }
+  },
+  async run() {
+    if (!process.env.SECRET_KEY_BASE) {
+      throw new Error(`Env variable SECRET_KEY_BASE must be set`)
+    }
+
+    if (!process.env.ELECTRIC_API) {
+      throw new Error(`ELECTRIC_API environment variable is required`)
+    }
+
+    const dbName = isProduction()
+      ? `phoenix-liveview-app`
+      : `phoenix-liveview-app-${$app.stage}`
+
+    const { pooledDatabaseUri, sourceId, sourceSecret } =
+      createDatabaseForCloudElectric({
+        dbName,
+        migrationsDirectory: `./db/migrations`,
path: examples/phoenix-liveview/sst.config.ts
user: login: kevin-dp
body: Indeed, but this is a common pattern across all examples. So best to keep it for now.
created_at: 2025-05-19T16:40:45Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-13T08:21:58Z
body: ## Examples
- phoenix-liveview: https://liveview-app-backend-stage-pr-2193.examples.electric-sql.com/
author_association: CONTRIBUTOR
user: login: netlify[bot]
created_at: 2025-05-13T13:48:53Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *electric-next* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | ce3e3c3b713355bc9e5e7f02e6e5b4219e66a34e |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/projects/electric-next/deploys/6825d331cd3b170008d946bf |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-2193--electric-next.netlify.app](https://deploy-preview-2193--electric-next.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTIxOTMtLWVsZWN0cmljLW5leHQubmV0bGlmeS5hcHAifQ.bMyk1yFjeMyuyP7ks4L9ZkF_-QaC5t5VBKrAQvr2Fe0)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [electric-next Preview](https://deploy-preview-2193--electric-next.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify project configuration](https://app.netlify.com/projects/electric-next/configuration/notifications#deploy-notifications)._
author_association: NONE
user: login: codecov[bot]
created_at: 2025-05-20T12:52:22Z
body: ### :x: 1 Tests Failed:
| Tests completed | Failed | Passed | Skipped |
|---|---|---|---|
| 2095 | 1 | 2094 | 974 |
<details><summary>View the top 1 failed test(s) by shortest run time</summary>

> <pre><code class="language-python">Elixir.Electric.Shapes.ConsumerTest::test transaction handling with real storage restarting a consumer doesn&#x27;t lower the last known offset when only snapshot is present</code></pre>
> <details><summary>Stack Traces | 0s run time</summary>
> 
> > <pre><code class="language-python">5) test transaction handling with real storage restarting a consumer doesn&#x27;t lower the last known offset when only snapshot is present (Electric.Shapes.ConsumerTest)
> >      .../electric/shapes/consumer_test.exs:869
> >      ** (EXIT from #PID&lt;0.5962.0&gt;) an exception was raised:
> >          ** (ArgumentError) errors were found at the given arguments:
> > 
> >        * 1st argument: table name already exists
> > 
> >              (stdlib 6.2) :ets.new(:&quot;Elixir.Electric.Shapes.ConsumerTest test transaction handling with real storage restarting a consumer doesn&#x27;t lower the last known offset when only snapshot is present:lsn_tracker&quot;, [:protected, :named_table])
> >              (electric 1.0.13) lib/electric/lsn_tracker.ex:34: Electric.LsnTracker.create_table/1
> >              (electric 1.0.13) lib/electric/lsn_tracker.ex:5: Electric.LsnTracker.init/2
> >              (electric 1.0.13) .../electric/replication/shape_log_collector.ex:133: Electric.Replication.ShapeLogCollector.handle_call/3
> >              (gen_stage 1.2.1) lib/gen_stage.ex:1942: GenStage.handle_call/3
> >              (stdlib 6.2) gen_server.erl:2381: :gen_server.try_handle_call/4
> >              (stdlib 6.2) gen_server.erl:2410: :gen_server.handle_msg/6
> >              (stdlib 6.2) proc_lib.erl:329: :proc_lib.init_p_do_apply/3</code></pre>
> 
> </details>

</details>

To view more test analytics, go to the [Test Analytics Dashboard](https://app.codecov.io/gh/electric-sql/electric/tests/kevin%2Fphoenix-liveview-deployment)
<sub>📋 Got 3 mins? [Take this short survey](https://forms.gle/BpocVj23nhr2Y45G7) to help us improve Test Analytics.</sub>
author_association: NONE