### Plug Router Sync Example
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Demonstrates using `Phoenix.Sync.Router` within a Plug router setup. This allows exposing data shapes without a full Phoenix LiveView context.
```elixir
defmodule MyApp.PlugRouter do
use Plug.Router, copy_opts_to_assign: :options
use Phoenix.Sync.Router
plug :match
plug :dispatch
sync "/shapes/todos", MyApp.Todos.Todo
sync "/shapes/active", MyApp.Todos.Todo, where: "completed = false"
end
```
--------------------------------
### Setup DataCase for Phoenix.Sync Sandbox
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Set up the ExUnit CaseTemplate to integrate with the Phoenix.Sync Sandbox. This includes checking out connections and starting the sandbox for tests.
```elixir
# test/support/data_case.ex
defmodule MyApp.DataCase do
use ExUnit.CaseTemplate
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, {:shared, self()})
end
# Start the Phoenix.Sync sandbox
Phoenix.Sync.Sandbox.start!(MyApp.Repo)
:ok
end
end
```
--------------------------------
### Phoenix.Sync.Controller.sync_render/3 Examples
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Examples demonstrating how to use sync_render/3 in Phoenix controllers for dynamic data synchronization.
```APIDOC
## POST /api/sync/todos
### Description
Renders sync shapes from controllers with dynamic query support based on request parameters.
### Method
POST
### Endpoint
/api/sync/todos
### Parameters
#### Query Parameters
- **params** (map) - Required - Parameters for dynamic query building.
#### Request Body
- **conn** (Plug.Conn) - The connection object.
- **params** (map) - Request parameters.
- **schema_or_query** (module | Ecto.Query | function) - The Ecto schema, query, or a function returning a query.
### Request Example
```elixir
def all(conn, params) do
sync_render(conn, params, MyApp.Todos.Todo)
end
def by_status(conn, %{"completed" => completed} = params) do
query = from(t in MyApp.Todos.Todo, where: t.completed == ^completed)
sync_render(conn, params, query)
end
def my_todos(%{assigns: %{current_user: user}} = conn, params) do
query = from(t in MyApp.Todos.Todo, where: t.user_id == ^user.id)
sync_render(conn, params, query)
end
def project_tasks(conn, %{"project_id" => project_id} = params) do
sync_render(conn, params, fn ->
task_ids = MyApp.Projects.get_visible_task_ids(project_id)
from(t in MyApp.Tasks.Task, where: t.id in ^task_ids)
end)
end
def full_todos(conn, params) do
sync_render(conn, params, MyApp.Todos.Todo, replica: :full)
end
```
### Response
#### Success Response (200)
- **sync_data** (any) - The synchronized data.
### Error Handling
- **400 Bad Request**: Invalid parameters or query.
- **500 Internal Server Error**: Server error during sync rendering.
```
--------------------------------
### Start Phoenix.Sync Shape Processes
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Configure the application to start Phoenix.Sync.Shape processes for different Ecto schemas. Supports starting a shape for a full schema or a filtered subset.
```elixir
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
MyApp.Repo,
# Start a shape process for todos
{Phoenix.Sync.Shape, [MyApp.Todos.Todo, name: MyApp.TodoShape]},
# Start a filtered shape
{Phoenix.Sync.Shape, [
Ecto.Query.from(t in MyApp.Todos.Todo, where: t.completed == false),
name: MyApp.ActiveTodoShape
]}
]
Supervisor.start_link(children, strategy: :one_for_one)
end
end
```
--------------------------------
### Start Phoenix.Sync Endpoint
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Start the Phoenix.Sync endpoint in your application.ex file by including it in the children list of the supervisor.
```elixir
def start(_type, _args) do
children = [
MyApp.Repo,
{MyAppWeb.Endpoint, phoenix_sync: Phoenix.Sync.plug_opts()}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
```
--------------------------------
### Phoenix.Sync Installation and Configuration
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Instructions on how to install and configure Phoenix.Sync in your Phoenix application, including embedded and HTTP modes.
```APIDOC
## Installation and Configuration
Configure Phoenix.Sync in your application with embedded or HTTP mode for connecting to ElectricSQL.
```elixir
# mix.exs - Add dependencies
defp deps do
[
{:electric, "~> 1.0"}, # Required for embedded mode
{:phoenix_sync, "~> 0.6"}
]
end
# config/config.exs - Embedded mode (Electric runs inside your app)
config :phoenix_sync,
env: config_env(),
mode: :embedded,
repo: MyApp.Repo
# config/config.exs - HTTP mode (connects to external Electric service)
config :phoenix_sync,
env: config_env(),
mode: :http,
url: "https://api.electric-sql.cloud",
credentials: [
secret: "your-secret-key",
source_id: "your-source-id" # Required for Electric Cloud
]
# application.ex - Start the endpoint with Phoenix.Sync
def start(_type, _args) do
children = [
MyApp.Repo,
{MyAppWeb.Endpoint, phoenix_sync: Phoenix.Sync.plug_opts()}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
```
```
--------------------------------
### Install Phoenix.Sync in HTTP Mode with Igniter
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Install `Phoenix.Sync` in HTTP mode using Igniter, specifying the URL for the external Electric service.
```bash
mix igniter.install phoenix_sync --sync-mode http --sync-url "https://api.electric-sql.cloud"
```
--------------------------------
### Start Local PostgreSQL Database
Source: https://github.com/electric-sql/phoenix_sync/blob/main/apps/txid_match/README.md
Use this command to create a local PostgreSQL database with an in-memory data directory for testing.
```bash
mix txid.postgres up
```
--------------------------------
### Install Phoenix.Sync in Embedded Mode with Igniter
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Use the Igniter mix task to install `Phoenix.Sync` in embedded mode. This command also sets up your `Ecto.Repo` with the sandbox test adapter.
```bash
mix igniter.install phoenix_sync --sync-mode embedded
```
--------------------------------
### Install Phoenix.Sync in Embedded Mode without Sandbox
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Install `Phoenix.Sync` in embedded mode using Igniter, opting out of the sandbox test adapter.
```bash
mix igniter.install phoenix_sync --sync-mode embedded --no-sync-sandbox
```
--------------------------------
### Transform Todo Title Example
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
An example transformation function that prepends a prefix to the 'title' field of a todo item.
```elixir
def transform_todo(msg, prefix) do
update_in(msg, ["value", "title"], fn title -> "#{prefix} #{title}" end)
end
```
--------------------------------
### Handle Client Writes with Phoenix.Sync.Writer
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Implement a Phoenix controller to handle batched client writes using `Phoenix.Sync.Writer`. This example demonstrates authorizing and validating writes against `Projects.Project` and `Projects.Issue` schemas before applying them atomically using `Ecto.Multi`.
```elixir
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,
# Use the sensible defaults:
# validate: Projects.Issue.changeset/2
# etc.
)
|> Writer.apply(transaction, Repo, format: Format.TanstackDB)
json(conn, %{txid: txid})
end
end
```
--------------------------------
### Phoenix.Sync Local HTTP Services Configuration
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Configure Phoenix.Sync to use HTTP mode with a local Electric service. This setup is useful for testing or when running an HTTP proxy. Include both 'electric' and 'phoenix_sync' dependencies.
```elixir
defp deps do
[
{:electric, "~> 1.0"},
{:phoenix_sync, "~> 0.6"}
]
end
config :phoenix_sync,
env: config_env(),
mode: :http,
http:
port: 3000,
repo: MyApp.Repo,
url: "http://localhost:3000"
children = [
MyApp.Repo,
# ...
{MyApp.Endpoint, phoenix_sync: Phoenix.Sync.plug_opts()}
]
```
--------------------------------
### Sync Filtered Todos in LiveView
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Use `sync_stream/3` with an Ecto query to synchronize a subset of records based on specific criteria. This example syncs todos for a given user ID.
```elixir
defmodule MyAppWeb.FilteredTodoLive do
use Phoenix.LiveView
import Phoenix.Sync.LiveView
import Ecto.Query
def mount(%{"user_id" => user_id}, _session, socket) do
# Sync only todos belonging to a specific user
query = from(t in MyApp.Todos.Todo, where: t.user_id == ^user_id)
socket = sync_stream(socket, :user_todos, query)
{:ok, socket}
end
def handle_info({:sync, event}, socket) do
{:noreply, sync_stream_update(socket, event)}
end
def render(assigns) do
~H"""
"""
end
end
```
--------------------------------
### Sync with Where Clause Filter via Router
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Filter records exposed via the router using a `where` option. This example syncs only incomplete todos.
```elixir
sync "/incomplete-todos", MyApp.Todos.Todo,
where: "completed = false"
```
--------------------------------
### Stream All Todos from Schema
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Stream all changes from the Todo Ecto schema using Phoenix.Sync.Client.stream/2. This example processes insert, update, and delete operations, and handles the :up_to_date control message.
```elixir
defmodule MyApp.SyncConsumer do
alias Phoenix.Sync.Client
alias MyApp.Todos.Todo
def stream_all_todos do
# Stream from an Ecto.Schema module
stream = Client.stream(Todo)
# Process stream events
Enum.each(stream, fn
%Electric.Client.Message.ChangeMessage{headers: %{operation: :insert}, value: todo} ->
IO.puts("New todo: #{todo.title}")
%Electric.Client.Message.ChangeMessage{headers: %{operation: :update}, value: todo} ->
IO.puts("Updated todo: #{todo.title}")
%Electric.Client.Message.ChangeMessage{headers: %{operation: :delete}, value: todo} ->
IO.puts("Deleted todo: #{todo.id}")
%Electric.Client.Message.ControlMessage{control: :up_to_date} ->
IO.puts("Sync is up to date")
end)
end
def stream_incomplete_todos do
# Stream from an Ecto.Query with where clause
import Ecto.Query
from(t in Todo, where: t.completed == false)
|> Client.stream()
|> Enum.take(10) # Get first 10 incomplete todos
end
def stream_with_keyword_shape do
# Stream using keyword-based shape definition
Client.stream(
table: "todos",
where: "completed = false",
columns: ["id", "title", "completed"]
)
|> Stream.map(fn msg -> msg.value end)
|> Enum.to_list()
end
end
```
--------------------------------
### Sync Specific Columns via Router
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Limit the exposed data to specific columns using the `columns` option. This example syncs only the `id` and `title` columns.
```elixir
sync "/todo-titles", MyApp.Todos.Todo,
columns: ["id", "title"]
```
--------------------------------
### Add Phoenix.Sync Dependencies
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Add the electric and phoenix_sync dependencies to your mix.exs file.
```elixir
defp deps do
[
{:electric, "~> 1.0"},
{:phoenix_sync, "~> 0.6"}
]
end
```
--------------------------------
### Add Electric and Phoenix.Sync Dependencies
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Manually add the `electric` and `phoenix_sync` libraries to your project's dependencies in `mix.exs` for embedded mode.
```elixir
defp deps do
[
{:electric, "~> 1.0"},
{:phoenix_sync, "~> 0.6"}
]
end
```
--------------------------------
### Initialize Test Session for Sync Endpoint
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Initialize the test session for a controller test to include the Phoenix.Sync.Sandbox client. This is necessary for testing sync endpoints.
```elixir
# test/my_app_web/controllers/todo_controller_test.exs
defmodule MyAppWeb.TodoControllerTest do
use MyAppWeb.ConnCase
setup %{conn: conn} do
# Initialize test session with sandbox client
conn = Phoenix.Sync.Sandbox.init_test_session(conn, MyApp.Repo)
{:ok, conn: conn}
end
test "sync endpoint returns shape data", %{conn: conn} do
# Create test data
{:ok, _todo} = MyApp.Todos.create_todo(%{title: "Test"})
conn = get(conn, ~p"/shapes/todos")
assert response(conn, 200)
end
end
```
--------------------------------
### Sync All Todos in LiveView
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Use `sync_stream/3` in `mount/3` to synchronize all records of a given Ecto schema. Requires `handle_info/2` to process sync events.
```elixir
defmodule MyAppWeb.TodoLive do
use Phoenix.LiveView
import Phoenix.Sync.LiveView
import Ecto.Query
def mount(_params, _session, socket) do
# Sync all todos from the database
socket = sync_stream(socket, :todos, MyApp.Todos.Todo)
{:ok, assign(socket, show_stream: false)}
end
# Handle sync events - required callback
def handle_info({:sync, event}, socket) do
{:noreply, sync_stream_update(socket, event)}
end
# Handle :loaded event - initial data has been loaded
def handle_info({:sync, {:todos, :loaded}}, socket) do
{:noreply, assign(socket, :show_stream, true)}
end
# Handle :live event - stream is now in live sync mode
def handle_info({:sync, {:todos, :live}}, socket) do
IO.puts("Todos are fully synchronized!")
{:noreply, socket}
end
def render(assigns) do
~H"""
<%= todo.title %>
<%= if todo.completed, do: "Done", else: "Pending" %>
"""
end
end
```
--------------------------------
### Basic Sync Render with Ecto Schema
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Renders a sync shape for a given Ecto schema. Ensure the schema is imported.
```elixir
def all(conn, params) do
sync_render(conn, params, MyApp.Todos.Todo)
end
```
--------------------------------
### Configure Phoenix.Sync Sandbox Mode
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Configure the application to use the sandbox adapter for testing Ecto operations. This enables the generation of replication events for testing.
```elixir
# config/test.exs
config :phoenix_sync,
env: config_env(),
mode: :sandbox
# lib/my_app/repo.ex - Configure repo with sandbox adapter
defmodule MyApp.Repo do
use Phoenix.Sync.Sandbox.Postgres
use Ecto.Repo,
otp_app: :my_app,
adapter: Phoenix.Sync.Sandbox.Postgres.adapter()
end
```
--------------------------------
### Configure Phoenix.Sync in HTTP Mode
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Configure Phoenix.Sync for HTTP mode in config/config.exs, specifying the environment, mode, URL, and cloud credentials.
```elixir
config :phoenix_sync,
env: config_env(),
mode: :http,
url: "https://api.electric-sql.cloud",
credentials: [
secret: "your-secret-key",
source_id: "your-source-id" # Required for Electric Cloud
]
```
--------------------------------
### Phoenix.Sync.Router.sync/2
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Router macro for exposing statically-defined shapes through your Phoenix or Plug router.
```APIDOC
## Phoenix.Sync.Router.sync/2
Router macro for exposing statically-defined shapes through your Phoenix or Plug router.
### Description
This macro simplifies the process of defining API endpoints that serve data from your Ecto schemas, allowing for filtering, column selection, and custom transformations.
### Usage in Phoenix Router
```elixir
defmodule MyAppWeb.Router do
use Phoenix.Router
import Phoenix.Sync.Router
pipeline :sync do
plug :authenticate_user
plug :require_sync_permission
end
scope "/shapes" do
pipe_through :sync
# Sync entire table
sync "/todos", MyApp.Todos.Todo
# Sync with where clause filter
sync "/incomplete-todos", MyApp.Todos.Todo,
where: "completed = false"
# Sync specific columns only
sync "/todo-titles", MyApp.Todos.Todo,
columns: ["id", "title"]
# Sync with full replica (includes all columns on updates)
sync "/full-todos", MyApp.Todos.Todo,
replica: :full
# Sync using keyword-based shape definition
sync "/projects", table: "projects", where: "archived = false"
# Sync with transform function
sync "/transformed-todos", MyApp.Todos.Todo,
transform: {MyAppWeb.Router, :transform_todo, ["[TODO]"]}
end
def transform_todo(msg, prefix) do
update_in(msg, ["value", "title"], fn title -> "#{prefix} #{title}" end)
end
end
```
### Usage in Plug Router
```elixir
defmodule MyApp.PlugRouter do
use Plug.Router, copy_opts_to_assign: :options
use Phoenix.Sync.Router
plug :match
plug :dispatch
sync "/shapes/todos", MyApp.Todos.Todo
sync "/shapes/active", MyApp.Todos.Todo, where: "completed = false"
end
```
### Options
- `where`: A string representing an Ecto `where` clause.
- `columns`: A list of strings specifying which columns to include.
- `replica`: Set to `:full` to include all columns on updates.
- `table`: Specifies the table name if it differs from the schema name.
- `transform`: A tuple `{module, function, args}` for transforming messages.
```
--------------------------------
### Phoenix.Sync Multi-Environment Configuration
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Configure Phoenix.Sync for different environments: embedded in dev, local HTTP in test, and external HTTP in prod. The 'electric' dependency is included only for dev and test environments.
```elixir
defp deps do
[
{:electric, "~> 1.0", only: [:dev, :test]},
{:phoenix_sync, "~> 0.6"}
]
end
# config/dev.exs
config :phoenix_sync,
env: config_env(),
mode: :embedded,
repo: MyApp.Repo
# config/test.esx
config :phoenix_sync,
env: config_env(),
mode: :http,
http:
port: 3000,
repo: MyApp.Repo,
url: "http://localhost:3000"
# config/prod.exs
config :phoenix_sync,
mode: :http,
url: "https://api.electric-sql.cloud",
credentials:
secret: "...", # required
source_id: "..." # optional, required for Electric Cloud
]
children = [
MyApp.Repo,
# ...
{MyApp.Endpoint, phoenix_sync: Phoenix.Sync.plug_opts()}
]
```
--------------------------------
### Basic Mutation Handler with TanStack/db Format
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Handles basic mutations using Phoenix.Sync.Writer with the TanStack/db format. Includes authorization checks and validation using Ecto changesets.
```elixir
def mutate(conn, %{"transaction" => transaction}) do
user_id = conn.assigns.current_user.id
{:ok, txid, _changes} =
Writer.new()
|> Writer.allow(
MyApp.Todos.Todo,
check: &check_todo_mutation(&1, user_id),
load: &load_todo_for_user(&1, user_id),
validate: &MyApp.Todos.Todo.changeset/2
)
|> Writer.allow(
MyApp.Projects.Project,
check: &check_project_mutation(&1, user_id),
validate: &MyApp.Projects.Project.changeset/2
)
|> Writer.apply(transaction, MyApp.Repo, format: Format.TanstackDB)
json(conn, %{txid: txid})
end
```
--------------------------------
### Sync Full Replica via Router
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Configure the sync to include all columns on updates by setting `replica: :full`. This ensures complete data is available for all operations.
```elixir
sync "/full-todos", MyApp.Todos.Todo,
replica: :full
```
--------------------------------
### Configure Phoenix.Sync for Embedded Mode
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Configure `Phoenix.Sync` in `config/config.exs` for embedded mode, specifying the environment, mode, and Ecto repository.
```elixir
config :phoenix_sync,
env: config_env(),
mode: :embedded,
repo: MyApp.Repo
```
--------------------------------
### Add Igniter to Project Dependencies
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Include the Igniter library in your project's `mix.exs` file for development and testing purposes.
```elixir
{:igniter, "~> 0.6", only: [:dev, :test]}
```
--------------------------------
### Stream Incomplete Todos with Query
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Stream the first 10 incomplete todos by defining an Ecto query with a where clause and then piping it to Client.stream/1.
```elixir
import Ecto.Query
from(t in Todo, where: t.completed == false)
|> Client.stream()
|> Enum.take(10)
```
--------------------------------
### Sync Render with Shape Options
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Renders a sync shape with additional options, such as specifying the replica mode. This allows for fine-grained control over sync behavior.
```elixir
def full_todos(conn, params) do
sync_render(conn, params, MyApp.Todos.Todo, replica: :full)
end
```
--------------------------------
### Writing Mutations with TanStack/db
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Ingest optimistic client mutations using the TanStack/db library and fetch API. Monitor the sync stream for transaction IDs to confirm writes.
```typescript
async function createTodo(title: string) {
const response = await fetch("/api/mutations", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
transaction: [
{
type: "insert",
syncMetadata: { relation: ["public", "todos"] },
modified: { title, completed: false },
},
],
}),
});
const { txid } = await response.json();
// Monitor sync stream for txid to confirm write
return txid;
}
```
--------------------------------
### Load Function for Todo Data with Authorization
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Retrieves the original todo record for a mutation, ensuring authorization by filtering by both ID and user ID. This prevents unauthorized data access.
```elixir
defp load_todo_for_user(%{"id" => id}, user_id) do
import Ecto.Query
from(t in MyApp.Todos.Todo,
where: t.id == ^id and t.user_id == ^user_id
)
|> MyApp.Repo.one()
end
```
--------------------------------
### Render Dynamic Shapes from Controller
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Utilize `Phoenix.Sync.Controller.sync_render/3` within a controller to dynamically define and render shapes based on request parameters or session data at runtime.
```elixir
defmodule Phoenix.Sync.LiveViewTest.TodoController do
use Phoenix.Controller
import Phoenix.Sync.Controller
import Ecto.Query, only: [from: 2]
def show(conn, %{"done" => done} = params) do
sync_render(conn, params, from(t in Todos.Todo, where: t.done == ^done))
end
def show_mine(%{assigns: %{current_user: user_id}} = conn, params) do
sync_render(conn, params, from(t in Todos.Todo, where: t.owner_id == ^user_id))
end
end
```
--------------------------------
### Phoenix.Sync Render Todos with Ecto Schema
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Render all todos from the Todos.Todo schema using Phoenix.Sync. This is a simpler way to define a shape when no specific filtering is needed beyond the schema itself.
```elixir
sync_render(conn, params, Todos.Todo)
```
--------------------------------
### Phoenix.Sync Render Todos with Ecto Query
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Render incomplete todos using an Ecto.Query with Phoenix.Sync. This method allows for dynamic filtering of data to be synced.
```elixir
sync_render(conn, params, from(t in Todos.Todo, where: t.completed == false))
```
--------------------------------
### Sync LiveView Stream with Database
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Replace `Phoenix.LiveView.stream/3` with `Phoenix.Sync.LiveView.sync_stream/4` to automatically update a LiveView with database changes. The `handle_info` callback is used to process sync events.
```elixir
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
```
--------------------------------
### Phoenix.Sync HTTP Mode Configuration
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Configure Phoenix.Sync to use HTTP mode with an external Electric Cloud service. Ensure the 'phoenix_sync' dependency is added to your mix.exs file.
```elixir
defp deps do
[
{:phoenix_sync, "~> 0.6"}
]
end
config :phoenix_sync,
env: config_env(),
mode: :http,
url: "https://api.electric-sql.cloud",
credentials:
secret: "...", # required
source_id: "..." # optional, required for Electric Cloud
]
children = [
MyApp.Repo,
# ...
{MyApp.Endpoint, phoenix_sync: Phoenix.Sync.plug_opts()}
]
```
--------------------------------
### Test Txid Matching with 64-bit IDs
Source: https://github.com/electric-sql/phoenix_sync/blob/main/apps/txid_match/README.md
Test transaction ID matching using `txid_current()` which returns 64-bit IDs. This test is expected to fail.
```bash
mix txid "txid_current()"
```
--------------------------------
### Expose Shapes via Phoenix Router
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Use the `Phoenix.Sync.Router.sync/2` macro to expose statically defined shapes directly in your Phoenix Router. This allows client connections to go through existing Plug middleware for authentication.
```elixir
defmodule MyWeb.Router do
use Phoenix.Router
import Phoenix.Sync.Router
pipeline :sync do
plug :my_auth
end
scope "/shapes" do
pipe_through :sync
sync "/todos", Todos.Todo
end
end
```
--------------------------------
### Sync Entire Table via Router
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Expose an entire Ecto table via the router using `sync/2`. This makes all columns available.
```elixir
sync "/todos", MyApp.Todos.Todo
```
--------------------------------
### Phoenix.Sync Static Shape with Replica Option
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Define a static sync shape for incomplete todos, specifying the replica mode as ':full'. This can be combined with a 'where' clause for fine-grained control over data synchronization.
```elixir
sync "/incomplete-todos", Todos.Todo, where: "completed = false", replica: :full
```
--------------------------------
### Phoenix.Sync.Writer API
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Handles write-path sync for ingesting batched optimistic writes from clients with authorization and validation.
```APIDOC
## POST /api/sync/mutate
### Description
Handles write-path sync for ingesting batched optimistic writes from clients with authorization and validation.
### Method
POST
### Endpoint
/api/sync/mutate
### Parameters
#### Path Parameters
None
#### Query Parameters
None
#### Request Body
- **conn** (Plug.Conn) - The connection object.
- **transaction** (map) - The transaction data to be applied.
### Request Example
```elixir
def mutate(conn, %{"transaction" => transaction}) do
user_id = conn.assigns.current_user.id
{:ok, txid, _changes} =
Writer.new()
|> Writer.allow(
MyApp.Todos.Todo,
check: &check_todo_mutation(&1, user_id),
load: &load_todo_for_user(&1, user_id),
validate: &MyApp.Todos.Todo.changeset/2
)
|> Writer.allow(
MyApp.Projects.Project,
check: &check_project_mutation(&1, user_id),
validate: &MyApp.Projects.Project.changeset/2
)
|> Writer.apply(transaction, MyApp.Repo, format: Format.TanstackDB)
json(conn, %{txid: txid})
end
def advanced_mutate(conn, %{"transaction" => transaction}) do
user_id = conn.assigns.current_user.id
{:ok, txid, _changes} =
Writer.new()
|> Writer.allow(
MyApp.Todos.Todo,
check: &check_todo_mutation(&1, user_id),
load: fn repo, data -> repo.get(MyApp.Todos.Todo, data["id"]) end,
validate: &MyApp.Todos.Todo.changeset/2,
insert: [
post_apply: &create_audit_log/3
],
update: [
validate: &MyApp.Todos.Todo.update_changeset/2,
pre_apply: &validate_update_permissions/3
],
delete: [
pre_apply: &check_delete_allowed/3
]
)
|> Writer.apply(transaction, MyApp.Repo, format: Format.TanstackDB)
json(conn, %{txid: txid})
end
```
### Response
#### Success Response (200)
- **txid** (integer) - The transaction ID.
### Error Handling
- **400 Bad Request**: Invalid transaction data or validation errors.
- **401 Unauthorized**: User not authorized for the operation.
- **500 Internal Server Error**: Server error during mutation processing.
```
--------------------------------
### Subscribe to Phoenix.Sync.Shape Events
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Implement a GenServer to subscribe to synchronization events from a Shape process. Handles insert, update, delete, and synchronization status messages.
```elixir
defmodule MyApp.TodoSubscriber do
use GenServer
alias Phoenix.Sync.Shape
def start_link(_) do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end
def init(_) do
ref = Shape.subscribe(MyApp.TodoShape)
{:ok, %{ref: ref}}
end
# Handle insert events
def handle_info({:sync, ref, {:insert, {key, todo}}}, %{ref: ref} = state) do
IO.puts("New todo added: #{todo.title}")
{:noreply, state}
end
# Handle update events
def handle_info({:sync, ref, {:update, {key, todo}}}, %{ref: ref} = state) do
IO.puts("Todo updated: #{todo.title}")
{:noreply, state}
end
# Handle delete events
def handle_info({:sync, ref, {:delete, {key, todo}}}, %{ref: ref} = state) do
IO.puts("Todo deleted: #{todo.id}")
{:noreply, state}
end
# Handle control messages
def handle_info({:sync, ref, :up_to_date}, %{ref: ref} = state) do
IO.puts("Shape is fully synchronized")
{:noreply, state}
end
def handle_info({:sync, ref, :must_refetch}, %{ref: ref} = state) do
IO.puts("Shape needs refetch")
{:noreply, state}
end
end
```
--------------------------------
### Create Audit Log Callback
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
A callback function used during mutation to create an audit log entry after a todo is successfully inserted. It records the action and the ID of the created record.
```elixir
defp create_audit_log(multi, changeset, context) do
name = Writer.operation_name(context, :audit_log)
Ecto.Multi.insert(multi, name, %MyApp.AuditLog{
action: "todo_created",
record_id: changeset.data.id
})
end
```
--------------------------------
### Sync Using Keyword-Based Shape Definition
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Define the data shape using keyword arguments for `table` and `where` clauses, useful for non-Ecto table sources or simpler configurations.
```elixir
sync "/projects", table: "projects", where: "archived = false"
```
--------------------------------
### Check Delete Allowed Callback
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
A placeholder callback function for checking delete permissions before a mutation is applied. Currently, it returns the multi-argument unchanged, indicating no custom permission checks are implemented.
```elixir
defp check_delete_allowed(multi, _changeset, _context) do
multi # Add delete permission checks
end
```
--------------------------------
### Advanced Mutation Handler with Per-Operation Callbacks
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Handles advanced mutations with per-operation callbacks for insert, update, and delete. This allows for custom logic like audit logging and permission checks before applying changes.
```elixir
def advanced_mutate(conn, %{"transaction" => transaction}) do
user_id = conn.assigns.current_user.id
{:ok, txid, _changes} =
Writer.new()
|> Writer.allow(
MyApp.Todos.Todo,
check: &check_todo_mutation(&1, user_id),
load: fn repo, data -> repo.get(MyApp.Todos.Todo, data["id"]) end,
validate: &MyApp.Todos.Todo.changeset/2,
insert: [
post_apply: &create_audit_log/3
],
update: [
validate: &MyApp.Todos.Todo.update_changeset/2,
pre_apply: &validate_update_permissions/3
],
delete: [
pre_apply: &check_delete_allowed/3
]
)
|> Writer.apply(transaction, MyApp.Repo, format: Format.TanstackDB)
json(conn, %{txid: txid})
end
```
--------------------------------
### Test Sync Stream with Insert Events
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Write tests to verify that sync_stream receives insert events when records are created within the sandboxed connection. Asserts that a sync event is received.
```elixir
# test/my_app/todos_sync_test.exs
defmodule MyApp.TodosSyncTest do
use MyApp.DataCase
import Phoenix.Sync.LiveView
test "sync_stream receives insert events" do
# Create a socket and sync stream
{:ok, socket} = Phoenix.LiveView.Socket.build_socket()
socket = sync_stream(socket, :todos, MyApp.Todos.Todo)
# Insert a record - the sandbox will emit sync events
{:ok, todo} = MyApp.Todos.create_todo(%{title: "Test Todo"})
# Verify the sync event was received
assert_receive {:sync, _event}
end
test "allowing external processes" do
# Allow a worker process to access the sandboxed connection
worker_pid = Process.whereis(MyApp.Worker)
Phoenix.Sync.Sandbox.allow(MyApp.Repo, self(), worker_pid)
# Worker can now insert and trigger sync events
GenServer.call(MyApp.Worker, {:create_todo, %{title: "Worker Todo"}})
end
end
```
--------------------------------
### Dynamic Sync Render Based on Request Parameters
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Renders a sync shape dynamically based on request parameters, allowing for filtered queries. The query is constructed using Ecto.Query.
```elixir
def by_status(conn, %{"completed" => completed} = params) do
query = from(t in MyApp.Todos.Todo, where: t.completed == ^completed)
sync_render(conn, params, query)
end
```
--------------------------------
### Phoenix.Sync Static Shape with Where Clause
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Define a static sync shape in the router for incomplete todos using an Ecto.Schema and a 'where' clause. This avoids recompilation issues associated with Ecto.Query structs in routers.
```elixir
sync "/incomplete-todos", Todos.Todo, where: "completed = false"
```
--------------------------------
### Consuming Phoenix.Sync Shapes with TypeScript Client
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Integrate Phoenix.Sync shapes into frontend applications using the Electric TypeScript client. Create shape streams, subscribe to changes, and handle authentication.
```typescript
import { Shape, ShapeStream } from "@electric-sql/client";
// Create a shape stream
const stream = new ShapeStream({
url: `/shapes/todos`,
});
const shape = new Shape(stream);
// Subscribe to changes
shape.subscribe((data) => {
console.log("Todos updated:", data);
});
// With authentication
const authenticatedStream = new ShapeStream({
url: `/shapes/my-todos`,
headers: {
Authorization: `Bearer ${authToken}`,
},
});
```
--------------------------------
### Test Txid Matching with 32-bit IDs
Source: https://github.com/electric-sql/phoenix_sync/blob/main/apps/txid_match/README.md
Test transaction ID matching using `pg_current_xact_id()::xid` which returns 32-bit IDs. This test is expected to pass.
```bash
mix txid "pg_current_xact_id()::xid"
```
--------------------------------
### Phoenix.Sync.Client.stream/2
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
API for converting an Ecto query or schema into an Elixir Stream for consuming sync events programmatically.
```APIDOC
## Phoenix.Sync.Client.stream/2
Converts an Ecto query or schema into an Elixir Stream for consuming sync events programmatically.
### Example Usage
```elixir
defmodule MyApp.SyncConsumer do
alias Phoenix.Sync.Client
alias MyApp.Todos.Todo
def stream_all_todos do
# Stream from an Ecto.Schema module
stream = Client.stream(Todo)
# Process stream events
Enum.each(stream, fn
%Electric.Client.Message.ChangeMessage{headers: %{operation: :insert}, value: todo} ->
IO.puts("New todo: #{todo.title}")
%Electric.Client.Message.ChangeMessage{headers: %{operation: :update}, value: todo} ->
IO.puts("Updated todo: #{todo.title}")
%Electric.Client.Message.ChangeMessage{headers: %{operation: :delete}, value: todo} ->
IO.puts("Deleted todo: #{todo.id}")
%Electric.Client.Message.ControlMessage{control: :up_to_date} ->
IO.puts("Sync is up to date")
end)
end
def stream_incomplete_todos do
# Stream from an Ecto.Query with where clause
import Ecto.Query
from(t in Todo, where: t.completed == false)
|> Client.stream()
|> Enum.take(10) # Get first 10 incomplete todos
end
def stream_with_keyword_shape do
# Stream using keyword-based shape definition
Client.stream(
table: "todos",
where: "completed = false",
columns: ["id", "title", "completed"]
)
|> Stream.map(fn msg -> msg.value end)
|> Enum.to_list()
end
end
```
```
--------------------------------
### Interruptible Sync Render for Dynamic Queries
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Supports interruptible sync shapes for dynamic queries that may change. The provided function is re-evaluated on interrupt, allowing for real-time updates.
```elixir
def project_tasks(conn, %{"project_id" => project_id} = params) do
sync_render(conn, params, fn ->
# This function is re-evaluated on interrupt
task_ids = MyApp.Projects.get_visible_task_ids(project_id)
from(t in MyApp.Tasks.Task, where: t.id in ^task_ids)
end)
end
```
--------------------------------
### Stream Ecto Query to Elixir Stream
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Use `Phoenix.Sync.Client.stream/2` to convert an Ecto.Query into an Elixir Stream for low-level usage.
```elixir
stream = Phoenix.Sync.Client.stream(Todos.Todo)
stream =
Ecto.Query.from(t in Todos.Todo, where: t.completed == false)
|> Phoenix.Sync.Client.stream()
```
--------------------------------
### Validate Update Permissions Callback
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
A placeholder callback function for validating update permissions before a mutation is applied. Currently, it returns the multi-argument unchanged, indicating no custom validation logic is implemented.
```elixir
defp validate_update_permissions(multi, _changeset, _context) do
multi # Add custom validation logic
end
```
--------------------------------
### Stop Local PostgreSQL Database
Source: https://github.com/electric-sql/phoenix_sync/blob/main/apps/txid_match/README.md
Use this command to stop the PostgreSQL server and release the in-memory RAM disk after testing.
```bash
mix txid.postgres down
```
--------------------------------
### Sync with Transform Function via Router
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Apply a custom transformation function to the data before it's exposed. The function receives the message and a prefix, allowing modification of the data.
```elixir
sync "/transformed-todos", MyApp.Todos.Todo,
transform: {MyAppWeb.Router, :transform_todo, ["[TODO]"]}
```
--------------------------------
### React Integration with useShape Hook
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Utilize the useShape hook from @electric-sql/react for seamless integration of Phoenix.Sync shapes within React components. Handles loading states and errors.
```typescript
import { useShape } from "@electric-sql/react";
function TodoList() {
const { data, isLoading, error } = useShape({
url: `/shapes/todos`,
});
if (isLoading) return Loading...
;
if (error) return Error: {error.message}
;
return (
{data.map((todo) => (
-
{todo.title} - {todo.completed ? "Done" : "Pending"}
))}
);
}
```
--------------------------------
### User-Specific Data Sync Render
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Renders sync shapes for user-specific data by accessing the current user from the connection assigns. Ensures data is scoped to the authenticated user.
```elixir
def my_todos(%{assigns: %{current_user: user}} = conn, params) do
query = from(t in MyApp.Todos.Todo, where: t.user_id == ^user.id)
sync_render(conn, params, query)
end
```
--------------------------------
### Stream Todos with Keyword Shape Definition
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Stream todos using a keyword-based shape definition, specifying the table, where clause, and desired columns. The stream is then mapped to extract the value and converted to a list.
```elixir
Client.stream(
table: "todos",
where: "completed = false",
columns: ["id", "title", "completed"]
)
|> Stream.map(fn msg -> msg.value end)
|> Enum.to_list()
```
--------------------------------
### Consume Shapes with Electric TypeScript Client
Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md
Use the Electric TypeScript client to subscribe to data changes from a specified URL. The callback function is invoked every time the data is updated.
```typescript
import { Shape, ShapeStream } from "@electric-sql/client";
const stream = new ShapeStream({
url: `/shapes/todos`,
});
const shape = new Shape(stream);
// The callback runs every time the data changes.
shape.subscribe((data) => console.log(data));
```
--------------------------------
### Access Data from Phoenix.Sync.Shape
Source: https://context7.com/electric-sql/phoenix_sync/llms.txt
Provides functions to retrieve data from a Shape process as a list, map, or stream. Supports transforming keys and values during retrieval.
```elixir
defmodule MyApp.TodoCache do
alias Phoenix.Sync.Shape
# Get all data as a list
def list_todos do
Shape.to_list(MyApp.TodoShape)
# Returns: [{key, %Todo{}}, ...]
end
# Get all values without keys
def list_todo_values do
Shape.to_list(MyApp.TodoShape, keys: false)
# Returns: [%Todo{}, ...]
end
# Get data as a map
def todos_map do
Shape.to_map(MyApp.TodoShape)
# Returns: %{key => %Todo{}, ...}
end
# Transform keys and values
def todos_by_id do
Shape.to_map(MyApp.TodoShape, fn {_key, %{id: id, title: title}} ->
{id, title}
end)
# Returns: %{1 => "My Todo", 2 => "Another Todo", ...}
end
# Find a specific item
def find_todo_by_title(title) do
Shape.find(MyApp.TodoShape, fn todo ->
todo.title == title
end)
end
# Get a lazy stream
def stream_todos do
Shape.stream(MyApp.TodoShape)
|> Enum.into(%{})
end
end
```