Phoenix.Sync

repository·main·Indexed 43 days ago

https://github.com/electric-sql/phoenix_sync

Real-time data synchronization for Postgres-backed Phoenix applications. Built on ElectricSQL, it enables bi-directional sync between Postgres and clients (LiveView, Web, Mobile) using 'Shapes' for partial replication. Features include sync_stream/4 for LiveView, a Writer module for atomic write-path sync via Ecto.Multi, and support for both embedded and HTTP interaction modes.

Tokens
10.9K
Snippets
32
Records
43
Agent score
54%

What's inside Phoenix.Sync

  1. Overview of TxidMatch

    main

    TxidMatch is a tool designed to force PostgreSQL's transaction ID (txid) to wrap around. This is used to validate transaction ID matching logic.

    Note: This tool is only tested and functional on Linux.

    On typical hardware, forcing a wrap-around can take several hours. You can adjust the frequency of the wrap-around by tweaking the bumper_count setting in TxidMatch.Writer.Supervisor to better suit your machine's performance.

  2. How Phoenix.Sync read-path APIs work

    main

    Phoenix.Sync provides four primary APIs to sync data from Postgres to your clients (LiveView, Web, or Mobile):

    1. Phoenix.Sync.Client.stream/2: Low-level Elixir API to convert an Ecto.Query into an Elixir Stream.
    2. Phoenix.Sync.LiveView.sync_stream/4: Used within LiveView to automatically keep a stream up-to-date with the database state.
    3. Phoenix.Sync.Router.sync/2: A macro used in your Router to expose statically defined shapes. This allows clients to connect through your existing Plug middleware (e.g., for authentication).
    4. Phoenix.Sync.Controller.sync_render/3: A view function used in standard Controllers to return dynamic shapes defined at runtime based on the request/session.
  3. Define shapes using Ecto.Query or Ecto.Schema

    main

    Phoenix.Sync supports two primary methods for defining shapes:

    1. Ecto-based definitions: Use Ecto.Query or an Ecto.Schema module. This is ideal for dynamic shapes and allows you to control data synchronization using standard Ecto logic. Note that sync_render/3 in a controller is required for Ecto.Query support, as router macros do not support them to avoid compile-time dependencies.
    2. Keyword list definitions: Use a keyword list for static or low-level shape definitions. This is used with router macros or for more granular control over Postgres-specific details like namespaces and column selection.
  4. How Phoenix.Sync and ElectricSQL work together

    main

    Phoenix.Sync is built on top of ElectricSQL, a real-time sync engine for Postgres. Phoenix.Sync leverages Electric to manage the core complexities of partial replication, data fan-out, and data delivery.

    Data is partitioned and defined using Shapes, which are the mechanism for partial replication in ElectricSQL.

  5. Run TxidMatch tests with local Postgres

    main

    To use TxidMatch, you can set up a local PostgreSQL database using an in-memory data directory via Mix tasks.

    1. Setup the database

    Use the following command to create the local Postgres instance with a ramdisk/in-memory directory:

    mix txid.postgres up

    2. Run the transaction ID matching tests

    You can test how the system handles different ID types by passing different PostgreSQL functions to the mix txid command:

    To test 64-bit IDs (expected to fail):

    mix txid "txid_current()"

    To test 32-bit IDs (expected to work):

    mix txid "pg_current_xact_id()::xid"

    3. Cleanup

    Once testing is complete, stop the Postgres server and release the ramdisk memory:

    mix txid.postgres down
    mix txid.postgres up
    mix txid "pg_current_xact_id()::xid"
    mix txid.postgres down
  6. Sync data into a LiveView stream

    main

    To keep a LiveView up-to-date with Postgres, replace Phoenix.LiveView.stream/3 with Phoenix.Sync.LiveView.sync_stream/4. You must also handle the {:sync, event} message in handle_info to update the stream using sync_stream_update/2.

    defmodule MyWeb.MyLive do
      use Phoenix.LiveView
      import Phoenix.Sync.LiveView
    
      def mount(_params, _session, socket) do
        {:ok, sync_stream(socket, :todos, Todos.Todo)}
      end
    
      def handle_info({:sync, event}, socket) do
        {:noreply, sync_stream_update(socket, event)}
      end
    end
  7. Configure PlugSync interaction modes

    main

    PlugSync can be configured using the PHOENIX_SYNC_MODE environment variable. This determines how the application interacts with the sync layer:

    • `
    # Example: Running in http mode
    PHOENIX_SYNC_MODE="http" mix phx.server
    
    # Example: Running in embedded mode
    PHOENIX_SYNC_MODE="embedded" mix phx.server
  8. Install Phoenix.Sync manually

    main

    If you are not using Igniter, you can configure Phoenix.Sync manually. The configuration depends on whether you are using :embedded or :http mode.

    Embedded Mode

    In :embedded mode, Electric is an application dependency and Phoenix.Sync consumes data internally via Elixir APIs. This is the most efficient mode for local development.

    HTTP Mode

    In :http mode, Phoenix.Sync consumes data from an external Electric service via its HTTP API. This is useful for production environments using Electric Cloud or when running Electric as a separate service.

    # Example for Embedded Mode
    # mix.exs
    defp deps do
      [
        {:electric, "~> 1.0"},
        {:phoenix_sync, "~> 0.6"}
      ]
    end
    
    # config/config.exs
    config :phoenix_sync,
      env: config_env(),
      mode: :embedded,
      repo: MyApp.Repo
    
    # application.ex
    children = [
      MyApp.Repo,
      # ...
      {MyApp.Endpoint, phoenix_sync: Phoenix.Sync.plug_opts()}
    ]
    
    # Example for HTTP Mode
    # mix.exs
    defp deps do
      [
        {:phoenix_sync, "~> 0.6"}
      ]
    end
    
    # config/config.exs
    config :phoenix_sync,
      env: config_env(),
      mode: :http,
      url: "https://api.electric-sql.cloud",
      credentials: [
        secret: "...",    # required
        source_id: "..."  # optional, required for Electric Cloud
      ]
    
    # application.ex
    children = [
      MyApp.Repo,
      # ...
      {MyApp.Endpoint, phoenix_sync: Phoenix.Sync.plug_opts()}
    ]
  9. Test sync endpoints with Phoenix.Sync.Sandbox

    main
    To test sync endpoints (including LiveView, controllers, and routers) within your Phoenix/Ecto application, use the Phoenix.Sync.Sandbox module. Refer to the module documentation for specific implementation details.
  10. Implement write-path sync with Phoenix.Sync.Writer

    main

    To sync writes from the client back to Postgres, use the Phoenix.Sync.Writer module. This allows you to ingest batches of writes (e.g., from local optimistic updates) and apply them atomically using Ecto.Multi.

    Workflow:

    1. Create a new writer with Writer.new().
    2. Use Writer.allow/3 to register schemas and define authorization, loading, and validation logic.
    3. Apply the transaction using Writer.apply/4.

    By default, Writer.allow/3 uses the schema's changeset/2 function for validation.

    defmodule MutationController do
      use Phoenix.Controller, formats: [:json]
    
      alias Phoenix.Sync.Writer
      alias Phoenix.Sync.Writer.Format
    
      def mutate(conn, %{"transaction" => transaction} = _params) do
        user_id = conn.assigns.user_id
    
        {:ok, txid, _changes} =
          Writer.new()
          |> Writer.allow(
            Projects.Project,
            check: reject_invalid_params/1,
            load: &Projects.load_for_user(&1, user_id),
            validate: &Projects.Project.changeset/2
          )
          |> Writer.allow(
            Projects.Issue
            # Uses sensible defaults: validate: Projects.Issue.changeset/2
          )
          |> Writer.apply(transaction, Repo, format: Format.TanstackDB)
    
        json(conn, %{txid: txid})
      end
    end
  11. Install Phoenix.Sync using Igniter

    main

    You can use Igniter to automatically configure Phoenix.Sync in your project. This handles dependency installation and sets up the Ecto.Repo with the Phoenix.Sync.Sandbox test adapter.

    First, add igniter to your mix.exs dependencies:

    {:igniter, "~> 0.6", only: [:dev, :test]}

    Then, run the appropriate installation command based on your desired sync mode.

    # For embedded mode
    mix igniter.install phoenix_sync --sync-mode embedded
    
    # For embedded mode without sandbox support
    mix igniter.install phoenix_sync --sync-mode embedded --no-sync-sandbox
    
    # For HTTP mode with an external URL
    mix igniter.install phoenix_sync --sync-mode http --sync-url "https://api.electric-sql.cloud"