When your PowerSync app writes data using SQL INSERT, UPDATE, or DELETE statements,
PowerSync automatically queues these mutations for processing using the uploadData function.
While there are no restrictions on the implementation of uploadData and the related write API,
this repository provides a client connector and two implementations of the write API for most simple use cases.
It provides extension points that will allow you to customize
the behavior to your own requirements, or you can modify it to suit your specific case.
- backend/shared/powersync-reference-write-api.openapi.yaml provides a simple but flexible API definition that serves as a contract between the client connector and either backend.
- A client connector implementing
uploadDatahandles most common workflows. It will marshal queued changes to the format specified by the API definition. A single-file client connector is provided for ease of copy-paste. - The write API (
POST /api/data) accepts queued transactions from the client connector and applies them to your source database. Choose the standalone Express backend, or, for Supabase apps, the Supabase Edge Function, which implements the same write API and enforces your existing RLS policies. Both use the shared core, which contains the transaction processor and adapters for Postgres, MongoDB, MySQL, and SQL Server.
The green blocks are provided by this repository; click them to explore the code. Solid arrows
show the write path, dotted arrows show shared-code dependencies and the existing sync path.
Choose one backend to serve the write API (POST /api/data).
flowchart TB
subgraph app["Your app"]
sdk["PowerSync SDK<br/>Local database and upload queue"]
connector["Client connector<br/>uploadData()"]
sdk -->|Queued local writes| connector
end
subgraph api["Write API"]
express["Express backend<br/>Postgres · MongoDB · MySQL · SQL Server"]
supabase["Supabase Edge Function<br/>Supabase Postgres · RLS"]
end
connector -->|POST /api/data| express
connector -->|POST /api/data| supabase
express -->|Write transactions| db["Your source database"]
supabase -->|Write transactions with RLS| db
express -. Uses .-> core["Shared core<br/>Transaction processor and database adapters"]
supabase -. Uses .-> core
db -. Replicates .-> powersync["Your PowerSync instance"]
powersync -. Syncs .-> sdk
classDef supplied fill:#f0fdf4,stroke:#15803d,color:#14532d,stroke-width:2px
classDef existing fill:#f8fafc,stroke:#64748b,color:#0f172a
class connector,express,supabase,core supplied
class sdk,db,powersync existing
click connector "./example-client/README.md" "Client connector setup"
click express "./backend/standalone/README.md" "Express backend setup"
click supabase "./backend/supabase/README.md" "Supabase Edge Function setup"
click core "./backend/shared/" "Shared core source"
Both backends implement the same write API contract. The shared core runs inside your chosen backend; it is not a separate service.
You supply the database, PowerSync instance, app, and application-specific authorization, and you must copy a client connector into your app.
Run the commands below from the repository root. docker compose runs the Express backend;
local development and tests use Node.js 24 and pnpm 9.
Configure the connection to the source database that your PowerSync instance replicates from.
Set these in .env.local (or the root .env for shared defaults) for Docker Compose:
| Variable | Meaning |
|---|---|
DATABASE_TYPE |
postgres (default), mongodb, mysql, or mssql |
DATABASE_URI |
Required connection string for your source database |
BATCH_ON_FATAL_ERROR |
stop (default) or skip; see error handling |
Connection string notes: percent-encode special characters in passwords. Postgres accepts ?sslmode=require (or verify-full) for managed databases. MSSQL verifies the server certificate by default; add ?trustServerCertificate=true only for self-signed development servers. Missing ports default to 5432 (Postgres) and 1433 (MSSQL). Pools are capped at 10 connections with connect and statement timeouts.
.env is committed, so keep it to shared defaults. Put this machine's values and any secret, such
as a DATABASE_URI with a password, in .env.local. This is gitignored, optional, and
overrides .env. Compose passes both files to the container, so docker compose up needs no extra
flags.
Tables and columns must match uploaded operations unless you add custom schema mapping.
Each table also needs an id primary key or unique column holding the client's row ID; see
row ID.
The default MongoDB mapper also requires collection $jsonSchema validators.
In Docker Desktop, use host.docker.internal to reach a database on the host.
For the Express backend running directly on the host, use the database's host-accessible address,
such as localhost. Other Docker environments may require a host-gateway configuration.
The Express backend accepts tokens signed by keys it trusts, normally the same provider your PowerSync instance trusts,
so your app authenticates with the same provider for read and write.
The default backend/standalone/src/auth/verifier.ts reads a PowerSync Cloud JSON export, resolves its authentication settings, and builds a verifier.
It may need additional trusted settings, such as the expected issuer or audience.
Choose an integration:
- PowerSync Cloud or self-hosted PowerSync: save your config as
backend/standalone/config/powersync-config.json(the default) and set anyAUTH_*supplements; see auth setup. - Any other provider (Clerk, Auth0, Supabase): set
AUTH_MODE=envwith your issuer, audience and JWKS URL, no file needed. See auth setup and the Supabase and Clerk guide. - Trying it out with no provider: use PowerSync development tokens.
docker compose up --buildThe Express backend listens at http://localhost:6060. Compose fixes both the container and host
port to 6060; change its ports mapping to expose a different host port.
Startup checks the database type, connection string, fatal-error policy, and verifier configuration.
Copy a client connector from example-client into your app. Configure
its write API URL, PowerSync instance URL, and token retrieval, then pass it to your existing
PowerSync database's connect() method. The client connector implements:
fetchCredentials(): credentials for the PowerSync sync connection. If your app is already reading data from PowerSync then you will have already implemented this function, we recommend that you keep your existing implementation and configure the Express backend to trust the same issuers - see auth setup.uploadData(): sends queued local transactions toPOST /api/data.
Use a URL reachable from the client device. localhost refers to that device, so another
computer or phone needs the Express backend's network address or a tunnel URL.
- Configure write authorization. The Express backend allows all authenticated writes by default. Add application checks or effective Postgres RLS. For the Supabase Edge Function, verify that grants and RLS policies cover every writable table and operation.
- Replace demo authentication. Integrate your provider's session and token refresh handling instead of using a fixed demo token; see client authentication.
- Decide how rejected writes are recovered. By default, backend-directed fatal failures are logged and removed from the client queue. The dead-letter hook provides no durable storage or guaranteed delivery. Implement recovery appropriate to your application; see error handling.
- Handle blocked uploads. Client-directed failures remain queued and block later uploads.
Implement a user-facing resolution flow through
onFatalTransaction. Review the authorization-failure defaults: Supabase RLS errors are retried, while denials from its optional custom authorizer are removed from the queue by default. - Validate application data and mapping. Review writable tables, fields, ownership, and value validation. The default mapping is not application-specific validation.
- Review conflict and retry behavior. Confirm the documented conflict-resolution behavior suits your app, and make custom writes and side effects safe when an upload is repeated.
The Express backend's default authorizer allows all authenticated writes. Add your application's checks in
backend/standalone/src/auth/authorizer.ts; see
authorization. On the Supabase Edge Function, your RLS policies
authorize each write and the authorizer is optional; see
Supabase authorization. Configure your PowerSync Sync Streams
separately to control which rows each user can read.
Schema mapping describes field conversion and custom writes. Error handling explains rejected transactions, queue retention, and the optional dead-letter callback.
Conflict resolution documents the approach taken in this reference implementation, as well as alternative conflict resolution approaches that can be implemented.
backend/shared/powersync-reference-write-api.openapi.yaml defines the write API (POST /api/data).
Both backends accept 1–50 transactions and process
them in order, each in its own database transaction. Each returns one result per submitted
transaction, including not_attempted for transactions after the stopping point.
Processed batches return HTTP 200; inspect the individual result statuses. Request validation
and authentication failures use HTTP errors such as 400 and 401. Transaction outcomes are
success, retryable_error, fatal_error, or not_attempted.
On the Express backend, BATCH_ON_FATAL_ERROR=skip continues after backend-directed fatal failures. Client-directed
fatal failures and retryable failures stop the batch. See error handling.
For Docker with automatic reloads:
docker compose -f docker-compose.yaml -f docker-compose.dev.yaml up --buildFor local execution, stop the Express backend running in Compose, create backend/.env from
backend/.env.template, and configure it for your host's database address and auth setup.
The local process loads backend/.env; Compose reads the root .env.
pnpm --dir backend install
pnpm --dir backend devThe local port defaults to 6060 and can be changed with PORT in backend/.env.
Rebuild the image after code changes when using Compose without the development overlay.
pnpm --dir backend test
pnpm --dir backend check
pnpm --dir backend generate-types
pnpm --dir backend generate-edge-validatorThese tests cover the shared core and the Express backend. They use local fixtures and mocks;
they need permission to open local listening ports, but no Docker, running database, or live
identity provider. After editing the OpenAPI spec, run both generators: generate-types updates
backend/shared/src/generated/api.ts and example-client/src/generated/api.d.ts, and
generate-edge-validator updates backend/shared/src/generated/transaction-batch-validator.ts,
which the Edge Function uses. A test fails if the validator is stale.
These need Docker, Deno, and the Supabase CLI. Start the local stack and serve the function from
backend/, then run the tests in another terminal:
cd backend
supabase start
supabase functions servecd backend/supabase/functions/powersync-write
deno task testhosted.test.ts runs only against a deployed function and skips unless the
POWERSYNC_TEST_HOSTED_* variables described at the top of that file are set.