This starter shows a pragmatic, production-friendly pattern that pairs Electric’s read sync with canonical server writes.
Note: This project uses basic TanStack Store today, not TanStackDB. A future version of the project will adopt TanStackDB.
- Read path (client): ElectricSQL HTTP Shapes → TanStack Store for a reactive collection.
- Write path (server): Phoenix 1.8.1 JSON API writes to Postgres. Electric immediately streams the resulting changes to all clients via Shapes (no manual re-fetch).
What’s included
server/— Phoenix API:POST /api/comments(plus optional bulk), Ecto schema + migration forcomments.client/— Vite + React + TypeScript app that subscribes to Electric Shapes and keeps a client store in sync.docker-compose.yml— Postgres (logical replication) + Electric service; optional dev services for server/client.- Dockerfiles —
server/Dockerfile,client/Dockerfilefor dev and prod builds.
Pinned versions
- Phoenix 1.8.1, Ecto 3.13.2, ecto_sql 3.13.2, postgrex 0.21.1
How it fits together
- Electric’s HTTP API is designed for Shape sync (initial + live via offsets). Writes go through your backend; Electric fans out changes to clients.
- TanStack Store gives a simple, reactive in-memory collection with clean update semantics.
Run all services in containers with hot reload and no local Elixir/Node:
# Start infra (Postgres + Electric)
docker compose up -d postgres electric
# Start app services (dev; code is volume-mounted for live reload)
docker compose up --build server client
Open these in your browser:
- Client: http://localhost:5173
- API: http://localhost:4000
- Electric (direct): http://localhost:3000
Notes
- DB is created and migrated automatically by the server dev image on startup.
- CORS is enabled on the server for
http://localhost:5173. - The client is built with
VITE_API_URL=http://localhost:4000andVITE_ELECTRIC_URL=http://localhost:3000, so it talks to Electric directly for shapes. If you prefer to proxy via the API, setVITE_ELECTRIC_URLtohttp://localhost:4000and add a/v1/shapeproxy route.
POST /api/comments— body{ id, post_id, author, body }→ inserts row and returns JSON (withcreated_at).POST /api/comments/bulk— optional batch insert{ comments: [ ... ] }.
If you’d rather run server and client on your host machine:
- Start Postgres + Electric (still via Docker):
docker compose up -d postgres electric
- Server:
cd server
mix deps.get
mix ecto.create
mix ecto.migrate
mix phx.server
- Client:
cd client
cp .env.example .env.local
pnpm i # or npm i / yarn
pnpm dev
- Client:
VITE_ELECTRIC_URL,VITE_API_URL(set inclient/.env.localfor local runs; compose sets them for container dev). - Server:
DATABASE_URLenv (defaults topostgres://postgres:postgres@localhost:5432/electric_devin dev). Binds on0.0.0.0and respectsPORT.
Server (release):
docker build -t myorg/server:prod -f server/Dockerfile --target prod server
Client (static):
docker build -t myorg/client:prod -f client/Dockerfile --target prod client
Run the built images:
docker run -p 4000:4000 -e PHX_SERVER=true -e DATABASE_URL=postgres://postgres:postgres@host.docker.internal:5432/electric_dev myorg/server:prod
docker run -p 8080:80 myorg/client:prod