Skip to content

Repository files navigation

PowerSync Write API

A self-hostable backend for the PowerSync write path: a client uploads its queued local changes to an HTTP API, which persists them to your source database. PowerSync replicates that database back to clients.

Clone it, point it at your own database, and change the code.

Quickstart — see it work

This brings up a complete, self-contained system with nothing for you to configure: a seeded Postgres, the PowerSync service, the write API, and a small demo client. The committed .env already holds everything it needs, including a throwaway signing keypair.

docker compose up --build

Open the demo client, add a todo, and watch it land in Postgres and sync back.

This is a smoke test, not the product. The todo schema, the seed data and the demo client are scaffolding to prove the machinery works before you wire in anything of your own. Everything specific to it lives under examples/ and can be deleted in one go.

Point it at your own database

Edit .env and select Adopter Mode:

COMPOSE_FILE=docker-compose.yaml
DATABASE_TYPE=postgres
DATABASE_URI=postgres://user:password@your-host:5432/your-db

Your database needs preparing first

PowerSync replicates by reading your database's change feed, and every flavour needs that turned on before anything syncs. This is the part that silently produces an empty app if skipped.

Flavour What must be true of your database
Postgres a publication named powersync covering the replicated tables; a user with SELECT on them and replication rights. A Postgres source without a publication replicates nothing.
MongoDB A replica set — change streams and the multi-document transactions the write API uses both require one. Post-images configured (post_images: auto_configure), since change streams alone do not carry the pre-update document.
MySQL log_bin on, gtid_mode=ON, enforce_gtid_consistency=ON, binlog_format=ROW, binlog_row_image=FULL, a unique server-id; a user with REPLICATION SLAVE and SELECT. On managed MySQL these usually live in a parameter group and need a restart.
SQL Server CDC enabled at database level and per replicated table; a CDC-enabled _powersync_checkpoints table; SQL Server Agent running, or CDC captures nothing while appearing enabled; the user needs cdc_reader, VIEW DATABASE PERFORMANCE STATE in the database, and VIEW SERVER PERFORMANCE STATE in master.

Each examples/<flavour>/README.md has the worked SQL and the managed-hosting wrinkles. Those live under examples/, which you are invited to delete — so the table above is the version that survives that, deliberately.

Then describe your own schema in config/service.yaml and config/sync-config.yaml. Those two files are yours from the first minute — no example ever writes to them.

Fill in config/sync-config.yaml before you start. It ships empty, because only you know your schema, and PowerSync will restart in a loop logging 'streams' are required until it has at least one stream. Remember auto_subscribe: true — without it a stream syncs nothing and reports no error anywhere.

If your database runs on this machine rather than in Docker, reach it at host.docker.internal, not localhost — inside a container, localhost is the container.

In Adopter Mode there is no bundled database and no demo client. Bring your own client.

Bucket storage — PowerSync's own internal store — always runs in a container this project owns, in every mode. We never create schemas in a database you merely pointed us at.

Switching modes

Mode selection is the COMPOSE_FILE line in .env, with the alternatives sitting there commented out. The command stays a plain docker compose up, so down, logs and ps behave normally.

.env line What runs
docker-compose.yaml:examples/postgres/compose.yaml Example Mode, Postgres
docker-compose.yaml:examples/mongodb/compose.yaml Example Mode, MongoDB
docker-compose.yaml:examples/mysql/compose.yaml Example Mode, MySQL (Beta)
docker-compose.yaml:examples/mssql/compose.yaml Example Mode, SQL Server (Beta)
docker-compose.yaml Adopter Mode, your database

Only one runs at a time — they share ports, and each has its own Compose project name so switching never reuses the previous flavour's volumes.

Bring the current mode down before switching. Because each mode is its own Compose project, docker compose down only stops the mode currently selected in .env. Edit the line first and the old containers keep running and holding ports, and the new mode fails with Bind for 0.0.0.0:6060 failed: port is already allocated. Down first, then switch.

If you would rather be explicit, the same thing without .env:

docker compose -f docker-compose.yaml -f examples/postgres/compose.yaml up

Layout

write-api/
├── docker-compose.yaml       # Base: PowerSync, bucket storage, write API
├── .env                      # Mode selection and throwaway dev keys
├── config/                   # ADOPTER MODE config — yours to edit
│   ├── service.yaml
│   └── sync-config.yaml
├── docker-compose.dev.yaml   # Overlay: edit backend code without rebuilding
├── examples/                 # Delete this when you no longer need it
│   ├── postgres/             # Seeded Postgres + demo client
│   │   ├── compose.yaml
│   │   ├── powersync/        # This example's PowerSync config
│   │   └── init-scripts/     # Demo schema + seed data
│   ├── mongodb/              # No source container — shares the bucket-storage replica set
│   ├── mysql/                # Binlog config + seeded schema
│   └── mssql/                # CDC bootstrap container + seeded schema
├── backend/                  # The write API (Express, port 6060)
│   └── openapi.yaml          # Shared contract, read by both packages
└── frontend/                 # Demo client (React/Vite) — a test fixture

Changing the backend

Append the development overlay to whichever mode you are in:

COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml:docker-compose.dev.yaml

Your working tree is mounted into the container and the process restarts on save — an edit is serving in about two seconds, with no image rebuild. It works in Adopter Mode too, which is arguably where it matters more: wiring this into your own database is exactly when you are editing src/persistance/ and src/auth/verifier.ts.

Without the overlay, changes ship on rebuild — the deployment-shaped path:

docker compose up --build

The demo client is a Vite app, so its own loop is the usual one, on the host:

cd frontend && pnpm dev

That reads .env.local at runtime, so changing a URL needs no rebuild. In the container the client is a production build with its URLs baked in, which is why it is not part of the overlay.

Auth seams worth knowing: backend/src/auth/verifier.ts is where you swap the demo's tokens for your own identity provider — see auth-verifiers.md for worked examples.

Replacing the throwaway signing keys is one command:

cd backend && pnpm generate-keys      # prints both values for .env

The signing keys in .env are a public throwaway pair, committed so the backend signs consistently across restarts. Replace them before this is anything but a demo.

Generating types from the contract

Both packages generate TypeScript from backend/openapi.yaml:

cd backend && pnpm generate-types   # -> src/generated/api.ts
cd frontend && pnpm generate        # -> src/generated/api.d.ts

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages