From ae3b93085e4f06e8c89efdd761604efb594e083a Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Wed, 16 Sep 2026 16:17:28 +0200 Subject: [PATCH 01/18] docs: record the self-hosting plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design for turning this repo into a starter template someone can clone, point at their own source database, and edit: two run modes selected by one line of configuration, four Example Source Databases, and a development loop that does not require rebuilding an image. Records the rejected Compose layouts with the specific failure each has, and the two load-bearing mechanisms verified against Compose v2.38.2 rather than assumed — overlay volume replacement keyed on mount target, and the Compose file list being read from the environment file. Co-Authored-By: Claude Opus 5 (1M context) --- self-host-plan.md | 238 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 self-host-plan.md diff --git a/self-host-plan.md b/self-host-plan.md new file mode 100644 index 0000000..5420106 --- /dev/null +++ b/self-host-plan.md @@ -0,0 +1,238 @@ +# Self-hosting plan: hand `write-api` to an adopter + +## Goal + +`write-api` becomes a **starter template**. An adopter clones it, points it at their own +source database, runs it, and edits the backend code. The todo app and its seeded tables are +scaffolding — clearly fenced, clearly deletable. + +## Vocabulary + +Used consistently across the READMEs. Not written to `CONTEXT.md` — that stays as-is. + +- **Adopter** — the person we hand this to. Distinct from **Writer**, which already means an + authenticated end user in this codebase. +- **Example Source Database** — the bundled, seeded, throwaway database. +- **Example Mode** / **Adopter Mode** — the two run modes. +- **Source database** — whichever database is actually configured. Plays two roles at once: + PowerSync *replicates from* it, and the write API *persists to* it. +- **Bucket storage** — the PowerSync service's own internal store. Ours, not the adopter's. + +## The two modes + +**Example Mode** — the default, and the README's first command. Zero config, zero credentials. +`docker compose up` yields a working system with a seeded database and the todo frontend. +Framed explicitly as *verify the machinery, then replace it*, so nobody mistakes the todo app +for the product. + +**Adopter Mode** — the real path. No source database container, no frontend. `DATABASE_URI` +must be set; the backend fails loudly with a readable message when it isn't, rather than +defaulting to something surprising. + +## Fixed rule + +**Bucket storage never leaves the docker boundary.** Always MongoDB, always a container we own. +Even where PowerSync would allow sharing an adopter's Postgres 14+ instance (supported from PG14 +onward, blocked below it), we don't. We do not create schemas in someone else's database. + +## Layout + +``` +write-api/ +├── docker-compose.yaml # base: powersync, mongo, mongo-rs-init, backend +├── docker-compose.dev.yaml # overlay: bind-mount backend/, tsx watch +├── .env # COMPOSE_FILE + dev keypair + adopter's DATABASE_URI +├── .env.template +├── README.md # rewritten +├── config/ # ADOPTER MODE config — the adopter's from minute one +│ ├── service.yaml +│ └── sync-config.yaml +├── examples/ +│ ├── postgres/ +│ │ ├── compose.yaml +│ │ ├── README.md +│ │ ├── powersync/{service.yaml,sync-config.yaml} +│ │ └── init-scripts/setup.sql +│ ├── mongodb/ +│ │ ├── compose.yaml +│ │ ├── README.md +│ │ └── powersync/{service.yaml,sync-config.yaml} +│ ├── mysql/ +│ │ ├── compose.yaml +│ │ ├── README.md +│ │ ├── powersync/{service.yaml,sync-config.yaml} +│ │ └── init-scripts/{my.cnf,setup.sql} +│ └── mssql/ +│ ├── compose.yaml +│ ├── README.md +│ ├── powersync/{service.yaml,sync-config.yaml} +│ └── init-scripts/setup.sql +├── backend/ +│ ├── openapi.yaml # moved from repo root +│ └── … +└── frontend/ # test fixture; exists only inside Example Mode +``` + +### Why base + overlays + +The matrix is four flavours x two modes x (dev | built). Overlays compose **multiplicatively**: +five overlay files cover eight combinations, and adding a fifth flavour is one new file. + +Two alternatives were rejected: + +- **One compose file per example** (the `self-host-demo` pattern, using `include:` + `extends:`) + restates the mount and `depends_on` wiring four times, and `include:` cannot override an + included service's `depends_on` — a wall `self-host-demo` hit on its own MSSQL demo, forcing + a copy-paste of the backend service. We would hit it on the identical flavour. +- **One file with Compose profiles** breaks on the config mount: the `powersync` service needs a + different `/config` source per flavour, and a single service definition cannot vary its volumes + by profile. The workaround (templating the path through a variable) leaves two things that must + agree; out of step, it boots the wrong sync rules with no error. + +### Overlay conventions + +- Each example overlay carries a top-level `name:` (`write-api-postgres`, …) so switching + flavours never silently reuses the previous flavour's volumes. +- Flavour-specific values (`DATABASE_TYPE`, `DATABASE_URI`, `PS_DATA_SOURCE_URI`) are literals in + the overlay. They are facts about the flavour, not things a human edits. +- `.env` holds only what a human edits. +- **Relative paths inside an overlay resolve against the project directory** — the directory of + the *first* compose file, i.e. the repo root — not against the overlay's own location. + Verified. So `examples/postgres/compose.yaml` writes `./examples/postgres/powersync:/config`, + not `./powersync:/config`. This reads wrong at a glance and will trip up anyone adding a + flavour; call it out in a comment at the top of each overlay. + +## Switching modes + +One line in `.env`, alternatives sitting there commented out: + +```bash +COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml +# COMPOSE_FILE=docker-compose.yaml:examples/mongodb/compose.yaml +# COMPOSE_FILE=docker-compose.yaml:examples/mysql/compose.yaml +# COMPOSE_FILE=docker-compose.yaml:examples/mssql/compose.yaml +# COMPOSE_FILE=docker-compose.yaml # Adopter Mode +``` + +Append `:docker-compose.dev.yaml` for the watch loop. The command is always plain +`docker compose up`, so `down` / `logs` / `ps` behave exactly as anyone expects. The explicit +`-f` form is documented underneath for anyone who prefers it. + +## The four examples + +| Flavour | Source container | Bootstrap | Connector status | +|---|---|---|---| +| **Postgres** | `postgres:18`, `wal_level=logical` | `setup.sql` ending in `create publication powersync` | GA (11+) | +| **MongoDB** | *none* — reuses the base `mongo` replica set, second database | none; `post_images: auto_configure` | GA (6.0+) | +| **MySQL** | `mysql`, mounted `my.cnf` | `gtid_mode`, `enforce_gtid_consistency`, `binlog_format=ROW`, `binlog_row_image=FULL`, server-id, `REPLICATION SLAVE` user | Beta (5.7+) | +| **SQL Server** | `mssql` + one-shot setup container | `sp_cdc_enable_db`, CDC-enabled `_powersync_checkpoints`, per-table `sp_cdc_enable_table`, `cdc_reader` grant | Beta (2019+) | + +MongoDB is nearly free: the base already runs a Mongo replica set for bucket storage, so the +example points replication at a second database on the same instance. + +All four remain valid `DATABASE_TYPE` values in Adopter Mode regardless — this table is only +about which flavours ship a runnable database. + +## Sync rules: migrate to edition 3 + +Both formats work on `journeyapps/powersync-service:latest`, and `bucket_definitions:` is legacy +rather than deprecated — but whatever we ship is what an adopter copies when writing rules for +their own schema, and upstream docs now describe only `streams:`. + +Three changes that must land together: + +1. `config: {edition: 3}` + `streams:` replacing `bucket_definitions:` +2. `sync_config:` replacing the deprecated `sync_rules:` key in the service config + (specifying both is a hard error) +3. `auto_subscribe: true` on **every** stream + +File renames following upstream: `config/powersync.yaml` -> `config/service.yaml`, +`config/sync_rules.yaml` -> `config/sync-config.yaml`. + +MongoDB's rules differ by necessity, because `_id` is the primary key: + +```yaml +queries: + - select _id as id, * from lists + - select _id as id, * from todos +``` + +**Gotcha:** `auto_subscribe` defaults to `false`. Omit it and the client syncs nothing, with no +error. Client-side subscription is new in edition 3; the legacy format has no equivalent. + +No frontend code changes needed: `@powersync/web` is at `^1.38.7`, past the 1.32.0 threshold +where the Rust sync client became the default. + +## Auth and keys + +A fixed throwaway keypair committed in `.env`, commented as public and replaceable, plus a +`pnpm generate-keys` script wrapping the existing `src/utils/generate-key.ts`. + +Rationale: `src/api/auth.ts` generates a temporary keypair at boot when +`POWERSYNC_PRIVATE_KEY` is unset. Under `tsx watch` the backend restarts on every save, so every +save mints a new signing key and silently 401s every token already issued — failures that look +like a bug in the adopter's own change. + +`PS_JWKS_URL` becomes `http://backend:6060/api/auth/keys`. The backend is inside compose now, so +`host.docker.internal` goes away entirely. + +Frontend `VITE_*` build args stay on `localhost` ports — they resolve in the browser, not on the +compose network. + +## `openapi.yaml` moves into `backend/` + +The containerised backend currently **boots but serves nothing**: `backend/app.ts` reads +`path.join(__dirname, '..', 'openapi.yaml')`, but `backend/Dockerfile`'s build context is +`backend/`, so the spec is never copied in. The validator loads the spec lazily, so the container +starts cleanly and then answers every request with +`500 openapi.validator: spec could not be read at /openapi.yaml`. + +Four edits: + +- `backend/app.ts:27` — `path.join(__dirname, '..', 'openapi.yaml')` -> `path.join(__dirname, 'openapi.yaml')` +- `backend/package.json:13` — `../openapi.yaml` -> `./openapi.yaml` +- `frontend/package.json:6` — `../openapi.yaml` -> `../backend/openapi.yaml` +- `README.md:41` — wording + +## Dev loop + +`docker-compose.dev.yaml` bind-mounts `backend/` and runs `tsx watch`. It touches the backend +only, because that is the code adopters change, and because an adopter in Adopter Mode has no +frontend service for a combined overlay to reference. + +It applies to **both** modes. Adopter Mode + dev is arguably the primary case: someone wiring the +write API into their own database is exactly who is editing `src/persistance/` and +`src/auth/verifier.ts`. + +The frontend's dev mode (`vite dev` against a bind-mount, with HMR and runtime `.env` reading +instead of baked-in build args) folds into the example overlays, where a frontend exists. + +A full `docker compose build` path remains as the deploy-shaped reference. + +## Deliverables + +1. Rewritten root `README.md` — Example Mode quickstart, then Adopter Mode setup, then the + code-change loop. +2. Four per-example READMEs covering each flavour's prerequisites and gotchas. + +Explicitly **not** in scope: `CONTEXT.md` updates, ADRs, `.scratch/` issues. The service image +stays on `:latest`. + +## Risks + +1. ~~The config mount override is load-bearing.~~ **Verified** on Compose v2.38.2. An overlay + declaring `./examples/postgres/powersync:/config` *replaces* the base's `./config:/config` — + the resolved config has exactly one bind at `/config`, pointing at the example. Volumes merge + keyed by target path, as hoped. No fallback needed. +2. ~~`COMPOSE_FILE` read from `.env`.~~ **Verified.** With + `COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml` in `.env`, a bare + `docker compose config` resolves the merged stack and picks up the overlay's `name:` + (`write-api-postgres`). Setting it to `docker-compose.yaml` alone yields Adopter Mode with the + `./config` mount and the default project name. No fallback needed. +4. **Four examples means four sets of sync rules** that must move together whenever the demo + schema changes. Ongoing drift we are signing up for. +5. **MySQL and SQL Server are Beta connectors.** On SQL Server, schema changes are *not* picked + up automatically — every table change needs a redeployed sync config. A real footgun for a + repo whose point is swapping in your own schema. Goes in `examples/mssql/README.md`. +6. **`:latest` drift.** A clone six months from now gets a service version we never tested + against. Accepted deliberately. From 620fdaf0032f76cbc87d4a9f460138b0652cc5e2 Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Wed, 16 Sep 2026 16:17:28 +0200 Subject: [PATCH 02/18] fix: make the containerised backend serve requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend image booted, logged that it was running, and then answered every request with 500 "openapi.validator: spec could not be read at /openapi.yaml". The contract it validates against lived at the repo root, outside the backend's Docker build context, so it was never copied into the image — and the validator loads it lazily on first request, so nothing failed at startup. A container that looked healthy and served nothing. Move the contract beside app.ts, inside the build context. Both packages still generate their types from that one file, so the API stays single-sourced. Two further defects surfaced while fixing it: The backend had no .dockerignore, so COPY / ./ copied the host's node_modules over the ones installed in the image — macOS-native binaries into a Linux container — along with local env files and logs. The root route was unreachable. It is registered after the OpenAPI validator, which rejects any path absent from the spec, so it 404'd even on a machine where the spec resolved. It now has an explicit exemption alongside the auth routes, which also gives the backend a liveness probe for Compose to depend on. Adds the first test at the existing HTTP seam, covering assembly rather than any one route: if the contract cannot be resolved, every test here fails at once. Verified against a built image — root route 200, token and JWKS endpoints responding, a contract-violating request rejected with 400 rather than falling through to the auth gate, and the image's node_modules the Linux ones. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 4 +++- backend/.dockerignore | 17 ++++++++++++++ backend/README.md | 3 ++- backend/app.test.ts | 33 ++++++++++++++++++++++++++++ backend/app.ts | 4 ++-- openapi.yaml => backend/openapi.yaml | 0 backend/package.json | 2 +- frontend/package.json | 2 +- 8 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 backend/.dockerignore create mode 100644 backend/app.test.ts rename openapi.yaml => backend/openapi.yaml (100%) diff --git a/README.md b/README.md index 597db39..ec6ffd0 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,9 @@ cd frontend && pnpm install && pnpm dev ## Generating Types from OpenAPI Spec -Both the backend and frontend generate TypeScript types from the shared `openapi.yaml` spec. +Both the backend and frontend generate TypeScript types from the shared contract at +`backend/openapi.yaml`. It lives inside the backend so that the backend's Docker build context +contains it; the frontend reads it across the directory boundary. ```bash # Backend (generates src/generated/api.ts) diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..3737519 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,17 @@ +# Without this, `COPY / ./` overwrites the node_modules installed in the image with the host's — +# macOS-native binaries landing in a Linux container. +node_modules + +# Secrets and local state must never reach the image. +.env +.envrc + +# Build and run artefacts. +*.tsbuildinfo +*.log +dist + +# Not needed at runtime. +Dockerfile +.dockerignore +*.test.ts diff --git a/backend/README.md b/backend/README.md index 7caf29e..d1ec62a 100644 --- a/backend/README.md +++ b/backend/README.md @@ -25,7 +25,8 @@ The endpoints are as follows: ### Error classification -Every failure is sorted into one of two kinds, in `src/persistance/classify-error.ts`: +Every failure is sorted into one of two kinds. `src/errors.ts` defines the two, and each +flavour maps its driver's errors onto them in `src/persistance//-errors.ts`: - **retryable** — the environment misbehaved (deadlock, lock timeout, connection loss, resource exhaustion). The client uploads the transaction again after a delay. - **fatal** — the data is wrong and can never be stored (missing required field, constraint violation, malformed or out-of-range value, schema mismatch). The client discards the transaction. diff --git a/backend/app.test.ts b/backend/app.test.ts new file mode 100644 index 0000000..64769d2 --- /dev/null +++ b/backend/app.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; +import request from 'supertest'; + +import app from './app.js'; + +/** + * Seam: HTTP requests against the assembled application. + * + * These cover the assembly itself rather than any one route. The OpenAPI contract is loaded while + * the app is being constructed, so if it cannot be resolved the validator throws on import and + * every test here fails at once — which is the point. The backend image shipped a build context + * that did not contain the contract, so the container started and died; this is the regression + * net for that. + */ +describe('the assembled application', () => { + it('serves its root route', async () => { + const response = await request(app).get('/'); + + expect(response.status).toBe(200); + }); + + it('rejects a request that violates the OpenAPI contract', async () => { + // An empty batch violates minItems on transactions. A bearer token is supplied so the + // request gets past the security check and fails on the body — a 401 here would mean the + // contract never loaded and the request fell through to the auth gate instead. + const response = await request(app) + .post('/api/data') + .set('Authorization', 'Bearer not-a-real-token') + .send({ transactions: [] }); + + expect(response.status).toBe(400); + }); +}); diff --git a/backend/app.ts b/backend/app.ts index 8d83666..74b853e 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -24,10 +24,10 @@ app.use((req: Request, res: Response, next: NextFunction) => { app.use( OpenApiValidator.middleware({ - apiSpec: path.join(__dirname, '..', 'openapi.yaml'), + apiSpec: path.join(__dirname, 'openapi.yaml'), validateRequests: true, validateResponses: false, - ignorePaths: /^\/api\/auth(\/|$)/ + ignorePaths: /^\/$|^\/api\/auth(\/|$)/ }) ); diff --git a/openapi.yaml b/backend/openapi.yaml similarity index 100% rename from openapi.yaml rename to backend/openapi.yaml diff --git a/backend/package.json b/backend/package.json index 896f61a..e9f6b4d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,7 +10,7 @@ "format": "prettier --write .", "check": "tsc -b", "test": "vitest run", - "generate-types": "openapi-typescript ../openapi.yaml -o src/generated/api.ts" + "generate-types": "openapi-typescript ./openapi.yaml -o src/generated/api.ts" }, "dependencies": { "dotenv": "^16.3.1", diff --git a/frontend/package.json b/frontend/package.json index ce07029..3e274ec 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,7 +3,7 @@ "version": "0.2.3", "private": true, "scripts": { - "generate": "openapi-typescript ../openapi.yaml -o src/generated/api.d.ts", + "generate": "openapi-typescript ../backend/openapi.yaml -o src/generated/api.d.ts", "dev": "vite", "build": "tsc -b && vite build", "preview": "vite preview", From dcfde32f24260047f79d2427fff4a35caaf21dd5 Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Wed, 16 Sep 2026 16:22:16 +0200 Subject: [PATCH 03/18] feat: move sync rules to the compiled-streams format Migrate from legacy bucket definitions to the edition 3 streams format, and rename the sync file to match. The service configuration now references it under sync_config; the deprecated sync_rules key is gone, since specifying both is a hard error. The legacy format still works and is not deprecated, so this is not forced. The reason to move is that whatever ships here is what an adopter copies when writing rules for their own schema, and upstream documentation now describes only the new shape. auto_subscribe is set explicitly on the stream. It defaults to false, and the legacy format had no equivalent concept, so omitting it syncs nothing while reporting no error anywhere. Verified end to end: the service accepted the config and replicated both tables, a sync stream opened with no explicit subscription reported the stream as default and delivered all seeded rows, and a write through the API came back through sync. Co-Authored-By: Claude Opus 5 (1M context) --- config/powersync.yaml | 4 ++-- config/sync-config.yaml | 16 ++++++++++++++++ config/sync_rules.yaml | 5 ----- 3 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 config/sync-config.yaml delete mode 100644 config/sync_rules.yaml diff --git a/config/powersync.yaml b/config/powersync.yaml index 36729ab..27b065f 100644 --- a/config/powersync.yaml +++ b/config/powersync.yaml @@ -13,8 +13,8 @@ storage: port: !env PS_PORT -sync_rules: - path: sync_rules.yaml +sync_config: + path: sync-config.yaml client_auth: jwks_uri: !env PS_JWKS_URL diff --git a/config/sync-config.yaml b/config/sync-config.yaml new file mode 100644 index 0000000..e6f0e49 --- /dev/null +++ b/config/sync-config.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://unpkg.com/@powersync/service-sync-rules@latest/schema/sync_rules.json + +config: + # The compiled-streams edition. Omitting this selects edition 1, the legacy compiler, which + # would reject the `streams` block below. + edition: 3 + +streams: + global: + # Streams do not subscribe by default — omit this and the client syncs nothing, with no error + # reported anywhere. The legacy bucket_definitions format had no equivalent concept, so this + # line is what preserves the old sync-everything-upfront behaviour. + auto_subscribe: true + queries: + - SELECT * FROM lists + - SELECT * FROM todos diff --git a/config/sync_rules.yaml b/config/sync_rules.yaml deleted file mode 100644 index 558aebe..0000000 --- a/config/sync_rules.yaml +++ /dev/null @@ -1,5 +0,0 @@ -bucket_definitions: - global: - data: - - SELECT * FROM lists - - SELECT * FROM todos From abd034dd92a0cd8ceb9e4f4239433809a00a9ddc Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Wed, 16 Sep 2026 16:26:57 +0200 Subject: [PATCH 04/18] fix: address code review on the contract move and streams migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standards axis: The test-file comment claimed the validator resolves the contract at construction. It does not — it resolves lazily on the first request, which is precisely why the container booted cleanly and only then failed everything. Corrected, and the claim that both tests are a regression net is now stated as verified rather than asserted: spec resolution runs before the ignorePaths check, so removing the contract fails both tests, including the exempted root route. Confirmed by deleting the file and watching them go red. The ignorePaths regex packed two unrelated exemptions into one opaque literal with no explanation; commented. .dockerignore used *.test.ts, which in Docker's matching does not cross a path separator, so any future src/**/*.test.ts would still have shipped. backend/README.md coined "flavour" as a domain term while the glossary is deliberately untouched; reworded. Test prose used bare "batch", which the glossary lists under Transaction's avoid-list. Spec axis: The README project layout still listed sync_rules.yaml, renamed in the previous commit — and had been stale for longer than that, still naming the backend and frontend by directory names they no longer use. self-host-plan.md described the service.yaml rename as part of the streams migration when it belongs with the directory restructure, and claimed .scratch issues were out of scope when the work is in fact tracked there. Two review findings were checked and rejected. The root route is not the only path outside the contract — only /api/data is declared, so the auth routes have always been exempt too, and a liveness probe follows that precedent. The sync config's editor schema URL resolves and is the path upstream ships on its own edition 3 files. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 13 +++++++------ backend/.dockerignore | 2 +- backend/README.md | 3 ++- backend/app.test.ts | 17 +++++++++-------- backend/app.ts | 2 ++ self-host-plan.md | 12 ++++++++---- 6 files changed, 29 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index ec6ffd0..3813263 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,16 @@ ``` write-api/ -├── docker-compose.yaml # Postgres + MongoDB + PowerSync +├── docker-compose.yaml # Postgres + MongoDB + PowerSync ├── config/ -│ ├── powersync.yaml # PowerSync service config -│ └── sync_rules.yaml # Sync rules (lists + todos) +│ ├── powersync.yaml # PowerSync service config +│ └── sync-config.yaml # Sync rules (lists + todos) ├── init-scripts/ -│ └── setup.sql # DB schema + seed data -├── powersync-nodejs-backend-todolist-demo/ # Backend (Express, port 6060) +│ └── setup.sql # DB schema + seed data +├── backend/ # Backend (Express, port 6060) +│ ├── openapi.yaml # Shared API contract, read by both packages │ └── .env -└── demo-app/ # Frontend (React/Vite, port 5173) +└── frontend/ # Demo client (React/Vite, port 5173) └── .env.local ``` diff --git a/backend/.dockerignore b/backend/.dockerignore index 3737519..3fd4213 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -14,4 +14,4 @@ dist # Not needed at runtime. Dockerfile .dockerignore -*.test.ts +**/*.test.ts diff --git a/backend/README.md b/backend/README.md index d1ec62a..1cdf1b0 100644 --- a/backend/README.md +++ b/backend/README.md @@ -26,7 +26,8 @@ The endpoints are as follows: ### Error classification Every failure is sorted into one of two kinds. `src/errors.ts` defines the two, and each -flavour maps its driver's errors onto them in `src/persistance//-errors.ts`: +supported database maps its driver's errors onto them in its own `*-errors.ts` under +`src/persistance/`: - **retryable** — the environment misbehaved (deadlock, lock timeout, connection loss, resource exhaustion). The client uploads the transaction again after a delay. - **fatal** — the data is wrong and can never be stored (missing required field, constraint violation, malformed or out-of-range value, schema mismatch). The client discards the transaction. diff --git a/backend/app.test.ts b/backend/app.test.ts index 64769d2..56c94ed 100644 --- a/backend/app.test.ts +++ b/backend/app.test.ts @@ -6,11 +6,12 @@ import app from './app.js'; /** * Seam: HTTP requests against the assembled application. * - * These cover the assembly itself rather than any one route. The OpenAPI contract is loaded while - * the app is being constructed, so if it cannot be resolved the validator throws on import and - * every test here fails at once — which is the point. The backend image shipped a build context - * that did not contain the contract, so the container started and died; this is the regression - * net for that. + * These cover the assembly itself rather than any one route. The validator resolves the OpenAPI + * contract lazily, on the first request rather than at construction — which is why the backend + * image booted, reported itself running, and only then answered everything with a 500. Spec + * resolution happens before the ignorePaths check, so an unreadable contract fails every request + * including the exempted ones: remove the contract and both tests below fail, which is what makes + * them the regression net for that bug. Verified by deleting it and watching them go red. */ describe('the assembled application', () => { it('serves its root route', async () => { @@ -20,9 +21,9 @@ describe('the assembled application', () => { }); it('rejects a request that violates the OpenAPI contract', async () => { - // An empty batch violates minItems on transactions. A bearer token is supplied so the - // request gets past the security check and fails on the body — a 401 here would mean the - // contract never loaded and the request fell through to the auth gate instead. + // A Transaction Batch carrying no transactions violates minItems. A bearer token is supplied + // so the request gets past the security check and fails on the body — a 401 here would mean + // the contract never loaded and the request fell through to the auth gate instead. const response = await request(app) .post('/api/data') .set('Authorization', 'Bearer not-a-real-token') diff --git a/backend/app.ts b/backend/app.ts index 74b853e..ec6f13e 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -27,6 +27,8 @@ app.use( apiSpec: path.join(__dirname, 'openapi.yaml'), validateRequests: true, validateResponses: false, + // Two exemptions: the root route, which is the liveness probe and deliberately absent from + // the contract, and the auth routes, which predate it. Everything else is validated. ignorePaths: /^\/$|^\/api\/auth(\/|$)/ }) ); diff --git a/self-host-plan.md b/self-host-plan.md index 5420106..557232d 100644 --- a/self-host-plan.md +++ b/self-host-plan.md @@ -146,8 +146,9 @@ Three changes that must land together: (specifying both is a hard error) 3. `auto_subscribe: true` on **every** stream -File renames following upstream: `config/powersync.yaml` -> `config/service.yaml`, -`config/sync_rules.yaml` -> `config/sync-config.yaml`. +File renames following upstream: `config/sync_rules.yaml` -> `config/sync-config.yaml` lands with +this migration. `config/powersync.yaml` -> `config/service.yaml` lands later, with the directory +restructure, since the mount path changes at the same time. MongoDB's rules differ by necessity, because `_id` is the primary key: @@ -215,8 +216,11 @@ A full `docker compose build` path remains as the deploy-shaped reference. code-change loop. 2. Four per-example READMEs covering each flavour's prerequisites and gotchas. -Explicitly **not** in scope: `CONTEXT.md` updates, ADRs, `.scratch/` issues. The service image -stays on `:latest`. +Explicitly **not** in scope: `CONTEXT.md` updates and ADRs. The service image stays on `:latest`. + +The work is tracked as tickets under `.scratch/self-host/`, alongside the spec, per the repo's +local issue-tracker convention. That directory is gitignored, so this plan is the only part of the +design that lives in version control. ## Risks From dd068d50f8bad28f07de44467cebe5fa6a9c3d3f Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Wed, 16 Sep 2026 16:35:46 +0200 Subject: [PATCH 05/18] feat: Example Mode on Postgres MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One command from a fresh clone now brings up a complete working system with no configuration and no credentials: seeded Postgres, bucket storage, the write API built from local source, and the demo client. Splits compose into a base definition plus overlays. The base is everything both modes share and is Adopter Mode on its own; examples/postgres/compose.yaml adds the source database, the demo client, and its own PowerSync config. Mode selection is the COMPOSE_FILE line in .env, so the command stays a plain docker compose up and every other subcommand behaves normally. Two pieces of wiring change now that the backend is containerised. PowerSync reaches it by service name rather than through the host, and gates startup on its healthcheck — which works only because ticket 01 made the root route reachable, so that exemption is now load-bearing for startup ordering. The signing keypair is committed as a public throwaway. Without it the backend mints a new key on every restart and PowerSync rejects tokens it just accepted with PSYNC_S2101; that failure was hit for real while verifying the previous commit. Tested directly here: after restarting the backend, both a token issued before the restart and a freshly issued one are accepted, with no PowerSync restart. config/service.yaml and config/sync-config.yaml are pulled forward from the next ticket. The base mounts ./config, and moving the demo config into examples/ left that path empty, so Compose would have created an empty directory and PowerSync would have died looking for /config/service.yaml. Verified from a down -v: all services healthy, seeded rows delivered through a sync stream with no explicit subscription, and a write round-tripping through Postgres and back. Co-Authored-By: Claude Opus 5 (1M context) --- .env | 22 +++ README.md | 132 +++++++++++++----- config/service.yaml | 37 +++++ config/sync-config.yaml | 24 ++-- docker-compose.yaml | 80 ++++++----- examples/postgres/README.md | 44 ++++++ examples/postgres/compose.yaml | 71 ++++++++++ .../postgres/init-scripts}/setup.sql | 0 .../postgres/powersync/service.yaml | 0 examples/postgres/powersync/sync-config.yaml | 16 +++ 10 files changed, 349 insertions(+), 77 deletions(-) create mode 100644 .env create mode 100644 config/service.yaml create mode 100644 examples/postgres/README.md create mode 100644 examples/postgres/compose.yaml rename {init-scripts => examples/postgres/init-scripts}/setup.sql (100%) rename config/powersync.yaml => examples/postgres/powersync/service.yaml (100%) create mode 100644 examples/postgres/powersync/sync-config.yaml diff --git a/.env b/.env new file mode 100644 index 0000000..1dc85c3 --- /dev/null +++ b/.env @@ -0,0 +1,22 @@ +# Selects the run mode. Uncomment exactly one. +# +# The command is always a plain `docker compose up` — switching modes is this one line, so every +# other compose subcommand keeps working the way you expect. +COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml +# COMPOSE_FILE=docker-compose.yaml # Adopter Mode — your own database + +# Adopter Mode only. Example Mode overrides both of these with the bundled database. +# DATABASE_TYPE=postgres +# DATABASE_URI= + +# --------------------------------------------------------------------------------------------- +# THROWAWAY SIGNING KEYS — PUBLIC, COMMITTED, AND KNOWN TO EVERYONE WHO HAS CLONED THIS REPO. +# +# They are here so the backend signs with a stable key across restarts. Without that, every +# restart mints a new key and PowerSync rejects tokens it just accepted with +# "PSYNC_S2101 — Could not find an appropriate key in the keystore". +# +# Replace them before this is anything other than a demo: `cd backend && pnpm generate-keys`. +# --------------------------------------------------------------------------------------------- +POWERSYNC_PRIVATE_KEY=eyJrdHkiOiJSU0EiLCJuIjoiNnJPRTVEdWRnbm5TYVh0NlZoVDNUQkpzRzNRdFVFQjA2N3dNZ29ocWstc3pvN3J3bVhOdTctdVJ4REM1OGdHYjBTVTM5R0tmNjVXMTlyVDhBSGVYNXg5bS1IbFUzZTdZMnYtanJLOW5OaGxlTUpfY3NzdElKdFJaaDlsYUJVb2ExejBtRVkzaHBMei1oRU1HNG5XQ3dZV1Q3SzZsR3lVYTFHOTRRSnYxaG5kZVkydmlGMWpMRHVERE1EckhwWkM4NEo3TWgxMkFpVXBUTkwxcWY0ckNoWEE2YXBWOW5DWmRzMERqcWhGUmFEWmhRUkR1MEJVZ1FRSFhkQWQ5X3hrX1JxYi1DQVVHX2tsSmVSRXlWU2p1LUV4M2tJWWlGMjFhSFNOWDFRN3c2amlKUWFnN25Bc2NmelEySVEwSFp0aGQ0Q3lTUzhHbE1RMFkwUFIyM09IYjBRIiwiZSI6IkFRQUIiLCJkIjoiSlFCQVJ5c08zZThPdVFwNmN1X0RQUDc2aENtQXExSS1ISnY4N09kTXhoMGlld0dSeE45cDhmRVZmZlNnbkFLYzZoQVFEanN1TXhuYkloWE9WTlNGNGk0Vk1iOFBIaDMxbWpFTFFNSTJaMVVBZ0hIemZVeUhCM2dhMVV2eTRUcVptSzFQUHgwN0labWFGb2Zxb2ZFY3VCMnpBSEZZSGp0dlMyWjNjdGdqa1JzYlBwbVloUWFoNUJWTFhlSmFEbTJHcTM3blBHZFRfa1pPN01KeVNRckVMTmxOMDZMNmJfMWk5RHRHc2d2WG5ZRmhWOUVPXzBpUk9BOTFDcTJCZzRHMHkzUF8yX0ZQMjRZNW9tQ2NDWGVTR0xrVmZsMVZBWmlhaXltTHVva05UZ3lnVnpYZHdYMnFKeGIwbk9kMmJCUHBvSm15Mnk2dmUwVGpoUzNOV2dCVzhRIiwicCI6Ii1LVWhXQW1RY2FxNFp4cEhWYnRlVmloREV4OU1TZm5kM0Q2TmZMUmYxMy1uZ192VE1JdHJkM2gyRjBuMjVTNmpLckhLQmlJUkNaOW9zcFdfSElCR1BwcGxYalIyaWI5R3BSdkdHeG4tMm5IM3pzcVZ3MjA1ODVqRWFkSElLa3hscnlIRks4b2xlSVc1ZUpzN0VkOU9ZOWxvS21LenU5emFyRUx4ZWpVLUFzOCIsInEiOiI4YVRNbDhQZUxoeTRSOG5rTHY4ZWpWajUxZDQ5elhiaS1GS2ZXZ2l3U2hRRTU2cXpTTHJuMG5kOXQ3cFVRdlpCUnk0d2NMLXJFMWhXNlZtRERJaU9hVTNKQm1mLVk0NVljVFYtcXNENDFXSjVMUC1fdVBydmFHZnNsYlJFVFZRaks3SWVOTTJfc0wyd1hhX3dtY1B3SGNyU2w0OXJ6U0lqTVBCTHE2X3NYMTgiLCJkcCI6InZxR3NETmxZV3kxdXItMmYzNFVGOEx4eG9JbVFiZThhUUNfZFBrejBaajVDNnBmNTlQQVBkc3R1anJCd2tJblBJMzZueTBmM0ZBLVpyOEhMZ2toLWtxVEJMeGEtQXlJWlFhRW5vOE9zZDBLRm9aQUVmbzZScmNma1h0VXR4X0JHelp6d2xJQXBkbHZnTlMyZWZqZGMzSVRrcmdwNmpuX25UOGNMYUl6RmZGVSIsImRxIjoiRXVzdkpYYXRUM2pxS0p5eTQ4Y1Bra3QwQ18yQll6TzZvMng4azJUNHdHUC0ybEJ3QnZLek1iUXZRSkl6QktjWkIwU2pnRUJSV1l0aUNwVDZnS0cwWEtROVotWC1jYmIwVDdDN2dRem9yblF1UG9xcmJRVWdkMUVqb2JqaVhCZUpSV09Gbi1hMzZsTl9tbVlxOVM3MF9yQWhlc0k4MDJ1bnk2NFVqcFdRY1FzIiwicWkiOiJmeV9GT2c4QzFvZG01c1lOejJkSEN4X0JtODdxYXNueG5WUXZYWWlZeXFHSlZrVy1ybkYtVHI4dl95UDZnWTBLNkFwQ1VrOV9yQlVVUEppU2pVcVJhMk0zaUlCQm9LUXVHM0lXQXRKOHc2RThRMVg1Z19kNlRXTnhxYmxRVlY4My11Szhtd25PNUpETzM3NXFCeVRaZkpWUUExSHVHV2ZEUld0cnlNSlNOdG8iLCJhbGciOiJSUzI1NiIsImtpZCI6InBvd2Vyc3luYy0yMTIyNjM5NzY1In0= +POWERSYNC_PUBLIC_KEY=eyJrdHkiOiJSU0EiLCJuIjoiNnJPRTVEdWRnbm5TYVh0NlZoVDNUQkpzRzNRdFVFQjA2N3dNZ29ocWstc3pvN3J3bVhOdTctdVJ4REM1OGdHYjBTVTM5R0tmNjVXMTlyVDhBSGVYNXg5bS1IbFUzZTdZMnYtanJLOW5OaGxlTUpfY3NzdElKdFJaaDlsYUJVb2ExejBtRVkzaHBMei1oRU1HNG5XQ3dZV1Q3SzZsR3lVYTFHOTRRSnYxaG5kZVkydmlGMWpMRHVERE1EckhwWkM4NEo3TWgxMkFpVXBUTkwxcWY0ckNoWEE2YXBWOW5DWmRzMERqcWhGUmFEWmhRUkR1MEJVZ1FRSFhkQWQ5X3hrX1JxYi1DQVVHX2tsSmVSRXlWU2p1LUV4M2tJWWlGMjFhSFNOWDFRN3c2amlKUWFnN25Bc2NmelEySVEwSFp0aGQ0Q3lTUzhHbE1RMFkwUFIyM09IYjBRIiwiZSI6IkFRQUIiLCJhbGciOiJSUzI1NiIsImtpZCI6InBvd2Vyc3luYy0yMTIyNjM5NzY1In0= diff --git a/README.md b/README.md index 3813263..66fa915 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,116 @@ -# PowerSync Write API Demo +# 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. ![Architecture diagram](./diagram.png) -## Project Layout +## Quickstart — see it work +This brings up a complete, self-contained system with **no configuration and no credentials**: a +seeded Postgres, the PowerSync service, the write API, and a small demo client. + +```bash +docker compose up --build ``` -write-api/ -├── docker-compose.yaml # Postgres + MongoDB + PowerSync -├── config/ -│ ├── powersync.yaml # PowerSync service config -│ └── sync-config.yaml # Sync rules (lists + todos) -├── init-scripts/ -│ └── setup.sql # DB schema + seed data -├── backend/ # Backend (Express, port 6060) -│ ├── openapi.yaml # Shared API contract, read by both packages -│ └── .env -└── frontend/ # Demo client (React/Vite, port 5173) - └── .env.local -``` -## Running Everything +- Demo client: http://localhost:5173 +- Write API: http://localhost:6060 +- PowerSync: http://localhost:8080 +- Example Postgres: localhost:5432 + +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: ```bash -# 1. Start infrastructure (Postgres, Mongo, PowerSync) -docker compose up -d +COMPOSE_FILE=docker-compose.yaml +DATABASE_TYPE=postgres +DATABASE_URI=postgres://user:password@your-host:5432/your-db +``` + +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. + +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. -# 2. Start backend (new terminal) -cd backend && pnpm install && pnpm start +| `.env` line | What runs | +| --- | --- | +| `docker-compose.yaml:examples/postgres/compose.yaml` | Example Mode, Postgres | +| `docker-compose.yaml` | Adopter Mode, your database | -# 3. Start frontend (new terminal) -cd frontend && pnpm install && pnpm dev +If you would rather be explicit, the same thing without `.env`: + +```bash +docker compose -f docker-compose.yaml -f examples/postgres/compose.yaml up ``` -- Frontend: http://localhost:5173 -- Backend: http://localhost:6060 -- PowerSync: http://localhost:8080 -- Postgres: localhost:5432 +## Layout -## Generating Types from OpenAPI Spec +``` +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 +├── examples/ # Delete this when you no longer need it +│ └── postgres/ +│ ├── compose.yaml # Seeded Postgres + demo client +│ ├── powersync/ # This example's PowerSync config +│ └── init-scripts/ # Demo schema + seed data +├── 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 -Both the backend and frontend generate TypeScript types from the shared contract at -`backend/openapi.yaml`. It lives inside the backend so that the backend's Docker build context -contains it; the frontend reads it across the directory boundary. +The backend is built from local source, so your changes ship when you rebuild: ```bash -# Backend (generates src/generated/api.ts) -cd backend && pnpm generate-types +docker compose up --build +``` + +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](./auth-verifiers.md) for worked examples. -# Frontend (generates src/generated/api.d.ts) -cd frontend && pnpm generate +> 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`: + +```bash +cd backend && pnpm generate-types # -> src/generated/api.ts +cd frontend && pnpm generate # -> src/generated/api.d.ts ``` + +## Troubleshooting + +**`ports are not available: ... 6060: bind: address already in use`** — something on the host is +already using the port, commonly a backend started with `pnpm start`. Stop it; the containerised +backend needs 6060. + +**Sync config changes do nothing** — the service reads them at boot. `docker compose restart powersync`. + +**Schema or seed changes do nothing** — init scripts only run on a database's first start. +`docker compose down -v` to drop the volume, then up again. diff --git a/config/service.yaml b/config/service.yaml new file mode 100644 index 0000000..ce5f086 --- /dev/null +++ b/config/service.yaml @@ -0,0 +1,37 @@ +# yaml-language-server: $schema=https://unpkg.com/@powersync/service-schema@latest/json-schema/powersync-config.json + +# Adopter Mode: this file is YOURS. Edit it to describe your own source database. +# +# Example Mode does not use it — each example mounts its own config over this one. Only variables +# prefixed PS_ can be substituted with !env. + +telemetry: + disable_telemetry_sharing: false + +replication: + connections: + # One of: postgresql, mongodb, mysql, mssql. Only the first connection of a given type is used. + - type: postgresql + uri: !env PS_DATA_SOURCE_URI + # 'disable' is fine on a private network. Use verify-full for anything reachable publicly. + # sslmode is a Postgres-only option; MySQL and SQL Server configure TLS differently. + sslmode: disable + +storage: + # Bucket storage is PowerSync's own internal store, not your data. It stays in a container this + # project owns — we do not create schemas in your database. + type: mongodb + uri: !env PS_MONGO_URI + +port: !env PS_PORT + +sync_config: + path: sync-config.yaml + +client_auth: + jwks_uri: !env PS_JWKS_URL + audience: ["powersync-dev", "powersync"] + +api: + tokens: + - use_a_better_token_in_production diff --git a/config/sync-config.yaml b/config/sync-config.yaml index e6f0e49..d9c3a9b 100644 --- a/config/sync-config.yaml +++ b/config/sync-config.yaml @@ -1,16 +1,20 @@ # yaml-language-server: $schema=https://unpkg.com/@powersync/service-sync-rules@latest/schema/sync_rules.json +# Adopter Mode: this file is YOURS. Describe what your clients should sync. +# +# There is no sensible default here — only you know your schema — so this starts empty and the +# service will tell you so until you fill it in. + config: - # The compiled-streams edition. Omitting this selects edition 1, the legacy compiler, which - # would reject the `streams` block below. + # The compiled-streams edition. Omitting this selects the legacy compiler, which rejects + # `streams` entirely. edition: 3 streams: - global: - # Streams do not subscribe by default — omit this and the client syncs nothing, with no error - # reported anywhere. The legacy bucket_definitions format had no equivalent concept, so this - # line is what preserves the old sync-everything-upfront behaviour. - auto_subscribe: true - queries: - - SELECT * FROM lists - - SELECT * FROM todos + # Replace with your own. For example: + # + # my_data: + # # Streams do NOT subscribe by default. Without this line the client syncs nothing and + # # reports no error anywhere. + # auto_subscribe: true + # query: SELECT * FROM my_table WHERE owner_id = auth.user_id() diff --git a/docker-compose.yaml b/docker-compose.yaml index b16fefb..a5bae76 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,36 +1,23 @@ -services: - # PostgreSQL - source database - pg-db: - image: postgres:18 - restart: always - environment: - POSTGRES_USER: postgres - POSTGRES_DB: postgres - POSTGRES_PASSWORD: mypassword - PGPORT: "5432" - volumes: - - pg_data:/var/lib/postgresql - - ./init-scripts:/docker-entrypoint-initdb.d - ports: - - "5432:5432" - command: ["postgres", "-c", "wal_level=logical"] - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] - interval: 5s - timeout: 5s - retries: 5 +# Base definition — everything both run modes share. +# +# On its own this is Adopter Mode: no Example Source Database, no demo client. Point it at a +# database you already run by setting DATABASE_URI in .env. +# +# An example overlay adds a source database, a demo client, and its own PowerSync config. Select +# one with COMPOSE_FILE in .env. - # MongoDB - PowerSync internal storage +services: + # PowerSync's bucket storage. This is the service's own internal store, not your data, and it + # stays inside the docker boundary in every mode — we never create schemas in a database you + # merely pointed us at. mongo: image: mongo:7.0 command: --replSet rs0 --bind_ip_all --quiet restart: unless-stopped - ports: - - "27017:27017" volumes: - mongo_storage:/data/db - # Initializes the MongoDB replica set + # Bucket storage needs a replica set; a single node is fine outside production. mongo-rs-init: image: mongo:7.0 depends_on: @@ -41,7 +28,34 @@ services: - -c - 'mongosh --host mongo:27017 --eval ''try{rs.status().ok && quit(0)} catch {} rs.initiate({_id: "rs0", version: 1, members: [{ _id: 0, host : "mongo:27017" }]})''' - # PowerSync service + # The write API. Built from local source, so your changes land by rebuilding. + backend: + build: + context: ./backend + restart: unless-stopped + environment: + PORT: "6060" + POWERSYNC_URL: powersync-dev + JWT_ISSUER: powersync-dev + POWERSYNC_PRIVATE_KEY: ${POWERSYNC_PRIVATE_KEY} + POWERSYNC_PUBLIC_KEY: ${POWERSYNC_PUBLIC_KEY} + # Adopter Mode: your database. An example overlay replaces both of these. + DATABASE_TYPE: ${DATABASE_TYPE:-postgres} + DATABASE_URI: ${DATABASE_URI:-} + ports: + - "6060:6060" + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://localhost:6060/').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))" + ] + interval: 5s + timeout: 2s + retries: 15 + powersync: image: journeyapps/powersync-service:latest restart: unless-stopped @@ -49,18 +63,19 @@ services: depends_on: mongo-rs-init: condition: service_completed_successfully - pg-db: + backend: condition: service_healthy volumes: + # Adopter Mode config. An example overlay replaces this mount with its own. - ./config:/config environment: - POWERSYNC_CONFIG_PATH: /config/powersync.yaml + POWERSYNC_CONFIG_PATH: /config/service.yaml NODE_OPTIONS: --max-old-space-size=1000 - PS_DATA_SOURCE_URI: postgres://postgres:mypassword@pg-db:5432/postgres - PS_MONGO_URI: mongodb://mongo:27017/powersync_demo - # host.docker.internal lets the container reach the backend running on the host - PS_JWKS_URL: http://host.docker.internal:6060/api/auth/keys + PS_MONGO_URI: mongodb://mongo:27017/powersync_bucket_storage + # The backend is containerised, so PowerSync reaches it by service name. + PS_JWKS_URL: http://backend:6060/api/auth/keys PS_PORT: "8080" + PS_DATA_SOURCE_URI: ${DATABASE_URI:-} healthcheck: test: [ @@ -76,5 +91,4 @@ services: - "8080:8080" volumes: - pg_data: mongo_storage: diff --git a/examples/postgres/README.md b/examples/postgres/README.md new file mode 100644 index 0000000..a53cf09 --- /dev/null +++ b/examples/postgres/README.md @@ -0,0 +1,44 @@ +# Example: Postgres + +A seeded Postgres Example Source Database, plus the demo client. Select it in `.env`: + +```bash +COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml +``` + +Then `docker compose up --build`. + +## What this flavour needs + +PowerSync replicates from Postgres via **logical decoding**, which is why the container runs with +`wal_level=logical`. Two things must exist in the database: + +- A publication named `powersync` covering the replicated tables. `init-scripts/setup.sql` creates + it — a Postgres source without one replicates nothing. +- A user with `SELECT` on those tables and replication rights. The example uses the superuser, + which you should not do anywhere real. + +Postgres 11 or newer. The connector is **generally available** — the most mature of the four. + +## Pointing at your own Postgres instead + +You do not need this example for that. Switch to Adopter Mode in `.env` and set `DATABASE_URI`. +The requirements above still apply to your database: logical replication enabled, a `powersync` +publication, and a user that can read the tables. + +If your database is on this machine rather than in Docker, reach it at `host.docker.internal` +rather than `localhost` — inside a container, `localhost` is the container. + +Hosted Postgres generally needs `sslmode` changed from `disable` in `config/service.yaml`. + +## Gotchas + +`init-scripts/setup.sql` runs **only on the database's first start**. Editing it after the fact +does nothing until you drop the volume: + +```bash +docker compose down -v +``` + +The seeded list id is fixed (`75f89104-…`) so the seeded todos can reference it. Changing it means +changing both. diff --git a/examples/postgres/compose.yaml b/examples/postgres/compose.yaml new file mode 100644 index 0000000..c13246a --- /dev/null +++ b/examples/postgres/compose.yaml @@ -0,0 +1,71 @@ +# Example Mode: Postgres. +# +# Overlays the base definition with a seeded Example Source Database and the demo client. +# +# NOTE: relative paths here resolve against the PROJECT directory — the repo root — not against +# this file's own location. That is why the mount below is ./examples/postgres/... and not +# ./powersync. It reads wrong; it is correct. + +name: write-api-postgres + +services: + # The Example Source Database. Delete this whole directory when you no longer need it. + pg-db: + image: postgres:18 + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_DB: postgres + POSTGRES_PASSWORD: mypassword + PGPORT: "5432" + volumes: + - pg_data:/var/lib/postgresql + - ./examples/postgres/init-scripts:/docker-entrypoint-initdb.d + ports: + - "5432:5432" + # PowerSync replicates from the write-ahead log, which needs logical decoding. + command: ["postgres", "-c", "wal_level=logical"] + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] + interval: 5s + timeout: 5s + retries: 5 + + backend: + environment: + # Facts about this flavour, not things you configure — so they live here, not in .env. + DATABASE_TYPE: postgres + DATABASE_URI: postgres://postgres:mypassword@pg-db:5432/postgres + depends_on: + pg-db: + condition: service_healthy + + powersync: + volumes: + # Replaces the base's ./config mount — Compose matches volumes on their target path. + - ./examples/postgres/powersync:/config + environment: + PS_DATA_SOURCE_URI: postgres://postgres:mypassword@pg-db:5432/postgres + depends_on: + pg-db: + condition: service_healthy + + # The demo client. It is a test fixture bound to this example's schema — bring your own client + # for anything real. + frontend: + build: + context: ./frontend + args: + # These resolve in your browser, not on the compose network, so they are localhost — + # unlike every server-to-server URL here. Vite bakes them in at build time. + VITE_POWERSYNC_URL: http://localhost:8080 + VITE_BACKEND_URL: http://localhost:6060 + restart: unless-stopped + depends_on: + backend: + condition: service_healthy + ports: + - "5173:4173" + +volumes: + pg_data: diff --git a/init-scripts/setup.sql b/examples/postgres/init-scripts/setup.sql similarity index 100% rename from init-scripts/setup.sql rename to examples/postgres/init-scripts/setup.sql diff --git a/config/powersync.yaml b/examples/postgres/powersync/service.yaml similarity index 100% rename from config/powersync.yaml rename to examples/postgres/powersync/service.yaml diff --git a/examples/postgres/powersync/sync-config.yaml b/examples/postgres/powersync/sync-config.yaml new file mode 100644 index 0000000..e6f0e49 --- /dev/null +++ b/examples/postgres/powersync/sync-config.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://unpkg.com/@powersync/service-sync-rules@latest/schema/sync_rules.json + +config: + # The compiled-streams edition. Omitting this selects edition 1, the legacy compiler, which + # would reject the `streams` block below. + edition: 3 + +streams: + global: + # Streams do not subscribe by default — omit this and the client syncs nothing, with no error + # reported anywhere. The legacy bucket_definitions format had no equivalent concept, so this + # line is what preserves the old sync-everything-upfront behaviour. + auto_subscribe: true + queries: + - SELECT * FROM lists + - SELECT * FROM todos From 600cf7c3816f5357d96212397c0446118b01e2ba Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Wed, 16 Sep 2026 16:41:28 +0200 Subject: [PATCH 06/18] feat: Adopter Mode, and refuse to start on bad configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopter Mode now works against a database this project does not own. Verified against a standalone Postgres started outside this Compose file, with its own schema and credentials, reached over host.docker.internal: the adopter's rows synced, a write through the API landed in their database, and it came back. The fixed rule was checked rather than asserted — after a full run against that external database, it contained zero powersync schemas. Bucket storage stayed in the container this project owns. Refusing to start is now a readable failure. It already failed before listening, but by throwing a raw stack trace, which reads as a bug in the adopter's code rather than a setting they have not filled in. A ConfigurationError distinguishes misconfiguration from a genuine fault, and the message names the fix, including the alternative of selecting a bundled example. An unsupported DATABASE_TYPE now lists what is supported. Written test-first at a process-level seam. The spec claimed Seam 1 covered this; it does not, and cannot — Seam 1 drives HTTP against an assembled application, and the behaviour here is a refusal to reach the point where there is anything to send a request to. The check spawns the backend and asserts on exit code and output, in the backend's existing runner rather than a third architectural seam. config/sync-config.yaml ships empty because only the adopter knows their schema, and PowerSync treats that as fatal and restart-loops on 'streams' are required. Shipping a stream that references tables which may not exist would be worse, so the template and README both state that this is expected and must be filled in before starting. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 8 ++++ backend/boot.test.ts | 58 ++++++++++++++++++++++++++++ backend/index.ts | 13 ++++++- backend/src/errors.ts | 10 +++++ backend/src/persistance/persister.ts | 16 +++++++- config/sync-config.yaml | 6 ++- 6 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 backend/boot.test.ts diff --git a/README.md b/README.md index 66fa915..0c6de00 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,14 @@ DATABASE_URI=postgres://user:password@your-host:5432/your-db 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, diff --git a/backend/boot.test.ts b/backend/boot.test.ts new file mode 100644 index 0000000..4f43709 --- /dev/null +++ b/backend/boot.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest'; +import { spawn } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; + +/** + * Seam: the backend process itself. + * + * Seam 1 (HTTP against the assembled application) cannot observe this, because the behaviour under + * test is a refusal to reach the point where there is anything to make a request to. An adopter + * pointing this at their own database is the most likely person to misconfigure it, and the + * failure has to read as configuration rather than as a bug in their code. + */ + +const backendDir = path.dirname(fileURLToPath(import.meta.url)); + +const bootWith = (env: Record): Promise<{ code: number | null; output: string }> => + new Promise((resolve) => { + const child = spawn('npx', ['tsx', 'index.ts'], { + cwd: backendDir, + // A port nothing else uses, so a regression that DOES start the server cannot collide with + // a real backend and look like a pass. + env: { ...process.env, PORT: '6098', ...env } + }); + + let output = ''; + child.stdout.on('data', (d) => (output += d)); + child.stderr.on('data', (d) => (output += d)); + child.on('close', (code) => resolve({ code, output })); + }); + +describe('refusing to start on bad configuration', () => { + it('explains that no connection string is configured, and exits non-zero', async () => { + const { code, output } = await bootWith({ DATABASE_URI: '', DATABASE_TYPE: 'postgres' }); + + expect(code).not.toBe(0); + expect(output).toContain('DATABASE_URI'); + // The message must name the fix, not just the fault. + expect(output.toLowerCase()).toContain('.env'); + // A raw stack trace is not a readable message. + expect(output).not.toContain('at createConfiguredPersister'); + expect(output).not.toContain('Server is running'); + }, 60000); + + it('names the supported databases when the type is not one of them', async () => { + const { code, output } = await bootWith({ + DATABASE_URI: 'postgres://u:p@h:5432/d', + DATABASE_TYPE: 'cassandra' + }); + + expect(code).not.toBe(0); + expect(output).toContain('cassandra'); + for (const supported of ['postgres', 'mongodb', 'mysql', 'mssql']) { + expect(output).toContain(supported); + } + expect(output).not.toContain('Server is running'); + }, 60000); +}); diff --git a/backend/index.ts b/backend/index.ts index 421313e..652aac4 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -1,12 +1,23 @@ import app from './app.js'; import config from './config.js'; import { getPersister } from './src/persistance/persister.js'; +import { ConfigurationError } from './src/errors.js'; const PORT = process.env.PORT || config.port; // Resolving the persister is lazy so that importing the app needs no database. Do it here, before // listening, so a misconfigured database still fails at boot rather than on the first write. -await getPersister(); +try { + await getPersister(); +} catch (error) { + if (error instanceof ConfigurationError) { + // An adopter pointing this at their own database is the most likely person to land here, and + // a stack trace reads like a bug in their code rather than a setting they have not filled in. + console.error(`\nCannot start.\n\n${error.message}\n`); + process.exit(1); + } + throw error; +} app.listen(PORT, () => { console.log(`Server is running @ http://127.0.0.1:${PORT}`); diff --git a/backend/src/errors.ts b/backend/src/errors.ts index 16b02e2..cb13803 100644 --- a/backend/src/errors.ts +++ b/backend/src/errors.ts @@ -1,3 +1,13 @@ +/** + * The backend is misconfigured and cannot start. Carries a message written for whoever is running + * it, naming the fix rather than only the fault. + */ +export class ConfigurationError extends Error { + constructor(message: string) { + super(message); + } +} + /** Transient failure (deadlock, timeout, connection error). Client should retry. */ export class RetryableError extends Error { constructor(message: string) { diff --git a/backend/src/persistance/persister.ts b/backend/src/persistance/persister.ts index ebb4ed1..c35ece7 100644 --- a/backend/src/persistance/persister.ts +++ b/backend/src/persistance/persister.ts @@ -1,5 +1,6 @@ import config from '../../config.js'; import { factories } from './persister-factories.js'; +import { ConfigurationError } from '../errors.js'; import type { Persister } from '../types.js'; let substituted: Persister | null = null; @@ -32,10 +33,21 @@ export const getPersister = async (): Promise => { const createConfiguredPersister = async (): Promise => { const factory = factories[config.database.type]; if (!factory) { - throw new Error(`Unsupported database type: ${config.database.type}`); + const supported = Object.keys(factories).sort().join(', '); + throw new ConfigurationError( + `DATABASE_TYPE is "${config.database.type}", which is not a database this backend supports.\n\n` + + `Supported: ${supported}\n\n` + + `Set it in .env.` + ); } if (!config.database.uri) { - throw new Error('DATABASE_URI environment variable is required'); + throw new ConfigurationError( + `DATABASE_URI is not set, so there is no source database to write to.\n\n` + + `Set it in .env to a database you already run:\n\n` + + ` DATABASE_URI=postgres://user:password@host:5432/database\n\n` + + `Or select a bundled example instead, which brings its own database:\n\n` + + ` COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml` + ); } return factory(config.database.uri); diff --git a/config/sync-config.yaml b/config/sync-config.yaml index d9c3a9b..d5b9f5a 100644 --- a/config/sync-config.yaml +++ b/config/sync-config.yaml @@ -2,8 +2,10 @@ # Adopter Mode: this file is YOURS. Describe what your clients should sync. # -# There is no sensible default here — only you know your schema — so this starts empty and the -# service will tell you so until you fill it in. +# There is no sensible default here — only you know your schema — so this starts empty. +# +# Until you add a stream, the PowerSync service will restart in a loop logging +# `'streams' are required`. That is expected, not a bug: fill this in before starting Adopter Mode. config: # The compiled-streams edition. Omitting this selects the legacy compiler, which rejects From 89a6f2858799a8029dba04c98a9a0e1193159417 Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Wed, 16 Sep 2026 16:44:16 +0200 Subject: [PATCH 07/18] test: assert the resolved compose topology MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guards the two mechanisms holding the mode switch together, both of which fail silently. If an example's config mount appended to the base's instead of replacing it, or a mode selection failed to resolve, the stack would come up perfectly healthy pointing at the wrong sync rules — nothing crashes, nothing logs, and the only symptom is data that does not sync. Compose is asked to resolve each mode rather than run it, so no containers start and no images are pulled; the suite takes about a second. It asserts the service set, the project name, that exactly one directory is mounted at /config and it is the right one, and that the write API and replication point at the same database. Two assertions go past the ticket's criteria deliberately: bucket storage must point at the owned mongo service in every mode, which is the spec's one fixed rule, and PowerSync must reach the backend by service name rather than through the host. Mutation-tested: breaking the overlay's mount target turns exactly one test red and leaves the rest green. Lives at the repo root with its own runner, because the topology is a repo-level concern and the backend's suite must stay runnable without Docker. The root had no package.json and no node_modules ignore rule; both added, the latter scoped so it cannot match the lockfiles the Dockerfiles install from. Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 3 + README.md | 14 + .../components/providers/SystemProvider.tsx | 1 + package.json | 12 + pnpm-lock.yaml | 689 ++++++++++++++++++ tests/compose.test.ts | 120 +++ vitest.config.ts | 10 + 7 files changed, 849 insertions(+) create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 tests/compose.test.ts create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore index 574dccc..f21396f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ docs/agents/ # Per-repo agent instructions CLAUDE.md + +# Dependencies. Lockfiles stay tracked — the Dockerfiles install with --frozen-lockfile. +node_modules/ diff --git a/README.md b/README.md index 0c6de00..91cd031 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,20 @@ your own identity provider — see [auth-verifiers.md](./auth-verifiers.md) for > 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. +## Tests + +```bash +pnpm install && pnpm test # repo root: the resolved compose topology +cd backend && pnpm test # the write API +``` + +The root suite asks Compose to *resolve* each mode rather than run it — no containers start and no +images are pulled, so it takes about a second. It exists because the two mechanisms holding the +mode switch together fail silently: if an example's config mount appended to the base's instead of +replacing it, the stack would come up perfectly healthy pointing at the wrong sync rules. + +The backend's own suite needs no Docker. + ## Generating types from the contract Both packages generate TypeScript from `backend/openapi.yaml`: diff --git a/frontend/src/components/providers/SystemProvider.tsx b/frontend/src/components/providers/SystemProvider.tsx index d0706e0..afa23ca 100644 --- a/frontend/src/components/providers/SystemProvider.tsx +++ b/frontend/src/components/providers/SystemProvider.tsx @@ -11,6 +11,7 @@ export const db = new PowerSyncDatabase({ database: { dbFilename: 'example.db' }, + crudUploadThrottleMs: 10000, schema: AppSchema, logger: Logger }); diff --git a/package.json b/package.json new file mode 100644 index 0000000..467dba2 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "write-api", + "private": true, + "type": "module", + "scripts": { + "test": "vitest run" + }, + "devDependencies": { + "vitest": "^4.1.10" + }, + "packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..7924c36 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,689 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + vitest: + specifier: ^4.1.10 + version: 4.1.11(vite@8.3.0) + +packages: + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@oxc-project/types@0.150.0': + resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==} + + '@rolldown/binding-android-arm-eabi@1.2.9': + resolution: {integrity: sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.9': + resolution: {integrity: sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.9': + resolution: {integrity: sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.9': + resolution: {integrity: sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.9': + resolution: {integrity: sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.9': + resolution: {integrity: sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.9': + resolution: {integrity: sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.2.9': + resolution: {integrity: sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.2.9': + resolution: {integrity: sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.2.9': + resolution: {integrity: sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.2.9': + resolution: {integrity: sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.2.9': + resolution: {integrity: sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.2.9': + resolution: {integrity: sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.9': + resolution: {integrity: sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.9': + resolution: {integrity: sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + rolldown@1.2.9: + resolution: {integrity: sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + vite@8.3.0: + resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.7.1 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + +snapshots: + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@oxc-project/types@0.150.0': {} + + '@rolldown/binding-android-arm-eabi@1.2.9': + optional: true + + '@rolldown/binding-android-arm64@1.2.9': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.9': + optional: true + + '@rolldown/binding-darwin-x64@1.2.9': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.9': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.9': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.9': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.9': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.9': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.9': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.9': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.9': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.9': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.9': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.9': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@vitest/expect@4.1.11': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.11(vite@8.3.0)': + dependencies: + '@vitest/spy': 4.1.11 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.3.0 + + '@vitest/pretty-format@4.1.11': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.11': + dependencies: + '@vitest/utils': 4.1.11 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.11': {} + + '@vitest/utils@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + assertion-error@2.0.1: {} + + chai@6.2.2: {} + + convert-source-map@2.0.0: {} + + detect-libc@2.1.2: {} + + es-module-lexer@2.3.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + nanoid@3.3.19: {} + + obug@2.2.1: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rolldown@1.2.9: + dependencies: + '@oxc-project/types': 0.150.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.9 + '@rolldown/binding-android-arm64': 1.2.9 + '@rolldown/binding-darwin-arm64': 1.2.9 + '@rolldown/binding-darwin-x64': 1.2.9 + '@rolldown/binding-freebsd-x64': 1.2.9 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.9 + '@rolldown/binding-linux-arm64-gnu': 1.2.9 + '@rolldown/binding-linux-arm64-musl': 1.2.9 + '@rolldown/binding-linux-ppc64-gnu': 1.2.9 + '@rolldown/binding-linux-s390x-gnu': 1.2.9 + '@rolldown/binding-linux-x64-gnu': 1.2.9 + '@rolldown/binding-linux-x64-musl': 1.2.9 + '@rolldown/binding-openharmony-arm64': 1.2.9 + '@rolldown/binding-win32-arm64-msvc': 1.2.9 + '@rolldown/binding-win32-x64-msvc': 1.2.9 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyrainbow@3.1.1: {} + + vite@8.3.0: + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.9 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + + vitest@4.1.11(vite@8.3.0): + dependencies: + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@8.3.0) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.2.1 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.3.0 + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - msw + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 diff --git a/tests/compose.test.ts b/tests/compose.test.ts new file mode 100644 index 0000000..29ae34f --- /dev/null +++ b/tests/compose.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it } from 'vitest'; +import { execFile } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +import { promisify } from 'node:util'; + +const run = promisify(execFile); +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +/** + * Seam: the resolved Compose topology. + * + * These guard two mechanisms that fail SILENTLY. If an example's config mount appended to the + * base's instead of replacing it, or a mode selection failed to resolve, the stack would come up + * perfectly healthy and point at the wrong sync rules — nothing crashes, nothing logs an error, + * and the only symptom is data that does not sync. + * + * Compose is asked to resolve each mode, not to run it: no containers start and no images are + * pulled, so this observes the resolved contract rather than the text of the files. + */ + +interface Mode { + name: string; + composeFile: string; + projectName: string; + services: string[]; + /** Repo-relative path expected to be mounted at /config, or null for no example override. */ + configMount: string; + databaseType?: string; + /** Substring the write API's and PowerSync's connection strings must both contain. */ + connectionHost?: string; +} + +const ADOPTER_ENV = { + DATABASE_TYPE: 'postgres', + DATABASE_URI: 'postgres://someone:secret@their-own-host:5432/theirdb' +}; + +const MODES: Mode[] = [ + { + name: 'Adopter Mode', + composeFile: 'docker-compose.yaml', + projectName: 'write-api', + services: ['backend', 'mongo', 'mongo-rs-init', 'powersync'], + configMount: 'config', + databaseType: 'postgres', + connectionHost: 'their-own-host' + }, + { + name: 'Example Mode: Postgres', + composeFile: 'docker-compose.yaml:examples/postgres/compose.yaml', + projectName: 'write-api-postgres', + services: ['backend', 'frontend', 'mongo', 'mongo-rs-init', 'pg-db', 'powersync'], + configMount: 'examples/postgres/powersync', + databaseType: 'postgres', + connectionHost: 'pg-db' + } +]; + +const resolve = async (composeFile: string) => { + const { stdout } = await run('docker', ['compose', 'config', '--format', 'json'], { + cwd: repoRoot, + env: { ...process.env, ...ADOPTER_ENV, COMPOSE_FILE: composeFile }, + maxBuffer: 10 * 1024 * 1024 + }); + return JSON.parse(stdout); +}; + +const mountedAtConfig = (service: { volumes?: { target: string; source: string }[] }): string[] => + (service.volumes ?? []) + .filter((v) => v.target === '/config') + .map((v) => path.relative(repoRoot, v.source)); + +describe.each(MODES)('$name', (mode) => { + it('resolves to exactly the expected services', async () => { + const resolved = await resolve(mode.composeFile); + + expect(Object.keys(resolved.services).sort()).toEqual(mode.services); + }); + + it('runs under its own project name, so modes cannot share volumes', async () => { + const resolved = await resolve(mode.composeFile); + + expect(resolved.name).toBe(mode.projectName); + }); + + it('mounts exactly one config directory, and it is the right one', async () => { + const resolved = await resolve(mode.composeFile); + + // Exactly one: an overlay that appended rather than replaced would give two, and PowerSync + // would read whichever Docker happened to layer last. + expect(mountedAtConfig(resolved.services.powersync)).toEqual([mode.configMount]); + }); + + it('points the write API and replication at the same database', async () => { + const resolved = await resolve(mode.composeFile); + const backend = resolved.services.backend.environment; + const powersync = resolved.services.powersync.environment; + + expect(backend.DATABASE_TYPE).toBe(mode.databaseType); + expect(backend.DATABASE_URI).toContain(mode.connectionHost); + expect(powersync.PS_DATA_SOURCE_URI).toContain(mode.connectionHost); + }); + + it('keeps bucket storage in a container this project owns', async () => { + const resolved = await resolve(mode.composeFile); + + // The fixed rule: bucket storage never points at the adopter's database, whatever mode it is. + expect(resolved.services.powersync.environment.PS_MONGO_URI).toContain('mongodb://mongo:'); + expect(resolved.services.mongo).toBeDefined(); + }); + + it('reaches the backend over the compose network, not the host', async () => { + const resolved = await resolve(mode.composeFile); + + expect(resolved.services.powersync.environment.PS_JWKS_URL).toBe( + 'http://backend:6060/api/auth/keys' + ); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..31d611e --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + // Only this repo's own topology tests. The backend and frontend own their suites, and the + // backend's must stay runnable on a machine with no Docker. + include: ['tests/**/*.test.ts'], + testTimeout: 30000 + } +}); From a3857dd0bca511501d839f5f90ef0ffb06b435b2 Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Wed, 16 Sep 2026 16:45:08 +0200 Subject: [PATCH 08/18] fix: stop the root vitest config hiding the backend's tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding a config at the repo root silently broke the backend's suite. Vitest walks up looking for a config file; the backend had none of its own, so it inherited the root's include pattern, found nothing matching, and exited 1 reporting "No test files found" — with both of its test files sitting right there. The backend now has an explicit config. Both suites verified by running them: 4 tests in the backend, 12 at the root. Co-Authored-By: Claude Opus 5 (1M context) --- backend/vitest.config.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 backend/vitest.config.ts diff --git a/backend/vitest.config.ts b/backend/vitest.config.ts new file mode 100644 index 0000000..aafcbe3 --- /dev/null +++ b/backend/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + // Explicit, because without a config here vitest walks up and finds the repo root's, whose + // include pattern points at the topology suite and hides every test in this package. + include: ['**/*.test.ts'], + exclude: ['**/node_modules/**', '**/dist/**'], + testTimeout: 60000 + } +}); From 32dc47a0b77cb63482b5a330cfb605d7290e6aaf Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Wed, 16 Sep 2026 16:48:47 +0200 Subject: [PATCH 09/18] feat: Example Mode on MongoDB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds no database container at all. PowerSync already runs a Mongo replica set for bucket storage, so replication points at a second database on that same server — powersync_demo_source alongside powersync_bucket_storage. Separate databases, one process, so trying this flavour costs nothing extra. No seed script either: collections are created implicitly by the first write, so the app starts empty and fills as you use it. The sync rules project _id onto id, which is what makes documents arrive with the identifier the rest of the system keys on. The comment explaining it was wrong — inherited from upstream's wording, it claimed * excludes underscore-prefixed fields. The synced documents carry _id alongside id, so the comment now describes what actually happens; the extra field is harmless. The topology suite covers this mode too, including that it introduces no source service. Eighteen assertions across three modes. Verified: a write returned success, landed in the source database with the client-generated string as _id, and came back through sync with a usable id. Co-Authored-By: Claude Opus 5 (1M context) --- .env | 1 + README.md | 6 ++- examples/mongodb/README.md | 52 +++++++++++++++++++++ examples/mongodb/compose.yaml | 40 ++++++++++++++++ examples/mongodb/powersync/service.yaml | 31 ++++++++++++ examples/mongodb/powersync/sync-config.yaml | 16 +++++++ tests/compose.test.ts | 11 +++++ 7 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 examples/mongodb/README.md create mode 100644 examples/mongodb/compose.yaml create mode 100644 examples/mongodb/powersync/service.yaml create mode 100644 examples/mongodb/powersync/sync-config.yaml diff --git a/.env b/.env index 1dc85c3..5b0177a 100644 --- a/.env +++ b/.env @@ -3,6 +3,7 @@ # The command is always a plain `docker compose up` — switching modes is this one line, so every # other compose subcommand keeps working the way you expect. COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml +# COMPOSE_FILE=docker-compose.yaml:examples/mongodb/compose.yaml # COMPOSE_FILE=docker-compose.yaml # Adopter Mode — your own database # Adopter Mode only. Example Mode overrides both of these with the bundled database. diff --git a/README.md b/README.md index 91cd031..ee31c69 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,13 @@ out. The command stays a plain `docker compose up`, so `down`, `logs` and `ps` b | `.env` line | What runs | | --- | --- | -| `docker-compose.yaml:examples/postgres/compose.yaml` | Example Mode, Postgres | +| `docker-compose.yaml:examples/postgres/compose.yaml` | Example Mode, [Postgres](./examples/postgres/README.md) | +| `docker-compose.yaml:examples/mongodb/compose.yaml` | Example Mode, [MongoDB](./examples/mongodb/README.md) | | `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. + If you would rather be explicit, the same thing without `.env`: ```bash diff --git a/examples/mongodb/README.md b/examples/mongodb/README.md new file mode 100644 index 0000000..e6a3c00 --- /dev/null +++ b/examples/mongodb/README.md @@ -0,0 +1,52 @@ +# Example: MongoDB + +A MongoDB Example Source Database. Select it in `.env`: + +```bash +COMPOSE_FILE=docker-compose.yaml:examples/mongodb/compose.yaml +``` + +Then `docker compose up --build`. + +## No source container, and no seeding + +This example adds **no database container at all**. PowerSync already runs a Mongo replica set for +bucket storage, so replication points at a second database on that same server — +`powersync_demo_source` alongside `powersync_bucket_storage`. Separate databases, one process. +Trying this flavour costs your machine nothing extra. + +There is also **no seed script**. Collections are created implicitly by the write API's first +write, so the app starts empty. Add a list in the demo client and it appears. + +## What this flavour needs + +- **A replica set.** Change streams require one, and so do the multi-document transactions the + write API uses to apply a Transaction atomically. A single node is fine outside production; the + base compose file initialises `rs0` for exactly this reason. +- **Pre/post images.** Change streams alone do not carry the document as it was before an update. + `post_images: auto_configure` in `powersync/service.yaml` turns this on for replicated + collections. On Azure DocumentDB, which shares this connector, post-images are unsupported and + this must be `off`. + +MongoDB 6.0 or newer. The connector is **generally available**. + +## The `_id` projection + +MongoDB's primary key is `_id`; the client schema expects `id`. The sync rules bridge that: + +```yaml +- SELECT _id as id, * FROM lists +``` + +Without the projection documents still sync, but arrive without the identifier the rest of the +system keys on. `*` carries `_id` through as well — harmless, since the client ignores columns it +does not declare. + +## Gotchas + +The write API stores `_id` as the **string** id the client generated, not an `ObjectId`. That is +what makes the round trip work: the id a client invents offline survives into Mongo unchanged. + +A single write can produce more than one operation in a bucket for the same document. That is the +op log behaving normally, not a duplicate — operations apply in order and the client converges on +the latest. diff --git a/examples/mongodb/compose.yaml b/examples/mongodb/compose.yaml new file mode 100644 index 0000000..7ef230a --- /dev/null +++ b/examples/mongodb/compose.yaml @@ -0,0 +1,40 @@ +# Example Mode: MongoDB. +# +# The cheapest of the examples: it adds NO source database container. PowerSync already runs a +# Mongo replica set for bucket storage, so replication points at a second database on that same +# server. Bucket storage and your data stay in separate databases; they merely share a process. +# +# NOTE: relative paths here resolve against the PROJECT directory — the repo root — not against +# this file's own location. + +name: write-api-mongodb + +services: + backend: + environment: + # The database name is part of the URI; the persister uses whatever it names. + DATABASE_TYPE: mongodb + DATABASE_URI: mongodb://mongo:27017/powersync_demo_source + depends_on: + mongo-rs-init: + condition: service_completed_successfully + + powersync: + volumes: + - ./examples/mongodb/powersync:/config + environment: + PS_DATA_SOURCE_URI: mongodb://mongo:27017/powersync_demo_source + + frontend: + build: + context: ./frontend + args: + # These resolve in your browser, not on the compose network. + VITE_POWERSYNC_URL: http://localhost:8080 + VITE_BACKEND_URL: http://localhost:6060 + restart: unless-stopped + depends_on: + backend: + condition: service_healthy + ports: + - "5173:4173" diff --git a/examples/mongodb/powersync/service.yaml b/examples/mongodb/powersync/service.yaml new file mode 100644 index 0000000..3cbb13b --- /dev/null +++ b/examples/mongodb/powersync/service.yaml @@ -0,0 +1,31 @@ +# yaml-language-server: $schema=https://unpkg.com/@powersync/service-schema@latest/json-schema/powersync-config.json + +telemetry: + disable_telemetry_sharing: false + +replication: + connections: + - type: mongodb + uri: !env PS_DATA_SOURCE_URI + # Change streams alone do not carry the document as it was before an update, which + # replication needs. auto_configure turns on pre/post images for replicated collections. + post_images: auto_configure + +storage: + # A different database on the same replica set as the source above. Bucket storage is still + # ours and still inside the docker boundary. + type: mongodb + uri: !env PS_MONGO_URI + +port: !env PS_PORT + +sync_config: + path: sync-config.yaml + +client_auth: + jwks_uri: !env PS_JWKS_URL + audience: ["powersync-dev", "powersync"] + +api: + tokens: + - use_a_better_token_in_production diff --git a/examples/mongodb/powersync/sync-config.yaml b/examples/mongodb/powersync/sync-config.yaml new file mode 100644 index 0000000..38aa084 --- /dev/null +++ b/examples/mongodb/powersync/sync-config.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://unpkg.com/@powersync/service-sync-rules@latest/schema/sync_rules.json + +config: + edition: 3 + +streams: + global: + # Streams do not subscribe by default — omit this and the client syncs nothing, silently. + auto_subscribe: true + queries: + # MongoDB's primary key is _id. The client schema expects `id`, so project it across — + # without this, documents sync but arrive without the identifier everything else keys on. + # `*` does still carry _id through as well; the extra field is harmless, since the client + # schema ignores columns it does not declare. + - SELECT _id as id, * FROM lists + - SELECT _id as id, * FROM todos diff --git a/tests/compose.test.ts b/tests/compose.test.ts index 29ae34f..bb088f3 100644 --- a/tests/compose.test.ts +++ b/tests/compose.test.ts @@ -46,6 +46,17 @@ const MODES: Mode[] = [ databaseType: 'postgres', connectionHost: 'their-own-host' }, + { + name: 'Example Mode: MongoDB', + composeFile: 'docker-compose.yaml:examples/mongodb/compose.yaml', + projectName: 'write-api-mongodb', + // Deliberately no source database service: replication points at a second database on the + // replica set already running for bucket storage. + services: ['backend', 'frontend', 'mongo', 'mongo-rs-init', 'powersync'], + configMount: 'examples/mongodb/powersync', + databaseType: 'mongodb', + connectionHost: 'mongo:27017/powersync_demo_source' + }, { name: 'Example Mode: Postgres', composeFile: 'docker-compose.yaml:examples/postgres/compose.yaml', From ea9f4824e42c256d0b4d89f434ca980807f1d835 Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Wed, 16 Sep 2026 16:55:20 +0200 Subject: [PATCH 10/18] feat: development overlay for editing backend code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Append docker-compose.dev.yaml to any mode and your working tree is mounted into the container with the watch process running: an edit is serving in about two seconds, no image rebuild. Verified in both modes by editing a route handler while the stack ran, including in Adopter Mode against an external Postgres. The keypair dependency was verified rather than assumed — a token issued before an edit triggered a reload was still accepted afterwards. That is precisely why the previous work committed a fixed pair; ephemeral keys would have fired this on every save. An anonymous volume preserves the image's node_modules. Without it the bind mount shadows them with the host's, which on macOS means binaries the container cannot run. The demo client stays out of the overlay deliberately. The overlay applies to both modes and Adopter Mode has no frontend service, so an override would try to create one with no build context. Its loop is the ordinary `pnpm dev` on the host, which reads .env.local at runtime — documented instead. pnpm generate-keys prints a replacement keypair in the form .env expects. Also documents a trap hit while testing: each mode is its own Compose project, so `docker compose down` only stops the mode currently selected in .env. Switching without bringing the old one down leaves containers holding ports. Co-Authored-By: Claude Opus 5 (1M context) --- .env | 3 +++ README.md | 33 ++++++++++++++++++++++++++++++- backend/package.json | 1 + backend/scripts/generate-keys.ts | 12 +++++++++++ docker-compose.dev.yaml | 20 +++++++++++++++++++ tests/compose.test.ts | 34 ++++++++++++++++++++++++++++++++ 6 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 backend/scripts/generate-keys.ts create mode 100644 docker-compose.dev.yaml diff --git a/.env b/.env index 5b0177a..d1303fa 100644 --- a/.env +++ b/.env @@ -5,6 +5,9 @@ COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml # COMPOSE_FILE=docker-compose.yaml:examples/mongodb/compose.yaml # COMPOSE_FILE=docker-compose.yaml # Adopter Mode — your own database +# +# Append :docker-compose.dev.yaml to any of the above for the development loop, e.g. +# COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml:docker-compose.dev.yaml # Adopter Mode only. Example Mode overrides both of these with the bundled database. # DATABASE_TYPE=postgres diff --git a/README.md b/README.md index ee31c69..fe97a77 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,11 @@ out. The command stays a plain `docker compose up`, so `down`, `logs` and `ps` b 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`: ```bash @@ -95,15 +100,41 @@ write-api/ ## Changing the backend -The backend is built from local source, so your changes ship when you rebuild: +Append the development overlay to whichever mode you are in: + +```bash +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: ```bash docker compose up --build ``` +The demo client is a Vite app, so its own loop is the usual one, on the host: + +```bash +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](./auth-verifiers.md) for worked examples. +Replacing the throwaway signing keys is one command: + +```bash +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. diff --git a/backend/package.json b/backend/package.json index e9f6b4d..efcf25e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,6 +7,7 @@ "scripts": { "start": "tsx index.ts", "dev": "tsx watch index.ts", + "generate-keys": "tsx scripts/generate-keys.ts", "format": "prettier --write .", "check": "tsc -b", "test": "vitest run", diff --git a/backend/scripts/generate-keys.ts b/backend/scripts/generate-keys.ts new file mode 100644 index 0000000..3d1df08 --- /dev/null +++ b/backend/scripts/generate-keys.ts @@ -0,0 +1,12 @@ +import { generateKeyPair } from '../src/utils/generate-key.js'; + +/** + * Prints a fresh signing keypair in the form .env expects. + * + * The pair committed to .env is public and known to everyone who has cloned this repo. Anything + * beyond a demo needs its own. + */ +const { privateBase64, publicBase64 } = await generateKeyPair(); + +console.log(`POWERSYNC_PRIVATE_KEY=${privateBase64}`); +console.log(`POWERSYNC_PUBLIC_KEY=${publicBase64}`); diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml new file mode 100644 index 0000000..5c071bf --- /dev/null +++ b/docker-compose.dev.yaml @@ -0,0 +1,20 @@ +# Development overlay: edit backend code and see it running in seconds, with no image rebuild. +# +# Append it to whichever mode you are in: +# +# COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml:docker-compose.dev.yaml +# COMPOSE_FILE=docker-compose.yaml:docker-compose.dev.yaml # Adopter Mode +# +# It overrides the backend only. An adopter running Adopter Mode has no demo client for an +# overlay to reference, so the client's own dev loop is `cd frontend && pnpm dev` on the host. + +services: + backend: + # tsx watch, already in the backend's scripts — restarts on save. + command: ["pnpm", "dev"] + volumes: + # Your working tree, live in the container. + - ./backend:/app + # Keeps the image's Linux node_modules. Without this the bind mount above shadows them with + # the host's, which on macOS means native binaries the container cannot run. + - /app/node_modules diff --git a/tests/compose.test.ts b/tests/compose.test.ts index bb088f3..4236d52 100644 --- a/tests/compose.test.ts +++ b/tests/compose.test.ts @@ -129,3 +129,37 @@ describe.each(MODES)('$name', (mode) => { ); }); }); + +/** + * The development overlay is a modifier rather than a mode: it may be appended to either mode and + * must change only how the backend runs. Adding or removing a service here would mean an adopter's + * dev loop differs from what they deploy. + */ +describe('development overlay', () => { + const withDev = (composeFile: string) => `${composeFile}:docker-compose.dev.yaml`; + + it.each(MODES)('adds no services and removes none from $name', async (mode) => { + const plain = await resolve(mode.composeFile); + const dev = await resolve(withDev(mode.composeFile)); + + expect(Object.keys(dev.services).sort()).toEqual(Object.keys(plain.services).sort()); + }); + + it.each(MODES)('mounts the working tree into the backend for $name', async (mode) => { + const dev = await resolve(withDev(mode.composeFile)); + const mounts = (dev.services.backend.volumes ?? []) as { target: string; source?: string }[]; + + const workingTree = mounts.find((v) => v.target === '/app'); + expect(workingTree?.source && path.relative(repoRoot, workingTree.source)).toBe('backend'); + + // An anonymous volume keeps the image's Linux node_modules. Without it the bind mount above + // shadows them with the host's, which on macOS are binaries the container cannot run. + expect(mounts.some((v) => v.target === '/app/node_modules')).toBe(true); + }); + + it.each(MODES)('leaves the config mount untouched for $name', async (mode) => { + const dev = await resolve(withDev(mode.composeFile)); + + expect(mountedAtConfig(dev.services.powersync)).toEqual([mode.configMount]); + }); +}); From 468f7c2059c3122ba527a25deef5cc2a64a3c88d Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Wed, 16 Sep 2026 16:59:27 +0200 Subject: [PATCH 11/18] feat: Example Mode on MySQL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A seeded MySQL Example Source Database, selected by the same one-line switch. The binlog configuration is mounted into the server's config directory rather than applied by a setup script, because PowerSync reads the binary log and it has to be in the right shape before the server starts — by the time a script could run it is too late. That is the structural difference from the Postgres example, where wal_level is a command-line flag and the publication is created afterwards in SQL. Two users deliberately: the write API connects as root while PowerSync connects as a restricted powersync user holding REPLICATION SLAVE to read the binary log and SELECT to take the initial snapshot, showing what least privilege replication actually needs. Verified by querying the running server for the settings rather than assuming the config was read — log_bin, gtid_mode, enforce_gtid_consistency, binlog_format ROW, binlog_row_image FULL and a server id all confirmed. Seeded rows synced, and a write round-tripped through MySQL and back. The connector is Beta, stated plainly in the example README and marked in the root README's mode table. Co-Authored-By: Claude Opus 5 (1M context) --- .env | 1 + README.md | 1 + examples/mysql/README.md | 51 +++++++++++++++++++ examples/mysql/compose.yaml | 60 +++++++++++++++++++++++ examples/mysql/init-scripts/my.cnf | 14 ++++++ examples/mysql/init-scripts/setup.sql | 38 ++++++++++++++ examples/mysql/powersync/service.yaml | 27 ++++++++++ examples/mysql/powersync/sync-config.yaml | 12 +++++ tests/compose.test.ts | 9 ++++ 9 files changed, 213 insertions(+) create mode 100644 examples/mysql/README.md create mode 100644 examples/mysql/compose.yaml create mode 100644 examples/mysql/init-scripts/my.cnf create mode 100644 examples/mysql/init-scripts/setup.sql create mode 100644 examples/mysql/powersync/service.yaml create mode 100644 examples/mysql/powersync/sync-config.yaml diff --git a/.env b/.env index d1303fa..0a6c866 100644 --- a/.env +++ b/.env @@ -4,6 +4,7 @@ # other compose subcommand keeps working the way you expect. COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml # COMPOSE_FILE=docker-compose.yaml:examples/mongodb/compose.yaml +# COMPOSE_FILE=docker-compose.yaml:examples/mysql/compose.yaml # COMPOSE_FILE=docker-compose.yaml # Adopter Mode — your own database # # Append :docker-compose.dev.yaml to any of the above for the development loop, e.g. diff --git a/README.md b/README.md index fe97a77..d885030 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ out. The command stays a plain `docker compose up`, so `down`, `logs` and `ps` b | --- | --- | | `docker-compose.yaml:examples/postgres/compose.yaml` | Example Mode, [Postgres](./examples/postgres/README.md) | | `docker-compose.yaml:examples/mongodb/compose.yaml` | Example Mode, [MongoDB](./examples/mongodb/README.md) | +| `docker-compose.yaml:examples/mysql/compose.yaml` | Example Mode, [MySQL](./examples/mysql/README.md) (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 diff --git a/examples/mysql/README.md b/examples/mysql/README.md new file mode 100644 index 0000000..562bc93 --- /dev/null +++ b/examples/mysql/README.md @@ -0,0 +1,51 @@ +# Example: MySQL + +A seeded MySQL Example Source Database. Select it in `.env`: + +```bash +COMPOSE_FILE=docker-compose.yaml:examples/mysql/compose.yaml +``` + +Then `docker compose up --build`. + +> **This connector is Beta.** Postgres and MongoDB are generally available; MySQL is not. Weigh +> that before building on it. + +## What this flavour needs + +PowerSync replicates MySQL by reading the **binary log**, which has to be enabled and in the right +shape *before the server starts*. That is why `init-scripts/my.cnf` is mounted into the server's +config directory rather than applied by a setup script — by the time a script could run, it is too +late. + +The settings that matter, all verifiable with `SELECT @@log_bin, @@gtid_mode, @@binlog_format;`: + +| Setting | Value | Why | +| --- | --- | --- | +| `log_bin` | on | Nothing replicates without it | +| `gtid_mode` | `ON` | Lets replication resume from a known position | +| `enforce_gtid_consistency` | `ON` | Required alongside GTID mode | +| `binlog_format` | `ROW` | PowerSync needs rows, not the statements that changed them | +| `binlog_row_image` | `FULL` | Partial images replicate updates incorrectly | +| `server-id` | unique | Each replica in a topology needs its own | + +Two users, deliberately: the write API connects as root, while PowerSync connects as a restricted +`powersync` user holding `REPLICATION SLAVE` (to read the binary log) and `SELECT` (to take the +initial snapshot). Splitting them shows the least privilege replication actually needs. + +MySQL 5.7 or newer. + +## Pointing at your own MySQL instead + +Switch to Adopter Mode in `.env` and set `DATABASE_URI`. Your server needs the same binlog settings +above — on managed MySQL that usually means a parameter group rather than a config file. AWS Aurora +in particular needs binary logging and GTID enabled through a DB Parameter Group, which requires a +restart. + +## Gotchas + +`init-scripts/setup.sql` runs **only on the database's first start**. Editing it later does nothing +until you drop the volume with `docker compose down -v`. + +Ids are `CHAR(36)`, not a native UUID type — MySQL has none. The client generates the id offline and +it is stored verbatim, which is what makes the round trip work. diff --git a/examples/mysql/compose.yaml b/examples/mysql/compose.yaml new file mode 100644 index 0000000..a8872db --- /dev/null +++ b/examples/mysql/compose.yaml @@ -0,0 +1,60 @@ +# Example Mode: MySQL. +# +# NOTE: relative paths here resolve against the PROJECT directory — the repo root — not against +# this file's own location. + +name: write-api-mysql + +services: + mysql-db: + image: mysql:8.4 + restart: always + environment: + MYSQL_ROOT_PASSWORD: mypassword + MYSQL_DATABASE: powersync_demo + volumes: + # Binlog settings must be in place before the server starts; a setup script is too late. + - ./examples/mysql/init-scripts/my.cnf:/etc/mysql/conf.d/powersync.cnf + - ./examples/mysql/init-scripts/setup.sql:/docker-entrypoint-initdb.d/setup.sql + - mysql_data:/var/lib/mysql + ports: + - "3306:3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-pmypassword"] + interval: 5s + timeout: 5s + retries: 20 + + backend: + environment: + # The write API connects as root; PowerSync connects as the restricted replication user. + DATABASE_TYPE: mysql + DATABASE_URI: mysql://root:mypassword@mysql-db:3306/powersync_demo + depends_on: + mysql-db: + condition: service_healthy + + powersync: + volumes: + - ./examples/mysql/powersync:/config + environment: + PS_DATA_SOURCE_URI: mysql://powersync:powersyncpw@mysql-db:3306/powersync_demo + depends_on: + mysql-db: + condition: service_healthy + + frontend: + build: + context: ./frontend + args: + VITE_POWERSYNC_URL: http://localhost:8080 + VITE_BACKEND_URL: http://localhost:6060 + restart: unless-stopped + depends_on: + backend: + condition: service_healthy + ports: + - "5173:4173" + +volumes: + mysql_data: diff --git a/examples/mysql/init-scripts/my.cnf b/examples/mysql/init-scripts/my.cnf new file mode 100644 index 0000000..aacdc61 --- /dev/null +++ b/examples/mysql/init-scripts/my.cnf @@ -0,0 +1,14 @@ +# PowerSync replicates MySQL from the binary log, which must be enabled and in the right shape +# before the server starts — this cannot be turned on afterwards from a setup script. +[mysqld] +log_bin = mysql-bin +server-id = 1 + +# Global transaction identifiers let replication resume from a known position. +gtid_mode = ON +enforce_gtid_consistency = ON + +# Row-based, full images: PowerSync needs the whole row as it was, not the statement that +# changed it. Anything less and updates replicate incorrectly or not at all. +binlog_format = ROW +binlog_row_image = FULL diff --git a/examples/mysql/init-scripts/setup.sql b/examples/mysql/init-scripts/setup.sql new file mode 100644 index 0000000..a298f94 --- /dev/null +++ b/examples/mysql/init-scripts/setup.sql @@ -0,0 +1,38 @@ +-- Runs only on the database's FIRST start. Editing it later does nothing until the volume is +-- dropped with `docker compose down -v`. + +CREATE TABLE lists ( + id CHAR(36) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + name TEXT NOT NULL, + owner_id CHAR(36) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE todos ( + id CHAR(36) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + completed_at TIMESTAMP NULL, + description TEXT NOT NULL, + completed BOOLEAN NOT NULL DEFAULT FALSE, + created_by CHAR(36) NULL, + completed_by CHAR(36) NULL, + list_id CHAR(36) NOT NULL, + photo_id CHAR(36) NULL, + PRIMARY KEY (id) +); + +-- Seed data, so the demo client has something to show on first run. +INSERT INTO lists (id, name, owner_id) + VALUES ('75f89104-d95a-4f16-8309-5363f1bb377a', 'Getting Started', UUID()); +INSERT INTO todos (id, description, list_id, completed) + VALUES (UUID(), 'Run services locally', '75f89104-d95a-4f16-8309-5363f1bb377a', TRUE); +INSERT INTO todos (id, description, list_id, completed) + VALUES (UUID(), 'Create a todo here. Query the todos table over a MySQL connection. Your todo should be synced', '75f89104-d95a-4f16-8309-5363f1bb377a', FALSE); + +-- The replication user PowerSync connects as. REPLICATION SLAVE is what lets it read the binary +-- log; SELECT is what lets it take the initial snapshot. +CREATE USER 'powersync'@'%' IDENTIFIED BY 'powersyncpw'; +GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'powersync'@'%'; +GRANT SELECT ON powersync_demo.* TO 'powersync'@'%'; +FLUSH PRIVILEGES; diff --git a/examples/mysql/powersync/service.yaml b/examples/mysql/powersync/service.yaml new file mode 100644 index 0000000..974c98a --- /dev/null +++ b/examples/mysql/powersync/service.yaml @@ -0,0 +1,27 @@ +# yaml-language-server: $schema=https://unpkg.com/@powersync/service-schema@latest/json-schema/powersync-config.json + +telemetry: + disable_telemetry_sharing: false + +replication: + connections: + - type: mysql + uri: !env PS_DATA_SOURCE_URI + +storage: + # Still ours, still inside the docker boundary. + type: mongodb + uri: !env PS_MONGO_URI + +port: !env PS_PORT + +sync_config: + path: sync-config.yaml + +client_auth: + jwks_uri: !env PS_JWKS_URL + audience: ["powersync-dev", "powersync"] + +api: + tokens: + - use_a_better_token_in_production diff --git a/examples/mysql/powersync/sync-config.yaml b/examples/mysql/powersync/sync-config.yaml new file mode 100644 index 0000000..e250319 --- /dev/null +++ b/examples/mysql/powersync/sync-config.yaml @@ -0,0 +1,12 @@ +# yaml-language-server: $schema=https://unpkg.com/@powersync/service-sync-rules@latest/schema/sync_rules.json + +config: + edition: 3 + +streams: + global: + # Streams do not subscribe by default — omit this and the client syncs nothing, silently. + auto_subscribe: true + queries: + - SELECT * FROM lists + - SELECT * FROM todos diff --git a/tests/compose.test.ts b/tests/compose.test.ts index 4236d52..a8b0535 100644 --- a/tests/compose.test.ts +++ b/tests/compose.test.ts @@ -57,6 +57,15 @@ const MODES: Mode[] = [ databaseType: 'mongodb', connectionHost: 'mongo:27017/powersync_demo_source' }, + { + name: 'Example Mode: MySQL', + composeFile: 'docker-compose.yaml:examples/mysql/compose.yaml', + projectName: 'write-api-mysql', + services: ['backend', 'frontend', 'mongo', 'mongo-rs-init', 'mysql-db', 'powersync'], + configMount: 'examples/mysql/powersync', + databaseType: 'mysql', + connectionHost: 'mysql-db:3306' + }, { name: 'Example Mode: Postgres', composeFile: 'docker-compose.yaml:examples/postgres/compose.yaml', From 04aeed49521dcea87767017c8dc1807913eb4279 Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Wed, 16 Sep 2026 17:08:16 +0200 Subject: [PATCH 12/18] feat: Example Mode on SQL Server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The heaviest of the four. SQL Server replicates through Change Data Capture rather than log streaming, and has no entrypoint directory to drop SQL into, so the bootstrap is a one-shot container gated on service_completed_successfully: CDC at database level, a _powersync_checkpoints table with CDC enabled on it, CDC per replicated table, and a restricted replication user. SQL Server Agent is enabled because CDC capture runs as Agent jobs — without it CDC appears enabled and captures nothing. Fixes a permission bug found by running it rather than reading about it. With CDC fully configured, replication still failed with "The user does not have permission to perform this action", preceded by VIEW SERVER PERFORMANCE STATE being denied in master. That is a server-level permission on the login; the script had granted only the database-level VIEW DATABASE PERFORMANCE STATE. Both are needed, and the two read alike in the documentation. Microsoft publishes no arm64 image, so this runs under linux/amd64 emulation with a long healthcheck start period. The other three flavours run natively. The README leads with the sharp edge rather than burying it: CDC captures a table's shape when enabled, so schema changes are not adopted automatically and an added column never arrives with nothing erroring — which matters most in a repo whose premise is replacing the schema. Verified from a dropped volume to prove the bootstrap works from nothing: CDC state confirmed on the server, seeded rows synced, and a write round-tripped. Co-Authored-By: Claude Opus 5 (1M context) --- .env | 1 + README.md | 1 + examples/mssql/README.md | 64 ++++++++++++++ examples/mssql/compose.yaml | 88 ++++++++++++++++++ examples/mssql/init-scripts/setup.sql | 103 ++++++++++++++++++++++ examples/mssql/powersync/service.yaml | 31 +++++++ examples/mssql/powersync/sync-config.yaml | 12 +++ tests/compose.test.ts | 11 +++ 8 files changed, 311 insertions(+) create mode 100644 examples/mssql/README.md create mode 100644 examples/mssql/compose.yaml create mode 100644 examples/mssql/init-scripts/setup.sql create mode 100644 examples/mssql/powersync/service.yaml create mode 100644 examples/mssql/powersync/sync-config.yaml diff --git a/.env b/.env index 0a6c866..e582c1b 100644 --- a/.env +++ b/.env @@ -5,6 +5,7 @@ COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml # COMPOSE_FILE=docker-compose.yaml:examples/mongodb/compose.yaml # COMPOSE_FILE=docker-compose.yaml:examples/mysql/compose.yaml +# COMPOSE_FILE=docker-compose.yaml:examples/mssql/compose.yaml # COMPOSE_FILE=docker-compose.yaml # Adopter Mode — your own database # # Append :docker-compose.dev.yaml to any of the above for the development loop, e.g. diff --git a/README.md b/README.md index d885030..9cb1f3f 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ out. The command stays a plain `docker compose up`, so `down`, `logs` and `ps` b | `docker-compose.yaml:examples/postgres/compose.yaml` | Example Mode, [Postgres](./examples/postgres/README.md) | | `docker-compose.yaml:examples/mongodb/compose.yaml` | Example Mode, [MongoDB](./examples/mongodb/README.md) | | `docker-compose.yaml:examples/mysql/compose.yaml` | Example Mode, [MySQL](./examples/mysql/README.md) (Beta) | +| `docker-compose.yaml:examples/mssql/compose.yaml` | Example Mode, [SQL Server](./examples/mssql/README.md) (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 diff --git a/examples/mssql/README.md b/examples/mssql/README.md new file mode 100644 index 0000000..2189fd6 --- /dev/null +++ b/examples/mssql/README.md @@ -0,0 +1,64 @@ +# Example: SQL Server + +A seeded SQL Server Example Source Database. Select it in `.env`: + +```bash +COMPOSE_FILE=docker-compose.yaml:examples/mssql/compose.yaml +``` + +Then `docker compose up --build`. + +> **This connector is Beta**, and it has a sharp edge the others do not — see *Schema changes* +> below before you change any table. + +## Schema changes are not adopted automatically + +SQL Server replicates through **Change Data Capture**, and CDC captures a table's shape at the +moment it is enabled. Alter a table afterwards and replication does not notice: it keeps producing +the old shape, and your new column simply never arrives. Nothing errors. + +Adopting a schema change means redeploying the Sync Config, and re-enabling CDC on the changed +table. In a project whose whole premise is swapping in your own schema, this is the thing most +likely to cost you an afternoon. It is upstream behaviour and cannot be fixed here. + +## What this flavour needs + +The most bootstrap of the four. There is no entrypoint directory to drop SQL into the way Postgres +and MySQL have, so `init-scripts/setup.sql` runs from a one-shot `mssql-setup` container that must +complete before PowerSync starts. It is written to be safe to run more than once. + +- **CDC at database level** — `sys.sp_cdc_enable_db`. +- **A `_powersync_checkpoints` table, with CDC enabled on it.** This is PowerSync's, not yours; it + is how the connector tracks position. +- **CDC per replicated table** — `sys.sp_cdc_enable_table` with `@role_name = N'cdc_reader'`. +- **SQL Server Agent running.** CDC capture and cleanup are Agent jobs. Without the Agent, CDC is + enabled and nothing is ever captured — replication stays silently empty. That is what + `MSSQL_AGENT_ENABLED: "true"` is for. +- **Two permission levels for the replication user.** `VIEW DATABASE PERFORMANCE STATE` in the user + database, and `VIEW SERVER PERFORMANCE STATE` **in `master`**. The second is a server-level + permission on the login; granting only the database-level one leaves replication failing with + *"The user does not have permission to perform this action."* + +The write API connects as `sa`; PowerSync connects as the restricted `powersync_user`. + +SQL Server 2019 or newer, or Azure SQL Database. + +## Running on Apple Silicon + +Microsoft publishes no arm64 image, so this runs under emulation via `platform: linux/amd64`. It +works, but the server is slow to start — the healthcheck allows a long start period for exactly +this reason. The other three flavours run natively. + +## Pointing at your own SQL Server instead + +Switch to Adopter Mode in `.env` and set `DATABASE_URI`. Your database needs everything listed +above. On Azure SQL Database the login is created differently — see PowerSync's docs for +`CREATE USER ... FROM EXTERNAL PROVIDER`. + +## Gotchas + +Ids are `UNIQUEIDENTIFIER`. SQL Server renders them uppercase, so a lowercase id from a client and +its uppercase form in the database are the same value. + +`setup.sql` is idempotent, but the seed data only inserts when the seeded list is absent. Drop the +volume with `docker compose down -v` for a genuinely clean start. diff --git a/examples/mssql/compose.yaml b/examples/mssql/compose.yaml new file mode 100644 index 0000000..336e363 --- /dev/null +++ b/examples/mssql/compose.yaml @@ -0,0 +1,88 @@ +# Example Mode: SQL Server. +# +# The heaviest of the examples. SQL Server replicates through Change Data Capture rather than log +# streaming, so it needs a bootstrap step that the other flavours do not, and SQL Server Agent must +# be running for the CDC capture jobs. +# +# NOTE: relative paths here resolve against the PROJECT directory — the repo root — not against +# this file's own location. + +name: write-api-mssql + +services: + mssql-db: + image: mcr.microsoft.com/mssql/server:2022-latest + # Microsoft publishes no arm64 image, so this runs under emulation on Apple Silicon. It works, + # but it is slow to start — expect the healthcheck to take a while on the first run. + platform: linux/amd64 + restart: always + environment: + ACCEPT_EULA: "Y" + MSSQL_SA_PASSWORD: "Powersync_demo_pw1" + # CDC capture and cleanup run as SQL Agent jobs. Without the Agent, CDC is enabled but + # nothing is ever captured, and replication silently stays empty. + MSSQL_AGENT_ENABLED: "true" + volumes: + - mssql_data:/var/opt/mssql + ports: + - "1433:1433" + healthcheck: + test: + [ + "CMD-SHELL", + "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P \"$$MSSQL_SA_PASSWORD\" -C -Q 'SELECT 1' || exit 1" + ] + interval: 10s + timeout: 10s + retries: 30 + start_period: 60s + + # There is no entrypoint directory to drop SQL into, the way Postgres and MySQL have, so the + # bootstrap is a one-shot container that must complete before PowerSync starts. + mssql-setup: + image: mcr.microsoft.com/mssql/server:2022-latest + platform: linux/amd64 + restart: on-failure + depends_on: + mssql-db: + condition: service_healthy + volumes: + - ./examples/mssql/init-scripts/setup.sql:/scripts/setup.sql:ro + entrypoint: + - /bin/bash + - -c + - '/opt/mssql-tools18/bin/sqlcmd -S mssql-db -U sa -P "Powersync_demo_pw1" -C -b -i /scripts/setup.sql' + + backend: + environment: + DATABASE_TYPE: mssql + DATABASE_URI: mssql://sa:Powersync_demo_pw1@mssql-db:1433/powersync_demo + depends_on: + mssql-setup: + condition: service_completed_successfully + + powersync: + volumes: + - ./examples/mssql/powersync:/config + environment: + # Replication connects as the restricted user, not sa. + PS_DATA_SOURCE_URI: mssql://powersync_user:Powersync_demo_pw1@mssql-db:1433/powersync_demo + depends_on: + mssql-setup: + condition: service_completed_successfully + + frontend: + build: + context: ./frontend + args: + VITE_POWERSYNC_URL: http://localhost:8080 + VITE_BACKEND_URL: http://localhost:6060 + restart: unless-stopped + depends_on: + backend: + condition: service_healthy + ports: + - "5173:4173" + +volumes: + mssql_data: diff --git a/examples/mssql/init-scripts/setup.sql b/examples/mssql/init-scripts/setup.sql new file mode 100644 index 0000000..ddfa6b2 --- /dev/null +++ b/examples/mssql/init-scripts/setup.sql @@ -0,0 +1,103 @@ +-- Safe to run more than once: the setup container may retry while SQL Server finishes starting. +-- +-- SQL Server replicates through Change Data Capture rather than log streaming, so this does more +-- than create tables. CDC must be enabled at database level, then per table, and PowerSync needs +-- its own checkpoints table with CDC enabled on it too. + +IF DB_ID('powersync_demo') IS NULL + EXEC('CREATE DATABASE powersync_demo'); +GO + +USE powersync_demo; +GO + +-- CDC at database level. Everything below depends on this. +IF (SELECT is_cdc_enabled FROM sys.databases WHERE name = 'powersync_demo') = 0 + EXEC sys.sp_cdc_enable_db; +GO + +-- PowerSync's own checkpoints table. Not your data — it is how the connector tracks position. +IF OBJECT_ID('dbo._powersync_checkpoints', 'U') IS NULL + CREATE TABLE dbo._powersync_checkpoints ( + id INT IDENTITY PRIMARY KEY, + last_updated DATETIME NOT NULL DEFAULT GETUTCDATE() + ); +GO + +IF OBJECT_ID('dbo.lists', 'U') IS NULL + CREATE TABLE dbo.lists ( + id UNIQUEIDENTIFIER NOT NULL PRIMARY KEY, + created_at DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(), + name NVARCHAR(MAX) NOT NULL, + owner_id UNIQUEIDENTIFIER NOT NULL + ); +GO + +IF OBJECT_ID('dbo.todos', 'U') IS NULL + CREATE TABLE dbo.todos ( + id UNIQUEIDENTIFIER NOT NULL PRIMARY KEY, + created_at DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(), + completed_at DATETIME2 NULL, + description NVARCHAR(MAX) NOT NULL, + completed BIT NOT NULL DEFAULT 0, + created_by UNIQUEIDENTIFIER NULL, + completed_by UNIQUEIDENTIFIER NULL, + list_id UNIQUEIDENTIFIER NOT NULL, + photo_id UNIQUEIDENTIFIER NULL + ); +GO + +-- Seed data, so the demo client has something to show on first run. +IF NOT EXISTS (SELECT 1 FROM dbo.lists WHERE id = '75F89104-D95A-4F16-8309-5363F1BB377A') +BEGIN + INSERT INTO dbo.lists (id, name, owner_id) + VALUES ('75F89104-D95A-4F16-8309-5363F1BB377A', 'Getting Started', NEWID()); + INSERT INTO dbo.todos (id, description, list_id, completed) + VALUES (NEWID(), 'Run services locally', '75F89104-D95A-4F16-8309-5363F1BB377A', 1); + INSERT INTO dbo.todos (id, description, list_id, completed) + VALUES (NEWID(), 'Create a todo here. Query the todos table over a SQL Server connection. Your todo should be synced', '75F89104-D95A-4F16-8309-5363F1BB377A', 0); +END +GO + +-- CDC per table. @role_name creates the cdc_reader role if it does not exist. +-- NOTE: this captures the table's shape AS IT IS NOW. Alter a table later and replication does not +-- adopt the change on its own — see this example's README. +IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = '_powersync_checkpoints' AND is_tracked_by_cdc = 1) + EXEC sys.sp_cdc_enable_table @source_schema = N'dbo', @source_name = N'_powersync_checkpoints', + @role_name = N'cdc_reader', @supports_net_changes = 0; +GO +IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'lists' AND is_tracked_by_cdc = 1) + EXEC sys.sp_cdc_enable_table @source_schema = N'dbo', @source_name = N'lists', + @role_name = N'cdc_reader', @supports_net_changes = 0; +GO +IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'todos' AND is_tracked_by_cdc = 1) + EXEC sys.sp_cdc_enable_table @source_schema = N'dbo', @source_name = N'todos', + @role_name = N'cdc_reader', @supports_net_changes = 0; +GO + +-- The restricted user PowerSync replicates as. The write API connects as sa in this example. +IF NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = 'powersync_user') + CREATE LOGIN [powersync_user] WITH PASSWORD = 'Powersync_demo_pw1', CHECK_POLICY = ON; +GO +-- VIEW SERVER PERFORMANCE STATE is a SERVER-level permission and must be granted in master to +-- the login, not in the user database. Granting only the database-level equivalent leaves +-- replication failing with "The user does not have permission to perform this action." +USE master; +GO +GRANT VIEW SERVER PERFORMANCE STATE TO [powersync_user]; +GO + +USE powersync_demo; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = 'powersync_user') + CREATE USER [powersync_user] FOR LOGIN [powersync_user]; +GO + +GRANT SELECT ON dbo.lists TO [powersync_user]; +GRANT SELECT ON dbo.todos TO [powersync_user]; +GRANT SELECT, INSERT, UPDATE ON dbo._powersync_checkpoints TO [powersync_user]; +GRANT SELECT ON SCHEMA::cdc TO [powersync_user]; +GRANT VIEW DATABASE PERFORMANCE STATE TO [powersync_user]; +ALTER ROLE cdc_reader ADD MEMBER powersync_user; +GO diff --git a/examples/mssql/powersync/service.yaml b/examples/mssql/powersync/service.yaml new file mode 100644 index 0000000..1dd70e0 --- /dev/null +++ b/examples/mssql/powersync/service.yaml @@ -0,0 +1,31 @@ +# yaml-language-server: $schema=https://unpkg.com/@powersync/service-schema@latest/json-schema/powersync-config.json + +telemetry: + disable_telemetry_sharing: false + +replication: + connections: + - type: mssql + uri: !env PS_DATA_SOURCE_URI + schema: dbo + additionalConfig: + # The demo server uses a self-signed certificate. + trustServerCertificate: true + +storage: + # Still ours, still inside the docker boundary. + type: mongodb + uri: !env PS_MONGO_URI + +port: !env PS_PORT + +sync_config: + path: sync-config.yaml + +client_auth: + jwks_uri: !env PS_JWKS_URL + audience: ["powersync-dev", "powersync"] + +api: + tokens: + - use_a_better_token_in_production diff --git a/examples/mssql/powersync/sync-config.yaml b/examples/mssql/powersync/sync-config.yaml new file mode 100644 index 0000000..e250319 --- /dev/null +++ b/examples/mssql/powersync/sync-config.yaml @@ -0,0 +1,12 @@ +# yaml-language-server: $schema=https://unpkg.com/@powersync/service-sync-rules@latest/schema/sync_rules.json + +config: + edition: 3 + +streams: + global: + # Streams do not subscribe by default — omit this and the client syncs nothing, silently. + auto_subscribe: true + queries: + - SELECT * FROM lists + - SELECT * FROM todos diff --git a/tests/compose.test.ts b/tests/compose.test.ts index a8b0535..419c78a 100644 --- a/tests/compose.test.ts +++ b/tests/compose.test.ts @@ -66,6 +66,17 @@ const MODES: Mode[] = [ databaseType: 'mysql', connectionHost: 'mysql-db:3306' }, + { + name: 'Example Mode: SQL Server', + composeFile: 'docker-compose.yaml:examples/mssql/compose.yaml', + projectName: 'write-api-mssql', + // mssql-setup is the one-shot CDC bootstrap; SQL Server has no entrypoint directory to drop + // SQL into, so it must exist and must complete before PowerSync starts. + services: ['backend', 'frontend', 'mongo', 'mongo-rs-init', 'mssql-db', 'mssql-setup', 'powersync'], + configMount: 'examples/mssql/powersync', + databaseType: 'mssql', + connectionHost: 'mssql-db:1433' + }, { name: 'Example Mode: Postgres', composeFile: 'docker-compose.yaml:examples/postgres/compose.yaml', From 8d9a1471aacfabe482c5461023bb4c2718a469ba Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Wed, 16 Sep 2026 17:17:32 +0200 Subject: [PATCH 13/18] fix: address code review on the self-hosting work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The serious one: Adopter Mode's readable configuration failure was a restart loop. The backend carried restart: unless-stopped, so exit(1) respawned forever — eight times in twenty-five seconds — scrolling the message past repeatedly, and powersync gates on backend: service_healthy, so an unconfigured Adopter Mode hung on a dependency that could never pass. This was verified on the host with tsx and never in the container, which is how it survived. Now restart: on-failure:3: it stops after four attempts with the message as the last thing on screen, while a transient crash still recovers. The boot test could not have caught it, and had a matching flaw: it resolved only on 'close', so a regression that DID start the server left the promise pending until the suite timed out and leaked a listening process. It now kills the child on a deadline and asserts explicitly that it was not still running. Its stack trace assertion also hardcoded a function name, so renaming that function would have made it vacuously true; it now matches the shape of a stack frame. Spawns the local tsx rather than npx, which can reach the network on a cold cache. Four copies had already drifted apart: examples/postgres/powersync/service.yaml came through as a rename and never picked up the schema header or storage comment the other three carry, and the mysql and mssql overlays silently dropped the comment explaining why the client's URLs are localhost. The SQL Server password was written literally four times, including in the bootstrap entrypoint, while the healthcheck two blocks above correctly used the env var. Defined once as an anchor now. Database credentials across the examples are marked as public throwaways the way the signing keys already were. Topology test table had optional fields that every mode sets and every assertion dereferences, so omitting one would have passed against undefined; now required. Resolutions are memoised — the suite was shelling out to Compose once per assertion. A doc comment described a null that cannot occur. Adds the per-flavour smoke procedure the spec promised and I never wrote, as the manual check the topology suite deliberately does not replace. The Postgres one was run verbatim rather than written from memory. The README's "no configuration and no credentials" was untrue — the committed .env is both; it now says there is nothing for you to configure. The layout block showed only one of the four examples. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 17 +++++--- backend/boot.test.ts | 49 +++++++++++++++++------- docker-compose.yaml | 5 ++- examples/mongodb/README.md | 45 ++++++++++++++++++++++ examples/mssql/README.md | 46 ++++++++++++++++++++++ examples/mssql/compose.yaml | 13 ++++++- examples/mysql/README.md | 45 ++++++++++++++++++++++ examples/mysql/compose.yaml | 6 +++ examples/postgres/README.md | 44 +++++++++++++++++++++ examples/postgres/compose.yaml | 3 ++ examples/postgres/powersync/service.yaml | 3 ++ tests/compose.test.ts | 25 ++++++++---- 12 files changed, 271 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 9cb1f3f..2baacf4 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,9 @@ 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 **no configuration and no credentials**: a -seeded Postgres, the PowerSync service, the write API, and a small demo client. +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. ```bash docker compose up --build @@ -90,11 +91,15 @@ write-api/ ├── 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/ -│ ├── compose.yaml # Seeded Postgres + demo client -│ ├── powersync/ # This example's PowerSync config -│ └── init-scripts/ # Demo schema + seed data +│ ├── 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 diff --git a/backend/boot.test.ts b/backend/boot.test.ts index 4f43709..b8f0c6b 100644 --- a/backend/boot.test.ts +++ b/backend/boot.test.ts @@ -13,46 +13,69 @@ import path from 'node:path'; */ const backendDir = path.dirname(fileURLToPath(import.meta.url)); +const tsx = path.join(backendDir, 'node_modules', '.bin', 'tsx'); -const bootWith = (env: Record): Promise<{ code: number | null; output: string }> => +interface Boot { + code: number | null; + output: string; + /** True if it was still running when we gave up — a refusal to start should never be. */ + stillRunning: boolean; +} + +const bootWith = (env: Record): Promise => new Promise((resolve) => { - const child = spawn('npx', ['tsx', 'index.ts'], { + const child = spawn(tsx, ['index.ts'], { cwd: backendDir, - // A port nothing else uses, so a regression that DOES start the server cannot collide with - // a real backend and look like a pass. + // A port nothing else uses, so this cannot bind over a real backend someone is running. env: { ...process.env, PORT: '6098', ...env } }); let output = ''; child.stdout.on('data', (d) => (output += d)); child.stderr.on('data', (d) => (output += d)); - child.on('close', (code) => resolve({ code, output })); + + // Without this, a regression that DOES start the server leaves the promise pending until the + // suite times out, and leaks a listening process. Kill it and report that it was still up. + const deadline = setTimeout(() => { + child.kill('SIGKILL'); + resolve({ code: null, output, stillRunning: true }); + }, 20000); + + child.on('close', (code) => { + clearTimeout(deadline); + resolve({ code, output, stillRunning: false }); + }); }); describe('refusing to start on bad configuration', () => { it('explains that no connection string is configured, and exits non-zero', async () => { - const { code, output } = await bootWith({ DATABASE_URI: '', DATABASE_TYPE: 'postgres' }); + const { code, output, stillRunning } = await bootWith({ + DATABASE_URI: '', + DATABASE_TYPE: 'postgres' + }); + expect(stillRunning).toBe(false); expect(code).not.toBe(0); expect(output).toContain('DATABASE_URI'); // The message must name the fix, not just the fault. expect(output.toLowerCase()).toContain('.env'); - // A raw stack trace is not a readable message. - expect(output).not.toContain('at createConfiguredPersister'); - expect(output).not.toContain('Server is running'); - }, 60000); + // A raw stack trace is not a readable message. Assert on the shape of one rather than on any + // particular frame, so renaming a function cannot quietly make this vacuous. + expect(output).not.toMatch(/^\s+at .+/m); + }, 40000); it('names the supported databases when the type is not one of them', async () => { - const { code, output } = await bootWith({ + const { code, output, stillRunning } = await bootWith({ DATABASE_URI: 'postgres://u:p@h:5432/d', DATABASE_TYPE: 'cassandra' }); + expect(stillRunning).toBe(false); expect(code).not.toBe(0); expect(output).toContain('cassandra'); for (const supported of ['postgres', 'mongodb', 'mysql', 'mssql']) { expect(output).toContain(supported); } - expect(output).not.toContain('Server is running'); - }, 60000); + expect(output).not.toMatch(/^\s+at .+/m); + }, 40000); }); diff --git a/docker-compose.yaml b/docker-compose.yaml index a5bae76..d7be724 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -32,7 +32,10 @@ services: backend: build: context: ./backend - restart: unless-stopped + # Bounded on purpose. A configuration error exits 1, and `unless-stopped` would respawn it + # forever — scrolling the message past and leaving powersync waiting on a healthcheck that can + # never pass. Three attempts still covers a transient crash. + restart: on-failure:3 environment: PORT: "6060" POWERSYNC_URL: powersync-dev diff --git a/examples/mongodb/README.md b/examples/mongodb/README.md index e6a3c00..a0dd83e 100644 --- a/examples/mongodb/README.md +++ b/examples/mongodb/README.md @@ -50,3 +50,48 @@ what makes the round trip work: the id a client invents offline survives into Mo A single write can produce more than one operation in a bucket for the same document. That is the op log behaving normally, not a duplicate — operations apply in order and the client converges on the latest. + +## Verify it end to end + +The automated suite checks the resolved topology, not a live round trip — that is deliberate, since +bringing four flavours up is slow and flaky. This is the manual check it stands in for. + +**1. Everything healthy.** + +```bash +docker compose ps +``` + +**2. Get a token and write through the API.** + +```bash +TOKEN=$(curl -s "http://localhost:6060/api/auth/token?user_id=demo-user" \ + | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])") + +curl -s -X POST http://localhost:6060/api/data \ + -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \ + -d '{"transactions":[{"transaction_id":1,"crud":[{"op":"PUT","table":"todos", + "id":"11111111-1111-1111-1111-111111111111", + "op_data":{"description":"smoke test","completed":false, + "list_id":"75f89104-d95a-4f16-8309-5363f1bb377a"}}]}]}' +``` + +Expect `{"results":[{"status":"success"}]}`. + +**3. Confirm it reached the database.** + +```bash +docker compose exec -T mongo mongosh --quiet --eval \ + 'db.getSiblingDB("powersync_demo_source").todos.find({description:"smoke test"}).toArray()' +``` + +**4. Confirm it syncs back.** + +```bash +curl -sN -m 10 -X POST http://localhost:8080/sync/stream \ + -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \ + -d '{"buckets":[],"include_checksum":true,"raw_data":true}' | grep "smoke test" +``` + +This example starts empty, so step 2 is also how you get your first row. The synced document +carries both `id` and `_id`; that is expected. diff --git a/examples/mssql/README.md b/examples/mssql/README.md index 2189fd6..0ff02c3 100644 --- a/examples/mssql/README.md +++ b/examples/mssql/README.md @@ -62,3 +62,49 @@ its uppercase form in the database are the same value. `setup.sql` is idempotent, but the seed data only inserts when the seeded list is absent. Drop the volume with `docker compose down -v` for a genuinely clean start. + +## Verify it end to end + +The automated suite checks the resolved topology, not a live round trip — that is deliberate, since +bringing four flavours up is slow and flaky. This is the manual check it stands in for. + +**1. Everything healthy.** + +```bash +docker compose ps +``` + +**2. Get a token and write through the API.** + +```bash +TOKEN=$(curl -s "http://localhost:6060/api/auth/token?user_id=demo-user" \ + | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])") + +curl -s -X POST http://localhost:6060/api/data \ + -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \ + -d '{"transactions":[{"transaction_id":1,"crud":[{"op":"PUT","table":"todos", + "id":"11111111-1111-1111-1111-111111111111", + "op_data":{"description":"smoke test","completed":false, + "list_id":"75F89104-D95A-4F16-8309-5363F1BB377A"}}]}]}' +``` + +Expect `{"results":[{"status":"success"}]}`. + +**3. Confirm it reached the database.** + +```bash +docker compose exec -T mssql-db /opt/mssql-tools18/bin/sqlcmd \ + -S localhost -U sa -P "Powersync_demo_pw1" -C -Q \ + "USE powersync_demo; SELECT description FROM dbo.todos WHERE description='smoke test';" +``` + +**4. Confirm it syncs back.** + +```bash +curl -sN -m 10 -X POST http://localhost:8080/sync/stream \ + -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \ + -d '{"buckets":[],"include_checksum":true,"raw_data":true}' | grep "smoke test" +``` + +CDC polls rather than streams, so step 4 can lag a few seconds behind step 3. If it never +arrives, check that SQL Server Agent is running and the CDC capture jobs exist. diff --git a/examples/mssql/compose.yaml b/examples/mssql/compose.yaml index 336e363..7d4cdbf 100644 --- a/examples/mssql/compose.yaml +++ b/examples/mssql/compose.yaml @@ -9,6 +9,11 @@ name: write-api-mssql +# Demo credentials, the same kind of throwaway as the signing keys in .env: public, committed, and +# known to anyone who has cloned this repo. Defined once here so the server, the bootstrap and the +# connection strings cannot drift apart. +x-mssql-sa-password: &sa_password "Powersync_demo_pw1" + services: mssql-db: image: mcr.microsoft.com/mssql/server:2022-latest @@ -18,7 +23,7 @@ services: restart: always environment: ACCEPT_EULA: "Y" - MSSQL_SA_PASSWORD: "Powersync_demo_pw1" + MSSQL_SA_PASSWORD: *sa_password # CDC capture and cleanup run as SQL Agent jobs. Without the Agent, CDC is enabled but # nothing is ever captured, and replication silently stays empty. MSSQL_AGENT_ENABLED: "true" @@ -46,12 +51,14 @@ services: depends_on: mssql-db: condition: service_healthy + environment: + MSSQL_SA_PASSWORD: *sa_password volumes: - ./examples/mssql/init-scripts/setup.sql:/scripts/setup.sql:ro entrypoint: - /bin/bash - -c - - '/opt/mssql-tools18/bin/sqlcmd -S mssql-db -U sa -P "Powersync_demo_pw1" -C -b -i /scripts/setup.sql' + - '/opt/mssql-tools18/bin/sqlcmd -S mssql-db -U sa -P "$$MSSQL_SA_PASSWORD" -C -b -i /scripts/setup.sql' backend: environment: @@ -75,6 +82,8 @@ services: build: context: ./frontend args: + # These resolve in your browser, not on the compose network, so they are localhost — + # unlike every server-to-server URL here. Vite bakes them in at build time. VITE_POWERSYNC_URL: http://localhost:8080 VITE_BACKEND_URL: http://localhost:6060 restart: unless-stopped diff --git a/examples/mysql/README.md b/examples/mysql/README.md index 562bc93..2b2b858 100644 --- a/examples/mysql/README.md +++ b/examples/mysql/README.md @@ -49,3 +49,48 @@ until you drop the volume with `docker compose down -v`. Ids are `CHAR(36)`, not a native UUID type — MySQL has none. The client generates the id offline and it is stored verbatim, which is what makes the round trip work. + +## Verify it end to end + +The automated suite checks the resolved topology, not a live round trip — that is deliberate, since +bringing four flavours up is slow and flaky. This is the manual check it stands in for. + +**1. Everything healthy.** + +```bash +docker compose ps +``` + +**2. Get a token and write through the API.** + +```bash +TOKEN=$(curl -s "http://localhost:6060/api/auth/token?user_id=demo-user" \ + | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])") + +curl -s -X POST http://localhost:6060/api/data \ + -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \ + -d '{"transactions":[{"transaction_id":1,"crud":[{"op":"PUT","table":"todos", + "id":"11111111-1111-1111-1111-111111111111", + "op_data":{"description":"smoke test","completed":false, + "list_id":"75f89104-d95a-4f16-8309-5363f1bb377a"}}]}]}' +``` + +Expect `{"results":[{"status":"success"}]}`. + +**3. Confirm it reached the database.** + +```bash +docker compose exec -T mysql-db mysql -uroot -pmypassword -N -B powersync_demo \ + -e "select description from todos where description='smoke test';" +``` + +**4. Confirm it syncs back.** + +```bash +curl -sN -m 10 -X POST http://localhost:8080/sync/stream \ + -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \ + -d '{"buckets":[],"include_checksum":true,"raw_data":true}' | grep "smoke test" +``` + +If step 3 finds the row but step 4 does not, the binlog settings are the first thing to check: +`select @@log_bin, @@gtid_mode, @@binlog_format, @@binlog_row_image;` diff --git a/examples/mysql/compose.yaml b/examples/mysql/compose.yaml index a8872db..ff156d7 100644 --- a/examples/mysql/compose.yaml +++ b/examples/mysql/compose.yaml @@ -5,6 +5,10 @@ name: write-api-mysql +# Demo credentials: public, committed, and known to anyone who has cloned this repo — the same +# kind of throwaway as the signing keys in .env. The powersync user's password also appears in +# init-scripts/setup.sql, which creates it; the two must match. + services: mysql-db: image: mysql:8.4 @@ -47,6 +51,8 @@ services: build: context: ./frontend args: + # These resolve in your browser, not on the compose network, so they are localhost — + # unlike every server-to-server URL here. Vite bakes them in at build time. VITE_POWERSYNC_URL: http://localhost:8080 VITE_BACKEND_URL: http://localhost:6060 restart: unless-stopped diff --git a/examples/postgres/README.md b/examples/postgres/README.md index a53cf09..ee2c98e 100644 --- a/examples/postgres/README.md +++ b/examples/postgres/README.md @@ -42,3 +42,47 @@ docker compose down -v The seeded list id is fixed (`75f89104-…`) so the seeded todos can reference it. Changing it means changing both. + +## Verify it end to end + +The automated suite checks the resolved topology, not a live round trip — that is deliberate, since +bringing four flavours up is slow and flaky. This is the manual check it stands in for. + +**1. Everything healthy.** + +```bash +docker compose ps +``` + +**2. Get a token and write through the API.** + +```bash +TOKEN=$(curl -s "http://localhost:6060/api/auth/token?user_id=demo-user" \ + | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])") + +curl -s -X POST http://localhost:6060/api/data \ + -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \ + -d '{"transactions":[{"transaction_id":1,"crud":[{"op":"PUT","table":"todos", + "id":"11111111-1111-1111-1111-111111111111", + "op_data":{"description":"smoke test","completed":false, + "list_id":"75f89104-d95a-4f16-8309-5363f1bb377a"}}]}]}' +``` + +Expect `{"results":[{"status":"success"}]}`. + +**3. Confirm it reached the database.** + +```bash +docker compose exec -T pg-db psql -U postgres -d postgres \ + -c "select description from todos where description='smoke test'" +``` + +**4. Confirm it syncs back.** + +```bash +curl -sN -m 10 -X POST http://localhost:8080/sync/stream \ + -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \ + -d '{"buckets":[],"include_checksum":true,"raw_data":true}' | grep "smoke test" +``` + +If step 4 finds nothing, replication is the problem, not the write — check `docker compose logs powersync`. diff --git a/examples/postgres/compose.yaml b/examples/postgres/compose.yaml index c13246a..e1b34e4 100644 --- a/examples/postgres/compose.yaml +++ b/examples/postgres/compose.yaml @@ -8,6 +8,9 @@ name: write-api-postgres +# Demo credentials: public, committed, and known to anyone who has cloned this repo — the same +# kind of throwaway as the signing keys in .env. Replace them before this is anything but a demo. + services: # The Example Source Database. Delete this whole directory when you no longer need it. pg-db: diff --git a/examples/postgres/powersync/service.yaml b/examples/postgres/powersync/service.yaml index 27b065f..7e14901 100644 --- a/examples/postgres/powersync/service.yaml +++ b/examples/postgres/powersync/service.yaml @@ -1,3 +1,5 @@ +# yaml-language-server: $schema=https://unpkg.com/@powersync/service-schema@latest/json-schema/powersync-config.json + telemetry: disable_telemetry_sharing: false @@ -8,6 +10,7 @@ replication: sslmode: disable storage: + # Still ours, still inside the docker boundary. type: mongodb uri: !env PS_MONGO_URI diff --git a/tests/compose.test.ts b/tests/compose.test.ts index 419c78a..587b833 100644 --- a/tests/compose.test.ts +++ b/tests/compose.test.ts @@ -24,11 +24,11 @@ interface Mode { composeFile: string; projectName: string; services: string[]; - /** Repo-relative path expected to be mounted at /config, or null for no example override. */ + /** Repo-relative path expected to be mounted at /config. */ configMount: string; - databaseType?: string; + databaseType: string; /** Substring the write API's and PowerSync's connection strings must both contain. */ - connectionHost?: string; + connectionHost: string; } const ADOPTER_ENV = { @@ -84,17 +84,26 @@ const MODES: Mode[] = [ services: ['backend', 'frontend', 'mongo', 'mongo-rs-init', 'pg-db', 'powersync'], configMount: 'examples/postgres/powersync', databaseType: 'postgres', - connectionHost: 'pg-db' + connectionHost: 'pg-db:5432' } ]; -const resolve = async (composeFile: string) => { - const { stdout } = await run('docker', ['compose', 'config', '--format', 'json'], { +// Resolving is pure, and every assertion below needs the same handful of resolutions. Without +// this the suite shells out to Compose once per assertion. +const cache = new Map>(); + +const resolve = (composeFile: string) => { + const cached = cache.get(composeFile); + if (cached) return cached; + + const pending = run('docker', ['compose', 'config', '--format', 'json'], { cwd: repoRoot, env: { ...process.env, ...ADOPTER_ENV, COMPOSE_FILE: composeFile }, maxBuffer: 10 * 1024 * 1024 - }); - return JSON.parse(stdout); + }).then(({ stdout }) => JSON.parse(stdout)); + + cache.set(composeFile, pending); + return pending; }; const mountedAtConfig = (service: { volumes?: { target: string; source: string }[] }): string[] => From ce9bf15596e5cbfe3c067b550fdc6dae9a7b5063 Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Thu, 17 Sep 2026 11:24:47 +0200 Subject: [PATCH 14/18] docs: remove stale documentation from before containerisation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four documents still described a workflow that no longer exists. backend/README.md: everything from "Running the app" onward predated Compose — cp .env.template && pnpm start on the host, and a whole ngrok section tunnelling to the hosted PowerSync dashboard, which has nothing to do with self-hosting. It also cited port 8000. Replaced with how the backend actually runs, the variables it reads and where they come from, the boot-time configuration failure, the verifier seam, and pnpm generate-keys. The endpoint and error-classification sections were already accurate and are kept. diagram.png: deleted. It showed the pre-batching, pre-auth, pre-container architecture — a request body of { crud: CrudEntry_API[] } where the contract now takes a transaction batch, no auth at all though a Write Token and Verifier both exist, Postgres as the only flavour, and the seed schema at a path it no longer occupies. It was the first thing in the root README, so it was also the first thing to mislead. frontend/README.md: three sentences, two of them wrong. It claimed changes to Postgres sync to clients, true for one of four flavours. Now says plainly that the client is a test fixture bound to the demo schema and absent from Adopter Mode, and documents the dev loop, the batching variables and type generation. backend/.env.template listed variables as though the backend were configured there; in Compose they come from the root .env and the overlays. It now says so, and warns about the port 6060 collision with the container. Also drops file line numbers from self-host-plan.md and fixes frontend's .env.template, which told you to copy a file that has never existed under that name. Batching defaults in the new docs were checked against DemoConnector rather than copied from the old text. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 2 - backend/.env.template | 28 ++++++++--- backend/README.md | 107 ++++++++++++++++------------------------- diagram.png | Bin 231338 -> 0 bytes frontend/.env.template | 6 ++- frontend/README.md | 51 ++++++++++++++++++-- self-host-plan.md | 6 +-- 7 files changed, 119 insertions(+), 81 deletions(-) delete mode 100644 diagram.png diff --git a/README.md b/README.md index 2baacf4..a4d97c5 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ to clients. Clone it, point it at your own database, and change the code. -![Architecture diagram](./diagram.png) - ## Quickstart — see it work This brings up a complete, self-contained system with **nothing for you to configure**: a seeded diff --git a/backend/.env.template b/backend/.env.template index 6c5331f..e5e7665 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -1,8 +1,24 @@ +# You probably do not need this file. +# +# The backend runs in Docker Compose, where these come from the root .env and the example +# overlays. See the root README for the two run modes. +# +# It is here for the unusual case of running the backend directly on the host with `pnpm start` — +# note that this collides with the containerised backend on port 6060, so stop that first. + +# One of: postgres, mongodb, mysql, mssql +DATABASE_TYPE=postgres +# Required. The backend refuses to start without it. +DATABASE_URI= + +PORT=6060 + +# Audience and issuer for the tokens this backend mints. +POWERSYNC_URL=powersync-dev +JWT_ISSUER=powersync-dev + +# Base64-encoded JWKs. Leave blank and a temporary pair is generated at boot — convenient for a +# one-off run, but every restart invalidates tokens PowerSync has already accepted. Generate a +# stable pair with `pnpm generate-keys`. POWERSYNC_PRIVATE_KEY= POWERSYNC_PUBLIC_KEY= -POWERSYNC_URL= -PORT= -JWT_ISSUER= -# Either 'mongodb', 'mysql', 'mssql' or 'postgres'. This defaults to Postgres -DATABASE_TYPE= -DATABASE_URI= diff --git a/backend/README.md b/backend/README.md index 1cdf1b0..9a5b8b4 100644 --- a/backend/README.md +++ b/backend/README.md @@ -34,91 +34,68 @@ supported database maps its driver's errors onto them in its own `*-errors.ts` u ## Packages -[node-postgres](https://github.com/brianc/node-postgres) is used to interact with the Postgres database when a client performs requests to the `/api/data` endpoint. +[node-postgres](https://github.com/brianc/node-postgres), [mongodb](https://www.npmjs.com/package/mongodb), +[mysql2](https://www.npmjs.com/package/mysql2) and [node-mssql](https://www.npmjs.com/package/mssql) back the +four persisters behind `POST /api/data`. [jose](https://github.com/panva/jose) signs and verifies the JWT. -[mongodb](https://www.npmjs.com/package/mongodb) is used to interact with the MongoDB database when a client performs requests to the `/api/data` endpoint. +## Running it -[mysql2](https://www.npmjs.com/package/mysql2) is used to interact with the MySQL database when a client performs requests to the `/api/data` endpoint. +The backend runs in Docker Compose, alongside the PowerSync service and bucket storage. It is not +meant to be started on its own — see the [root README](../README.md) for the two run modes. -[node-mssql](https://www.npmjs.com/package/mssql) is used to connect to a MSSQL database to perform operations from the `/api/data` endpoint. +From the repo root: -[jose](https://github.com/panva/jose) is used to sign the JWT which PowerSync uses for authorization. - -## Requirements - -Based on configuration, this app needs a Postgres, Mongo, MSSQL or MySQL instance. Easiest is probably to use docker containers for these databases. -Hosted free versions that can also be used: - -1. Postgres: For a free version for testing/demo purposes, visit [Supabase](https://supabase.com/). - -## Running the app - -1. Clone the repository -2. Follow the steps outlined in [PowerSync Custom Authentication Example](https://github.com/journeyapps/powersync-jwks-example) → [Generate a key-pair](https://github.com/journeyapps/powersync-jwks-example#1-generate-a-key-pair) to get the keys you need for this app. This is an easy way to get started with this demo app. You can use your own public/private keys as well. Note: This backend will generate a temporary key pair for development purposes if the keys are not present in the `.env` file. This should not be used in production. -3. Create a new `.env` file in the root project directory and add the variables as defined in the `.env` file: - -```shell -cp .env.template .env +```bash +docker compose up --build ``` -4. Install dependancies +To edit backend code without rebuilding the image, append the development overlay to `COMPOSE_FILE` +in the root `.env`: -```shell -nvm use +```bash +COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml:docker-compose.dev.yaml ``` -```shell -pnpm install -``` +Your working tree is mounted in and the process restarts on save. -## Start App +> Running `pnpm start` on the host as well will fail with `address already in use`, or quietly +> shadow the container — both want port 6060. -1. Run the following to start the application +## Configuration -```shell -pnpm start -``` +Set in the root `.env` and in the Compose overlays, not here: -This will start the app on `http://127.0.0.1:PORT`, where PORT is what you specify in your `.env` file. +| Variable | Meaning | +| --- | --- | +| `DATABASE_TYPE` | `postgres`, `mongodb`, `mysql` or `mssql` | +| `DATABASE_URI` | Connection string for the source database | +| `PORT` | Defaults to 6060 | +| `POWERSYNC_URL`, `JWT_ISSUER` | Audience and issuer for the tokens this backend mints | +| `POWERSYNC_PRIVATE_KEY`, `POWERSYNC_PUBLIC_KEY` | Base64 JWKs for signing | -2. Test if the app is working by opening `http://127.0.0.1:PORT/api/auth/token/` in the browser +The backend refuses to start, before serving any traffic, if `DATABASE_URI` is unset or +`DATABASE_TYPE` is not one of the four — with a message naming the fix rather than a stack trace. -3. You should get a JSON object as the response to that request +If no keypair is configured it generates a temporary one at boot. That is fine for a one-off run +and wrong for everything else: every restart mints a new key, and PowerSync rejects tokens it +accepted moments earlier with `PSYNC_S2101 — Could not find an appropriate key in the keystore`. +The repo ships a committed throwaway pair so this does not happen. To mint your own: -## Connecting the app with PowerSync - -This process is only designed for demo/testing purposes, and is not intended for production use. You won't be using ngrok to host your application and database. - -1. Download and install [ngrok](https://ngrok.com/) -2. Run the ngrok command to create a HTTPS tunnel to your local application - -```shell -ngrok http 8000 +```bash +pnpm generate-keys ``` -This should create the tunnel and a new HTTPS URL should be availible e.g. +## Using your own identity provider -```shell -ngrok by @inconshreveable (Ctrl+C to quit) +`src/auth/verifier.ts` is the seam. The demo verifies the same token this backend mints; replace +that export to accept tokens from Supabase, Clerk, Auth0 or anything else. Worked examples are in +[auth-verifiers.md](../auth-verifiers.md). -Session Status online -Account Michael Barnes (Plan: Free) -Update update available (version 2.3.41, Ctrl-U to update) -Version 2.3.40 -Region United States (us) -Web Interface http://127.0.0.1:4040 -Forwarding http://your_id.ngrok-free.app -> http://localhost:8000 -Forwarding https://your_id.ngrok-free.app -> http://localhost:8000 +## Tests -Connections ttl opn rt1 rt5 p50 p90 - 1957 0 0.04 0.03 0.01 89.93 -``` - -3. Open the [PowerSync Dashboard](https://powersync.journeyapps.com/) and paste the `Forwarding` URL starting with HTTPS into the Credentials tab of your PowerSync instance e.g. - -``` -JWKS URI -https://your_id.ngrok-free.app/api/auth/keys/ +```bash +pnpm test # HTTP against the assembled app, plus the boot-failure contract +pnpm check # types ``` -Pay special attention to the URL, it should include the `/api/auth/keys/` path as this is used by the PowerSync server to validate tokens. +Neither needs Docker. diff --git a/diagram.png b/diagram.png deleted file mode 100644 index 85d9a6a312f264d65bc55a1e5c0303c2d30fce46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 231338 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�V`;P&`Q$oH<*|ruGdd9*-T6xHq589;K?o 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%0D;8zXL1K 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 diff --git a/frontend/.env.template b/frontend/.env.template index fa00f38..2b377fb 100644 --- a/frontend/.env.template +++ b/frontend/.env.template @@ -1,5 +1,7 @@ -# Copy this template: `cp .env.local.template .env.local` -# Edit .env.local and enter your project details. +# Only needed to run the client on the host with `pnpm dev`. In Docker these are supplied as build +# args by the example overlay, because Vite inlines VITE_* variables at build time. +# +# cp .env.template .env.local VITE_BACKEND_URL=http://localhost:6060 VITE_POWERSYNC_URL=http://localhost:8080 diff --git a/frontend/README.md b/frontend/README.md index c9a42b4..9d94a84 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,7 +1,52 @@ -# PowerSync Self hosted demo app +# Demo client -This is a small demonstration for connecting to a self hosted PowerSync instance. Changes made to the Postgres server should be synced to clients. +A small React/Vite app that exercises the write path: it queues local changes, uploads them to the +write API as a transaction batch, and syncs the results back from PowerSync. + +**This is a test fixture, not a starting point.** It is bound to the demo `lists` and `todos` +schema, so it only works against a bundled example — Adopter Mode does not run it. Bring your own +client for anything real. + +## Running it + +It comes up with any of the examples, at http://localhost:5173: + +```bash +docker compose up --build # from the repo root +``` + +In the container it is a production build with its URLs baked in at build time, because Vite +inlines `VITE_*` variables. For a loop that reloads on save and reads configuration at runtime: + +```bash +pnpm install && pnpm dev +``` + +That reads `.env.local` — copy `.env.template` to create it. ## Authentication -This essentially uses anonymous authentication. A random user ID is generated and stored in local storage. The backend returns a valid token which is not linked to a specific user. All data is synced to all users. +Effectively anonymous. A random user id is generated and stored in local storage, and the backend +returns a valid token that is not tied to a specific user. Every client syncs the same data. + +Swapping this for a real identity provider is a backend concern — see +[auth-verifiers.md](../auth-verifiers.md). + +## Upload behaviour + +`.env.template` documents the batching knobs. There is one upload path and one endpoint +(`POST /api/data`), which always takes an ordered run of whole transactions; the variables only +bound how much of the queue goes in each request: + +- `VITE_BATCH_MAX_TRANSACTIONS` — transactions per request, default 10 +- `VITE_BATCH_MAX_OPERATIONS` — operation ceiling, whichever bound is hit first, default 1000 +- `VITE_BATCH_ON_FATAL_ERROR` — `stop` (default) ends the batch at a fatal failure; `skip` drops + that transaction and continues, so a queue blocked by a poison operation can still drain + +## Types + +Generated from the shared contract at `backend/openapi.yaml`: + +```bash +pnpm generate +``` diff --git a/self-host-plan.md b/self-host-plan.md index 557232d..45c00b5 100644 --- a/self-host-plan.md +++ b/self-host-plan.md @@ -190,9 +190,9 @@ starts cleanly and then answers every request with Four edits: -- `backend/app.ts:27` — `path.join(__dirname, '..', 'openapi.yaml')` -> `path.join(__dirname, 'openapi.yaml')` -- `backend/package.json:13` — `../openapi.yaml` -> `./openapi.yaml` -- `frontend/package.json:6` — `../openapi.yaml` -> `../backend/openapi.yaml` +- `backend/app.ts` — the spec path the validator loads, from `../openapi.yaml` to alongside `app.ts` +- `backend/package.json` — the type-generation script's input +- `frontend/package.json` — the type-generation script's input, now reaching across into `backend/` - `README.md:41` — wording ## Dev loop From 9b238584f83e3eb26dcdfb3003f2a53917061611 Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Thu, 17 Sep 2026 11:52:30 +0200 Subject: [PATCH 15/18] docs: list the DATABASE_TYPE options where they are actually needed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The root .env is the file an adopter edits, and it offered `# DATABASE_TYPE=postgres` with no indication that three other values exist. The options were only written down in backend/README.md and backend/.env.template, neither of which someone configuring Adopter Mode necessarily opens. Also records the spelling trap: PowerSync's replication config in config/service.yaml calls the Postgres connector `postgresql`, while this variable is read by the write API and spells it `postgres`. Two adjacent config files, same concept, different spelling. The failure is loud rather than silent — the backend refuses to start and lists what it supports — but the note saves the trip. Adds a connection-string shape per type, and the reminder that a database on the host is reachable at host.docker.internal rather than localhost. All four shapes are the ones the working examples actually use. Co-Authored-By: Claude Opus 5 (1M context) --- .env | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.env b/.env index e582c1b..8b55c07 100644 --- a/.env +++ b/.env @@ -12,6 +12,22 @@ COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml # COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml:docker-compose.dev.yaml # Adopter Mode only. Example Mode overrides both of these with the bundled database. +# +# DATABASE_TYPE is one of: postgres, mongodb, mysql, mssql +# +# Careful: PowerSync's own replication config in config/service.yaml spells the Postgres +# connector `postgresql`. This variable is read by the write API, which spells it `postgres`. +# Get it wrong and the backend refuses to start and tells you the supported values. +# +# DATABASE_URI shapes, one per type: +# postgres postgres://user:password@host:5432/database +# mongodb mongodb://user:password@host:27017/database +# mysql mysql://user:password@host:3306/database +# mssql mssql://user:password@host:1433/database +# +# A database running on this machine rather than in Docker is reachable at host.docker.internal, +# not localhost — inside a container, localhost is the container. +# # DATABASE_TYPE=postgres # DATABASE_URI= From dc84dd9c09661078f5b73f01dff5e168edd90d91 Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Thu, 17 Sep 2026 12:06:37 +0200 Subject: [PATCH 16/18] docs: put source database prerequisites where they survive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopter Mode told you to set DATABASE_URI and fill in a sync config, but never that your database needs its change feed turned on first — no publication for Postgres, no binlog settings for MySQL, no CDC for SQL Server, no replica set for MongoDB. Skipping any of them produces an empty app rather than an error, which is the worst way to learn. The per-flavour prerequisites were documented, but only inside examples/, which the same README invites you to delete. Deleting it as intended removed the only place stating that a Postgres source without a publication replicates nothing. The Adopter Mode section now carries a table of what must be true of your database for each flavour, and says plainly that it is the version which survives deleting examples/. The example READMEs keep the worked SQL and the managed-hosting wrinkles. MongoDB also gained the "pointing at your own instance instead" section the other three already had: replica set requirements, post-images and privileges, the note that Azure DocumentDB shares the connector but does not support post-images, and that bucket storage stays in our container even though the bundled example shares one Mongo process between source and storage. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 16 ++++++++++++++++ examples/mongodb/README.md | 17 +++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/README.md b/README.md index a4d97c5..25c2f96 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,22 @@ 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** | `wal_level=logical`; 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//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. diff --git a/examples/mongodb/README.md b/examples/mongodb/README.md index a0dd83e..bd61c44 100644 --- a/examples/mongodb/README.md +++ b/examples/mongodb/README.md @@ -42,6 +42,23 @@ Without the projection documents still sync, but arrive without the identifier t system keys on. `*` carries `_id` through as well — harmless, since the client ignores columns it does not declare. +## Pointing at your own MongoDB instead + +You do not need this example for that. Switch to Adopter Mode in `.env` and set `DATABASE_URI` to +your own server. The requirements above still apply: + +- **A replica set.** Atlas gives you one automatically. A self-managed single node needs + `rs.initiate()` before change streams or transactions work at all. +- **Post-images.** Set `post_images: auto_configure` in `config/service.yaml`, and make sure the + replication user can configure `changeStreamPreAndPostImages` on the replicated collections. +- **Privileges.** `changeStream` at database level, plus read on the collections you replicate. + +**Azure DocumentDB** uses this same connector, but does **not** support post-images — set +`post_images: off` there. + +Bucket storage does not go into your server. Even here, where the example shares one Mongo process +between source and storage, Adopter Mode keeps storage in a container this project owns. + ## Gotchas The write API stores `_id` as the **string** id the client generated, not an `ObjectId`. That is From 7e6e0e23d4b75806e4c4fe1ece4e79a058674949 Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Thu, 17 Sep 2026 14:15:58 +0200 Subject: [PATCH 17/18] Cleanup. Added test sheet. --- .env | 5 +- README.md | 49 +- .../components/providers/SystemProvider.tsx | 1 - test.txt | 710 ++++++++++++++++++ 4 files changed, 724 insertions(+), 41 deletions(-) create mode 100644 test.txt diff --git a/.env b/.env index 8b55c07..1da5c41 100644 --- a/.env +++ b/.env @@ -11,8 +11,6 @@ COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml # Append :docker-compose.dev.yaml to any of the above for the development loop, e.g. # COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml:docker-compose.dev.yaml -# Adopter Mode only. Example Mode overrides both of these with the bundled database. -# # DATABASE_TYPE is one of: postgres, mongodb, mysql, mssql # # Careful: PowerSync's own replication config in config/service.yaml spells the Postgres @@ -27,7 +25,8 @@ COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml # # A database running on this machine rather than in Docker is reachable at host.docker.internal, # not localhost — inside a container, localhost is the container. -# + +# Adopter Mode only. Example Mode overrides both of these with the bundled database. # DATABASE_TYPE=postgres # DATABASE_URI= diff --git a/README.md b/README.md index 25c2f96..25507e1 100644 --- a/README.md +++ b/README.md @@ -42,11 +42,11 @@ DATABASE_URI=postgres://user:password@your-host:5432/your-db 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** | `wal_level=logical`; 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. | +| 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//README.md` has the worked SQL and the managed-hosting wrinkles. Those @@ -74,13 +74,13 @@ In Adopter Mode there is no bundled database and no demo client. Bring your own 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](./examples/postgres/README.md) | -| `docker-compose.yaml:examples/mongodb/compose.yaml` | Example Mode, [MongoDB](./examples/mongodb/README.md) | -| `docker-compose.yaml:examples/mysql/compose.yaml` | Example Mode, [MySQL](./examples/mysql/README.md) (Beta) | -| `docker-compose.yaml:examples/mssql/compose.yaml` | Example Mode, [SQL Server](./examples/mssql/README.md) (Beta) | -| `docker-compose.yaml` | Adopter Mode, your database | +| `.env` line | What runs | +| ---------------------------------------------------- | ------------------------------------------------------------- | +| `docker-compose.yaml:examples/postgres/compose.yaml` | Example Mode, [Postgres](./examples/postgres/README.md) | +| `docker-compose.yaml:examples/mongodb/compose.yaml` | Example Mode, [MongoDB](./examples/mongodb/README.md) | +| `docker-compose.yaml:examples/mysql/compose.yaml` | Example Mode, [MySQL](./examples/mysql/README.md) (Beta) | +| `docker-compose.yaml:examples/mssql/compose.yaml` | Example Mode, [SQL Server](./examples/mssql/README.md) (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. @@ -159,20 +159,6 @@ 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. -## Tests - -```bash -pnpm install && pnpm test # repo root: the resolved compose topology -cd backend && pnpm test # the write API -``` - -The root suite asks Compose to *resolve* each mode rather than run it — no containers start and no -images are pulled, so it takes about a second. It exists because the two mechanisms holding the -mode switch together fail silently: if an example's config mount appended to the base's instead of -replacing it, the stack would come up perfectly healthy pointing at the wrong sync rules. - -The backend's own suite needs no Docker. - ## Generating types from the contract Both packages generate TypeScript from `backend/openapi.yaml`: @@ -181,14 +167,3 @@ 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 ``` - -## Troubleshooting - -**`ports are not available: ... 6060: bind: address already in use`** — something on the host is -already using the port, commonly a backend started with `pnpm start`. Stop it; the containerised -backend needs 6060. - -**Sync config changes do nothing** — the service reads them at boot. `docker compose restart powersync`. - -**Schema or seed changes do nothing** — init scripts only run on a database's first start. -`docker compose down -v` to drop the volume, then up again. diff --git a/frontend/src/components/providers/SystemProvider.tsx b/frontend/src/components/providers/SystemProvider.tsx index afa23ca..d0706e0 100644 --- a/frontend/src/components/providers/SystemProvider.tsx +++ b/frontend/src/components/providers/SystemProvider.tsx @@ -11,7 +11,6 @@ export const db = new PowerSyncDatabase({ database: { dbFilename: 'example.db' }, - crudUploadThrottleMs: 10000, schema: AppSchema, logger: Logger }); diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..d93248a --- /dev/null +++ b/test.txt @@ -0,0 +1,710 @@ +================================================================================ +MANUAL TEST PLAN — write-api self-hosting starter +================================================================================ + +Every avenue the project claims to support, in an order that builds on itself. +Work top to bottom, or jump to a section — each one says what state it assumes. + +Tick the [ ] as you go. Where a step has an expected result, it is marked with +"->". If something differs from the "->" line, that is the bug, whether the +fault is in the code or in the documentation. + + +-------------------------------------------------------------------------------- +BEFORE YOU START +-------------------------------------------------------------------------------- + +[ ] Docker Desktop running, with at least 6 GB of memory allocated. SQL Server + alone wants ~2 GB and runs under emulation on Apple Silicon. + +[ ] Working from a clean checkout of this branch, at the repo root. + +[ ] Nothing already on ports 5173, 6060, 8080, 5432, 3306 or 1433: + + lsof -nP -iTCP:6060 -iTCP:8080 -iTCP:5173 -sTCP:LISTEN + + -> no output. If a stray backend from a previous run is there, kill it. + +[ ] Know the escape hatch. Any time you want to start over from nothing: + + docker compose down -v --remove-orphans + docker ps -aq --filter "name=write-api-" | xargs -r docker rm -f + + The second line matters because each mode is its own Compose project; + `down` only reaches the project currently named in .env. + +Rough timings on an M-series laptop, cold (image pulls included): + postgres ~1 min mongodb ~1 min + mysql ~2 min mssql ~5 min, and that is not a hang + + +-------------------------------------------------------------------------------- +1. QUICKSTART — THE PATH IN THE README +-------------------------------------------------------------------------------- + +Assumes: nothing running. This is the "clone it and see it work" claim. + +[ ] Confirm .env is untouched from the checkout: + + grep '^COMPOSE_FILE' .env + + -> COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml + +[ ] Bring it up with no other configuration at all: + + docker compose up --build + + -> images build, four containers plus the client come up, and the log + settles. You are looking for the write API's "Server is running" and + PowerSync reaching "Replication active" / no restart loop. + +[ ] In a second terminal: + + curl -s http://localhost:6060/ + curl -s http://localhost:8080/probes/liveness + + -> {"message":"backend"} and a 200. + +[ ] Open http://localhost:5173 + + -> a todo list named "Getting Started" with two items, one ticked. If the + page loads but the list is empty, sync is broken — not the client. + +[ ] Add a todo in the UI. + + -> it appears immediately (local write), and stays after a page refresh + (it round-tripped through the write API, into Postgres, and back out + through replication). + +[ ] Prove it really reached the source database, not just local storage: + + docker compose exec pg-db psql -U postgres -d postgres \ + -c "select description, completed from todos order by created_at desc limit 5;" + + -> your new todo is the top row. + +[ ] Tick the todo's checkbox in the UI, re-run that query. + + -> completed flipped to t. (PUT and PATCH both go through /api/data.) + +[ ] Delete a todo in the UI, re-run the query. + + -> the row is gone. That is the third CRUD op exercised. + +Leave this running for section 2. + + +-------------------------------------------------------------------------------- +2. THE API DIRECTLY, WITHOUT THE CLIENT +-------------------------------------------------------------------------------- + +Assumes: section 1 is up. This is how an adopter with their own client will +actually drive it. + +[ ] Mint a token: + + TOKEN=$(curl -s "http://localhost:6060/api/auth/token?user_id=manual" \ + | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])") + echo "${TOKEN:0:24}..." + + -> a JWT prefix, not empty. + +[ ] Check the key the service verifies against is being served: + + curl -s http://localhost:6060/api/auth/keys + + -> a JWKS document with one key, and a "kid". + +[ ] Write a transaction batch: + + curl -s -X POST http://localhost:6060/api/data \ + -H "Authorization: Bearer $TOKEN" \ + -H 'content-type: application/json' \ + -d '{"transactions":[{"transaction_id":1,"crud":[ + {"op":"PUT","table":"todos","id":"11111111-1111-1111-1111-111111111111", + "op_data":{"description":"manual test row","completed":false, + "list_id":"75f89104-d95a-4f16-8309-5363f1bb377a"}}]}]}' + + -> {"status":"success",...} + +[ ] Read it back out of the sync stream — this proves persistence AND + replication in one shot: + + curl -sN -m 15 -X POST http://localhost:8080/sync/stream \ + -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \ + -d '{"buckets":[],"include_checksum":true,"raw_data":true}' \ + | head -c 20000 | grep -c "manual test row" + + -> 1 or more. (Ctrl-C if it hangs after printing; the stream stays open by + design.) + +[ ] Refresh http://localhost:5173 + + -> "manual test row" is in the list. A write made outside the client showed + up inside it. + +[ ] Now the contract. Send a request that violates openapi.yaml: + + curl -s -X POST http://localhost:6060/api/data \ + -H "Authorization: Bearer $TOKEN" \ + -H 'content-type: application/json' \ + -d '{"transactions":"not-an-array"}' + + -> a 400 naming the offending field, from express-openapi-validator. Not a + 500, and not a success. + +[ ] And auth. Same request, no token: + + curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:6060/api/data \ + -H 'content-type: application/json' -d '{"transactions":[]}' + + -> 401. + +[ ] Tear down, keeping the volume: + + docker compose down + + +-------------------------------------------------------------------------------- +3. THE OTHER THREE EXAMPLE DATABASES +-------------------------------------------------------------------------------- + +Assumes: nothing running. Repeat this block per flavour. The point is that +switching source database is ONE line in .env. + +For each flavour below: + + a. Bring the current mode down FIRST (see section 4 for why): + docker compose down -v + + b. Edit .env: comment out the current COMPOSE_FILE line, uncomment the one + for this flavour. Exactly one uncommented. + + c. docker compose up --build + + d. Wait for health: + curl -s http://localhost:6060/ && curl -s http://localhost:8080/probes/liveness + + e. Open http://localhost:5173, add a todo, refresh. + + f. Run the flavour's verification query below. + +--- 3a. MongoDB ----------------------------------------------------------------- + + COMPOSE_FILE=docker-compose.yaml:examples/mongodb/compose.yaml + +[ ] Note what is NOT there: no source database container. Replication points at + a second database on the same Mongo server that holds bucket storage. + + docker compose ps --services + + -> mongo, mongo-rs-init, backend, powersync, frontend. No "mongo-db". + +[ ] Open the client. + + -> EMPTY. This is the one flavour that ships no seed data, deliberately. + An empty list here is correct, not a sync failure. + +[ ] Create a list in the UI, then add a todo to it. + +[ ] Verify: + + docker compose exec mongo mongosh powersync_demo_source --quiet \ + --eval 'db.todos.find({}, {description:1, completed:1}).toArray()' + + -> your todo, with an _id. + +[ ] Confirm the two stores are genuinely separate databases: + + docker compose exec mongo mongosh --quiet --eval 'db.adminCommand({listDatabases:1}).databases.map(d=>d.name)' + + -> both powersync_demo_source and powersync_bucket_storage listed, distinct. + +[ ] Confirm the _id -> id projection works (this is what the sync rules do): + the todo you created is visible in the client after a refresh. If documents + sync but the client shows nothing, the projection is the suspect. + +--- 3b. MySQL (Beta) ------------------------------------------------------------ + + COMPOSE_FILE=docker-compose.yaml:examples/mysql/compose.yaml + +[ ] Client shows "Getting Started" with two todos. Add one. + +[ ] Verify: + + docker compose exec mysql-db mysql -uroot -pmypassword powersync_demo \ + -e "select description, completed from todos;" + +[ ] Confirm the binlog prerequisites actually took (this is what the root + README's table demands of an adopter's own MySQL): + + docker compose exec mysql-db mysql -uroot -pmypassword \ + -e "select @@log_bin, @@gtid_mode, @@enforce_gtid_consistency, @@binlog_format, @@binlog_row_image;" + + -> 1, ON, ON, ROW, FULL. + +--- 3c. SQL Server (Beta) ------------------------------------------------------- + + COMPOSE_FILE=docker-compose.yaml:examples/mssql/compose.yaml + +[ ] Be patient on first start. The mssql-setup container must finish before + PowerSync starts; watch it: + + docker compose logs -f mssql-setup + + -> it runs the CDC bootstrap and exits 0. If it exits non-zero, nothing + downstream will work and that log is the whole story. + +[ ] Client shows "Getting Started" with two todos. Add one. + +[ ] Verify: + + docker compose exec mssql-db /opt/mssql-tools18/bin/sqlcmd \ + -S localhost -U sa -P 'Powersync_demo_pw1' -C -d powersync_demo \ + -Q "select description, completed from dbo.todos" + +[ ] Confirm CDC is on AND the Agent is running — the pair of things that fail + silently together: + + docker compose exec mssql-db /opt/mssql-tools18/bin/sqlcmd \ + -S localhost -U sa -P 'Powersync_demo_pw1' -C -d powersync_demo \ + -Q "select name, is_cdc_enabled from sys.databases where name='powersync_demo'" + + -> is_cdc_enabled = 1. + +[ ] Finish with: + + docker compose down -v + + +-------------------------------------------------------------------------------- +4. SWITCHING MODES — INCLUDING GETTING IT WRONG +-------------------------------------------------------------------------------- + +Assumes: nothing running. This tests the warning in the README rather than the +happy path, because this is the mistake everyone makes once. + +[ ] Select postgres in .env and bring it up: docker compose up -d + +[ ] WITHOUT bringing it down, edit .env to select mysql, then: + + docker compose up -d + + -> it fails with something like + "Bind for 0.0.0.0:6060 failed: port is already allocated". + The postgres containers are still running, under their own project name. + +[ ] Confirm both projects exist: + + docker ps --format '{{.Names}}' | sort + + -> write-api-postgres-* containers still up. + +[ ] Recover the documented way: switch .env BACK to postgres, down, then + switch forward: + + # .env -> postgres + docker compose down + # .env -> mysql + docker compose up -d + + -> clean start. + +[ ] Confirm the projects really are isolated (so switching never reuses the + previous flavour's data): + + docker volume ls | grep write-api + + -> volumes are namespaced per project, e.g. write-api-postgres_pg_data and + write-api-mysql_mysql_data. + +[ ] Now the explicit form, with no .env involvement at all: + + docker compose down + docker compose -f docker-compose.yaml -f examples/postgres/compose.yaml up -d + curl -s http://localhost:6060/ + + -> works identically. Then: + docker compose -f docker-compose.yaml -f examples/postgres/compose.yaml down + + +-------------------------------------------------------------------------------- +5. CHANGING THE BACKEND CODE +-------------------------------------------------------------------------------- + +Assumes: nothing running. Two loops are claimed — the fast one and the +deployment-shaped one. Test both. + +--- 5a. The development overlay (no rebuild) ----------------------------------- + +[ ] In .env, append the dev overlay to the postgres line: + + COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml:docker-compose.dev.yaml + +[ ] docker compose up --build (the build is only needed the first time) + +[ ] Confirm the backend is running under watch, not the built image: + + docker compose logs backend | tail -20 + + -> tsx watch output, and "Server is running". + +[ ] Edit backend/app.ts — change the root route's body: + + res.status(200).send({ message: 'backend' }); + -> + res.status(200).send({ message: 'edited live' }); + +[ ] Save, wait ~2 seconds, then: + + curl -s http://localhost:6060/ + + -> {"message":"edited live"} with NO rebuild and NO restart command. + +[ ] Confirm node_modules were not clobbered by the bind mount (the macOS + native-binary trap): + + docker compose exec backend node -e "require('pg'); console.log('deps ok')" + + -> deps ok. A crash here means the host's node_modules leaked in. + +[ ] Revert the edit, confirm it reverts live. + +[ ] docker compose down + +--- 5b. The rebuild path ------------------------------------------------------- + +[ ] Remove the dev overlay from the .env line, leaving plain postgres mode. + +[ ] docker compose up -d --build then curl -s http://localhost:6060/ + + -> {"message":"backend"} — the original, from the image. + +[ ] Edit backend/app.ts again to 'rebuilt', then WITHOUT rebuilding: + + docker compose restart backend + curl -s http://localhost:6060/ + + -> still {"message":"backend"}. The image is what runs; a restart is not + enough. This is the point of the distinction. + +[ ] Now rebuild: + + docker compose up -d --build backend + curl -s http://localhost:6060/ + + -> {"message":"rebuilt"}. + +[ ] Revert the edit and rebuild once more so the tree is clean. + +--- 5c. The client's own loop -------------------------------------------------- + +[ ] With a mode up in Docker, run the client on the host instead: + + cd frontend + cp .env.template .env.local + pnpm install && pnpm dev + + -> http://localhost:5173 (Vite's own port) serves the same app, reading + .env.local at runtime, reloading on save. Note the containerised client + may also be on 5173 — stop it first, or expect Vite to pick 5174. + +[ ] Edit a label in the client source, save. + + -> the browser updates without a rebuild. + + +-------------------------------------------------------------------------------- +6. ADOPTER MODE — YOUR OWN DATABASE +-------------------------------------------------------------------------------- + +Assumes: nothing running. This is the real product; sections 1-3 are scaffolding. +We fake "your own database" with a Postgres that this project does not manage. + +[ ] Start a database OUTSIDE the project, on a different port: + + docker run -d --name adopter-db -p 5433:5432 \ + -e POSTGRES_PASSWORD=adopterpw -e POSTGRES_DB=adopter \ + postgres:18 -c wal_level=logical + +[ ] Prepare it the way the root README's table demands — schema, then the + publication that is easy to forget: + + docker exec adopter-db psql -U postgres -d adopter -c \ + "create table widgets (id uuid primary key default gen_random_uuid(), name text not null, owner_id uuid)" + + docker exec adopter-db psql -U postgres -d adopter -c \ + "insert into widgets (name, owner_id) values ('first widget', gen_random_uuid())" + + docker exec adopter-db psql -U postgres -d adopter -c \ + "create publication powersync for table widgets" + +[ ] Point the project at it. In .env: + + COMPOSE_FILE=docker-compose.yaml + DATABASE_TYPE=postgres + DATABASE_URI=postgres://postgres:adopterpw@host.docker.internal:5433/adopter + + (host.docker.internal, not localhost — inside a container localhost is the + container.) + +[ ] Describe the schema in config/sync-config.yaml — replace the commented + example under `streams:` with: + + streams: + widgets: + auto_subscribe: true + queries: + - SELECT * FROM widgets + +[ ] docker compose up --build + + -> backend, powersync, mongo (bucket storage) only. NO source database + container and NO client: + + docker compose ps --services + +[ ] Prove the round trip without a client: + + TOKEN=$(curl -s "http://localhost:6060/api/auth/token?user_id=manual" \ + | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])") + + curl -sN -m 15 -X POST http://localhost:8080/sync/stream \ + -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \ + -d '{"buckets":[],"include_checksum":true,"raw_data":true}' \ + | head -c 20000 | grep -c "first widget" + + -> 1 or more. Your own row, replicating. + +[ ] Write into your own table through the API: + + curl -s -X POST http://localhost:6060/api/data \ + -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \ + -d '{"transactions":[{"transaction_id":1,"crud":[ + {"op":"PUT","table":"widgets","id":"22222222-2222-2222-2222-222222222222", + "op_data":{"name":"written by the API"}}]}]}' + + docker exec adopter-db psql -U postgres -d adopter -c "select name from widgets;" + + -> "written by the API" is there. + +[ ] THE IMPORTANT ONE — confirm we did not colonise your database: + + docker exec adopter-db psql -U postgres -d adopter -c "\dn" + docker exec adopter-db psql -U postgres -d adopter \ + -c "select tablename from pg_tables where schemaname='public';" + + -> only your own schema and the widgets table. No powersync_* anything. + Bucket storage lives in this project's mongo container; check it is + genuinely there: + + docker compose exec mongo mongosh --quiet \ + --eval 'db.adminCommand({listDatabases:1}).databases.map(d=>d.name)' + + -> powersync_bucket_storage present. + +[ ] Clean up when done with section 6 (keep adopter-db for section 7 if you + want, it costs nothing): + + docker compose down -v + + +-------------------------------------------------------------------------------- +7. FAILURE PATHS — THE MESSAGES SOMEONE WILL ACTUALLY HIT +-------------------------------------------------------------------------------- + +Assumes: Adopter Mode selected in .env, nothing running. Each of these is a +mistake the docs promise to catch. If a message is unhelpful, that is a finding. + +[ ] Missing URI. Comment out DATABASE_URI in .env, then: + + docker compose up backend + + -> the backend exits, having printed a message that names DATABASE_URI, + shows the URI shape, AND offers the example-mode alternative. It must + NOT loop forever: it gives up after a handful of attempts so the message + stays on screen. + +[ ] Unsupported type. Restore DATABASE_URI, set DATABASE_TYPE=postgresql + (the near-miss spelling PowerSync's own config uses), then: + + docker compose up backend + + -> a message saying postgresql is not supported and listing exactly: + mongodb, mssql, mysql, postgres. Set it back to postgres afterwards. + +[ ] Empty sync rules. Blank out the `streams:` entries you added to + config/sync-config.yaml (leave the key with nothing under it), then: + + docker compose up + + -> PowerSync restarts in a loop logging that 'streams' are required. This + is documented as expected, not a bug — confirm the log actually says + something you could act on. Restore the file afterwards. + +[ ] Forgetting auto_subscribe. Restore the stream but DELETE the + `auto_subscribe: true` line, then restart: + + docker compose up -d + docker compose restart powersync + # re-run the sync stream curl from section 6 + + -> zero rows come back, and NOTHING anywhere reports an error. This is the + silent failure the docs warn about; confirm with your own eyes that it + is silent, because that is why the warning exists. Restore the line. + +[ ] Unstable signing keys. Blank POWERSYNC_PRIVATE_KEY and POWERSYNC_PUBLIC_KEY + in .env, then: + + docker compose up -d + # mint a token, then: + docker compose restart backend + # use the OLD token against the sync stream + + -> PSYNC_S2101 "Could not find an appropriate key in the keystore". Now + generate a stable pair and confirm the failure goes away: + + cd backend && pnpm generate-keys + + -> prints both values; paste them into .env, `docker compose up -d`, and a + token survives a backend restart. Restore the committed pair when done, + or keep the new one — either is fine, they are both throwaway. + + +-------------------------------------------------------------------------------- +8. THE TROUBLESHOOTING CLAIMS +-------------------------------------------------------------------------------- + +Assumes: postgres Example Mode, up. Each of these is an assertion in the +README's troubleshooting section; each should be falsifiable. + +[ ] "Sync config changes do nothing — the service reads them at boot." + Edit examples/postgres/powersync/sync-config.yaml, narrowing the stream: + + - SELECT * FROM lists + (delete the todos line) + + -> the client still shows todos. Then: + + docker compose restart powersync + + -> after a refresh, todos are gone from the client, lists remain. Restore + the line and restart again. + +[ ] "Schema or seed changes do nothing — init scripts only run on first start." + Add a row to examples/postgres/init-scripts/setup.sql, then: + + docker compose restart pg-db + + -> the new row is absent. Then: + + docker compose down -v && docker compose up -d + + -> now it is there. Revert the SQL change. + +[ ] "Port already in use." Start something on 6060 on the host, then + `docker compose up`. + + -> a bind error naming 6060. (`pnpm start` in backend/ is the realistic way + to cause this by accident.) + + +-------------------------------------------------------------------------------- +9. THE AUTOMATED SUITES +-------------------------------------------------------------------------------- + +Assumes: nothing running; neither suite needs Docker running containers, +though the root one shells out to `docker compose config`. + +[ ] Root suite — resolves every mode's topology without starting anything: + + pnpm install && pnpm test + + -> all green, in about a second. Nothing was pulled and nothing started; + confirm with `docker ps` before and after. + +[ ] Backend suite: + + cd backend && pnpm test + + -> all green. No Docker involved at all. + +[ ] Typecheck both: + + cd backend && pnpm check + +[ ] Mutation-check the root suite, so you know it is load-bearing. In + examples/postgres/compose.yaml, change the powersync config mount target: + + - ./examples/postgres/powersync:/config + -> + - ./examples/postgres/powersync:/config-typo + + -> `pnpm test` at the root goes red, and says which mode broke. Revert. + (Without this guard the stack would come up perfectly healthy pointing + at the wrong sync rules — hence the test.) + +[ ] Contract generation still works from the shared spec: + + cd backend && pnpm generate-types + cd ../frontend && pnpm generate + git status --short + + -> no diff, i.e. the committed generated types match the contract. + + +-------------------------------------------------------------------------------- +10. DELETING THE EXAMPLES +-------------------------------------------------------------------------------- + +Assumes: nothing running. The README invites an adopter to delete examples/ +in one go. This checks that the invitation is honest. Do this on a scratch +branch or with the diff thrown away afterwards. + +[ ] git switch -c scratch/delete-examples + +[ ] rm -rf examples/ frontend/ + +[ ] Set .env to Adopter Mode (COMPOSE_FILE=docker-compose.yaml) with the + adopter-db URI from section 6, and config/sync-config.yaml filled in. + +[ ] docker compose up --build + + -> it comes up. Nothing in the base compose file, the backend, or config/ + depended on the deleted directories. + +[ ] Read README.md as if for the first time. + + -> the "Your database needs preparing first" table is still there and still + tells you what your own database needs, with no surviving link you must + follow into the deleted examples/ to be able to proceed. Any instruction + that now dead-ends is a documentation bug. + +[ ] cd backend && pnpm test -> still green. +[ ] pnpm test (root) -> the topology suite will fail, since the modes + it resolves are gone. Note whether that is + acceptable or should be handled. + +[ ] git switch - && git branch -D scratch/delete-examples + + +-------------------------------------------------------------------------------- +TEARDOWN +-------------------------------------------------------------------------------- + +[ ] docker compose down -v --remove-orphans +[ ] docker ps -aq --filter "name=write-api-" | xargs -r docker rm -f +[ ] docker rm -f adopter-db +[ ] git status --short -> clean, apart from .env if you changed keys. + + +-------------------------------------------------------------------------------- +NOTES / FINDINGS +-------------------------------------------------------------------------------- + +Section What happened Verdict +------- ------------------------------------------------- ------- + + + + From 9baa3342c0c00b9ce5ba4866b3f49c8f42c8db96 Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Thu, 17 Sep 2026 14:35:30 +0200 Subject: [PATCH 18/18] Removed LLM tests. --- self-host-plan.md | 242 ------------------------------------------ tests/compose.test.ts | 194 --------------------------------- 2 files changed, 436 deletions(-) delete mode 100644 self-host-plan.md delete mode 100644 tests/compose.test.ts diff --git a/self-host-plan.md b/self-host-plan.md deleted file mode 100644 index 45c00b5..0000000 --- a/self-host-plan.md +++ /dev/null @@ -1,242 +0,0 @@ -# Self-hosting plan: hand `write-api` to an adopter - -## Goal - -`write-api` becomes a **starter template**. An adopter clones it, points it at their own -source database, runs it, and edits the backend code. The todo app and its seeded tables are -scaffolding — clearly fenced, clearly deletable. - -## Vocabulary - -Used consistently across the READMEs. Not written to `CONTEXT.md` — that stays as-is. - -- **Adopter** — the person we hand this to. Distinct from **Writer**, which already means an - authenticated end user in this codebase. -- **Example Source Database** — the bundled, seeded, throwaway database. -- **Example Mode** / **Adopter Mode** — the two run modes. -- **Source database** — whichever database is actually configured. Plays two roles at once: - PowerSync *replicates from* it, and the write API *persists to* it. -- **Bucket storage** — the PowerSync service's own internal store. Ours, not the adopter's. - -## The two modes - -**Example Mode** — the default, and the README's first command. Zero config, zero credentials. -`docker compose up` yields a working system with a seeded database and the todo frontend. -Framed explicitly as *verify the machinery, then replace it*, so nobody mistakes the todo app -for the product. - -**Adopter Mode** — the real path. No source database container, no frontend. `DATABASE_URI` -must be set; the backend fails loudly with a readable message when it isn't, rather than -defaulting to something surprising. - -## Fixed rule - -**Bucket storage never leaves the docker boundary.** Always MongoDB, always a container we own. -Even where PowerSync would allow sharing an adopter's Postgres 14+ instance (supported from PG14 -onward, blocked below it), we don't. We do not create schemas in someone else's database. - -## Layout - -``` -write-api/ -├── docker-compose.yaml # base: powersync, mongo, mongo-rs-init, backend -├── docker-compose.dev.yaml # overlay: bind-mount backend/, tsx watch -├── .env # COMPOSE_FILE + dev keypair + adopter's DATABASE_URI -├── .env.template -├── README.md # rewritten -├── config/ # ADOPTER MODE config — the adopter's from minute one -│ ├── service.yaml -│ └── sync-config.yaml -├── examples/ -│ ├── postgres/ -│ │ ├── compose.yaml -│ │ ├── README.md -│ │ ├── powersync/{service.yaml,sync-config.yaml} -│ │ └── init-scripts/setup.sql -│ ├── mongodb/ -│ │ ├── compose.yaml -│ │ ├── README.md -│ │ └── powersync/{service.yaml,sync-config.yaml} -│ ├── mysql/ -│ │ ├── compose.yaml -│ │ ├── README.md -│ │ ├── powersync/{service.yaml,sync-config.yaml} -│ │ └── init-scripts/{my.cnf,setup.sql} -│ └── mssql/ -│ ├── compose.yaml -│ ├── README.md -│ ├── powersync/{service.yaml,sync-config.yaml} -│ └── init-scripts/setup.sql -├── backend/ -│ ├── openapi.yaml # moved from repo root -│ └── … -└── frontend/ # test fixture; exists only inside Example Mode -``` - -### Why base + overlays - -The matrix is four flavours x two modes x (dev | built). Overlays compose **multiplicatively**: -five overlay files cover eight combinations, and adding a fifth flavour is one new file. - -Two alternatives were rejected: - -- **One compose file per example** (the `self-host-demo` pattern, using `include:` + `extends:`) - restates the mount and `depends_on` wiring four times, and `include:` cannot override an - included service's `depends_on` — a wall `self-host-demo` hit on its own MSSQL demo, forcing - a copy-paste of the backend service. We would hit it on the identical flavour. -- **One file with Compose profiles** breaks on the config mount: the `powersync` service needs a - different `/config` source per flavour, and a single service definition cannot vary its volumes - by profile. The workaround (templating the path through a variable) leaves two things that must - agree; out of step, it boots the wrong sync rules with no error. - -### Overlay conventions - -- Each example overlay carries a top-level `name:` (`write-api-postgres`, …) so switching - flavours never silently reuses the previous flavour's volumes. -- Flavour-specific values (`DATABASE_TYPE`, `DATABASE_URI`, `PS_DATA_SOURCE_URI`) are literals in - the overlay. They are facts about the flavour, not things a human edits. -- `.env` holds only what a human edits. -- **Relative paths inside an overlay resolve against the project directory** — the directory of - the *first* compose file, i.e. the repo root — not against the overlay's own location. - Verified. So `examples/postgres/compose.yaml` writes `./examples/postgres/powersync:/config`, - not `./powersync:/config`. This reads wrong at a glance and will trip up anyone adding a - flavour; call it out in a comment at the top of each overlay. - -## Switching modes - -One line in `.env`, alternatives sitting there commented out: - -```bash -COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml -# COMPOSE_FILE=docker-compose.yaml:examples/mongodb/compose.yaml -# COMPOSE_FILE=docker-compose.yaml:examples/mysql/compose.yaml -# COMPOSE_FILE=docker-compose.yaml:examples/mssql/compose.yaml -# COMPOSE_FILE=docker-compose.yaml # Adopter Mode -``` - -Append `:docker-compose.dev.yaml` for the watch loop. The command is always plain -`docker compose up`, so `down` / `logs` / `ps` behave exactly as anyone expects. The explicit -`-f` form is documented underneath for anyone who prefers it. - -## The four examples - -| Flavour | Source container | Bootstrap | Connector status | -|---|---|---|---| -| **Postgres** | `postgres:18`, `wal_level=logical` | `setup.sql` ending in `create publication powersync` | GA (11+) | -| **MongoDB** | *none* — reuses the base `mongo` replica set, second database | none; `post_images: auto_configure` | GA (6.0+) | -| **MySQL** | `mysql`, mounted `my.cnf` | `gtid_mode`, `enforce_gtid_consistency`, `binlog_format=ROW`, `binlog_row_image=FULL`, server-id, `REPLICATION SLAVE` user | Beta (5.7+) | -| **SQL Server** | `mssql` + one-shot setup container | `sp_cdc_enable_db`, CDC-enabled `_powersync_checkpoints`, per-table `sp_cdc_enable_table`, `cdc_reader` grant | Beta (2019+) | - -MongoDB is nearly free: the base already runs a Mongo replica set for bucket storage, so the -example points replication at a second database on the same instance. - -All four remain valid `DATABASE_TYPE` values in Adopter Mode regardless — this table is only -about which flavours ship a runnable database. - -## Sync rules: migrate to edition 3 - -Both formats work on `journeyapps/powersync-service:latest`, and `bucket_definitions:` is legacy -rather than deprecated — but whatever we ship is what an adopter copies when writing rules for -their own schema, and upstream docs now describe only `streams:`. - -Three changes that must land together: - -1. `config: {edition: 3}` + `streams:` replacing `bucket_definitions:` -2. `sync_config:` replacing the deprecated `sync_rules:` key in the service config - (specifying both is a hard error) -3. `auto_subscribe: true` on **every** stream - -File renames following upstream: `config/sync_rules.yaml` -> `config/sync-config.yaml` lands with -this migration. `config/powersync.yaml` -> `config/service.yaml` lands later, with the directory -restructure, since the mount path changes at the same time. - -MongoDB's rules differ by necessity, because `_id` is the primary key: - -```yaml -queries: - - select _id as id, * from lists - - select _id as id, * from todos -``` - -**Gotcha:** `auto_subscribe` defaults to `false`. Omit it and the client syncs nothing, with no -error. Client-side subscription is new in edition 3; the legacy format has no equivalent. - -No frontend code changes needed: `@powersync/web` is at `^1.38.7`, past the 1.32.0 threshold -where the Rust sync client became the default. - -## Auth and keys - -A fixed throwaway keypair committed in `.env`, commented as public and replaceable, plus a -`pnpm generate-keys` script wrapping the existing `src/utils/generate-key.ts`. - -Rationale: `src/api/auth.ts` generates a temporary keypair at boot when -`POWERSYNC_PRIVATE_KEY` is unset. Under `tsx watch` the backend restarts on every save, so every -save mints a new signing key and silently 401s every token already issued — failures that look -like a bug in the adopter's own change. - -`PS_JWKS_URL` becomes `http://backend:6060/api/auth/keys`. The backend is inside compose now, so -`host.docker.internal` goes away entirely. - -Frontend `VITE_*` build args stay on `localhost` ports — they resolve in the browser, not on the -compose network. - -## `openapi.yaml` moves into `backend/` - -The containerised backend currently **boots but serves nothing**: `backend/app.ts` reads -`path.join(__dirname, '..', 'openapi.yaml')`, but `backend/Dockerfile`'s build context is -`backend/`, so the spec is never copied in. The validator loads the spec lazily, so the container -starts cleanly and then answers every request with -`500 openapi.validator: spec could not be read at /openapi.yaml`. - -Four edits: - -- `backend/app.ts` — the spec path the validator loads, from `../openapi.yaml` to alongside `app.ts` -- `backend/package.json` — the type-generation script's input -- `frontend/package.json` — the type-generation script's input, now reaching across into `backend/` -- `README.md:41` — wording - -## Dev loop - -`docker-compose.dev.yaml` bind-mounts `backend/` and runs `tsx watch`. It touches the backend -only, because that is the code adopters change, and because an adopter in Adopter Mode has no -frontend service for a combined overlay to reference. - -It applies to **both** modes. Adopter Mode + dev is arguably the primary case: someone wiring the -write API into their own database is exactly who is editing `src/persistance/` and -`src/auth/verifier.ts`. - -The frontend's dev mode (`vite dev` against a bind-mount, with HMR and runtime `.env` reading -instead of baked-in build args) folds into the example overlays, where a frontend exists. - -A full `docker compose build` path remains as the deploy-shaped reference. - -## Deliverables - -1. Rewritten root `README.md` — Example Mode quickstart, then Adopter Mode setup, then the - code-change loop. -2. Four per-example READMEs covering each flavour's prerequisites and gotchas. - -Explicitly **not** in scope: `CONTEXT.md` updates and ADRs. The service image stays on `:latest`. - -The work is tracked as tickets under `.scratch/self-host/`, alongside the spec, per the repo's -local issue-tracker convention. That directory is gitignored, so this plan is the only part of the -design that lives in version control. - -## Risks - -1. ~~The config mount override is load-bearing.~~ **Verified** on Compose v2.38.2. An overlay - declaring `./examples/postgres/powersync:/config` *replaces* the base's `./config:/config` — - the resolved config has exactly one bind at `/config`, pointing at the example. Volumes merge - keyed by target path, as hoped. No fallback needed. -2. ~~`COMPOSE_FILE` read from `.env`.~~ **Verified.** With - `COMPOSE_FILE=docker-compose.yaml:examples/postgres/compose.yaml` in `.env`, a bare - `docker compose config` resolves the merged stack and picks up the overlay's `name:` - (`write-api-postgres`). Setting it to `docker-compose.yaml` alone yields Adopter Mode with the - `./config` mount and the default project name. No fallback needed. -4. **Four examples means four sets of sync rules** that must move together whenever the demo - schema changes. Ongoing drift we are signing up for. -5. **MySQL and SQL Server are Beta connectors.** On SQL Server, schema changes are *not* picked - up automatically — every table change needs a redeployed sync config. A real footgun for a - repo whose point is swapping in your own schema. Goes in `examples/mssql/README.md`. -6. **`:latest` drift.** A clone six months from now gets a service version we never tested - against. Accepted deliberately. diff --git a/tests/compose.test.ts b/tests/compose.test.ts deleted file mode 100644 index 587b833..0000000 --- a/tests/compose.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { execFile } from 'node:child_process'; -import { fileURLToPath } from 'node:url'; -import path from 'node:path'; -import { promisify } from 'node:util'; - -const run = promisify(execFile); -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); - -/** - * Seam: the resolved Compose topology. - * - * These guard two mechanisms that fail SILENTLY. If an example's config mount appended to the - * base's instead of replacing it, or a mode selection failed to resolve, the stack would come up - * perfectly healthy and point at the wrong sync rules — nothing crashes, nothing logs an error, - * and the only symptom is data that does not sync. - * - * Compose is asked to resolve each mode, not to run it: no containers start and no images are - * pulled, so this observes the resolved contract rather than the text of the files. - */ - -interface Mode { - name: string; - composeFile: string; - projectName: string; - services: string[]; - /** Repo-relative path expected to be mounted at /config. */ - configMount: string; - databaseType: string; - /** Substring the write API's and PowerSync's connection strings must both contain. */ - connectionHost: string; -} - -const ADOPTER_ENV = { - DATABASE_TYPE: 'postgres', - DATABASE_URI: 'postgres://someone:secret@their-own-host:5432/theirdb' -}; - -const MODES: Mode[] = [ - { - name: 'Adopter Mode', - composeFile: 'docker-compose.yaml', - projectName: 'write-api', - services: ['backend', 'mongo', 'mongo-rs-init', 'powersync'], - configMount: 'config', - databaseType: 'postgres', - connectionHost: 'their-own-host' - }, - { - name: 'Example Mode: MongoDB', - composeFile: 'docker-compose.yaml:examples/mongodb/compose.yaml', - projectName: 'write-api-mongodb', - // Deliberately no source database service: replication points at a second database on the - // replica set already running for bucket storage. - services: ['backend', 'frontend', 'mongo', 'mongo-rs-init', 'powersync'], - configMount: 'examples/mongodb/powersync', - databaseType: 'mongodb', - connectionHost: 'mongo:27017/powersync_demo_source' - }, - { - name: 'Example Mode: MySQL', - composeFile: 'docker-compose.yaml:examples/mysql/compose.yaml', - projectName: 'write-api-mysql', - services: ['backend', 'frontend', 'mongo', 'mongo-rs-init', 'mysql-db', 'powersync'], - configMount: 'examples/mysql/powersync', - databaseType: 'mysql', - connectionHost: 'mysql-db:3306' - }, - { - name: 'Example Mode: SQL Server', - composeFile: 'docker-compose.yaml:examples/mssql/compose.yaml', - projectName: 'write-api-mssql', - // mssql-setup is the one-shot CDC bootstrap; SQL Server has no entrypoint directory to drop - // SQL into, so it must exist and must complete before PowerSync starts. - services: ['backend', 'frontend', 'mongo', 'mongo-rs-init', 'mssql-db', 'mssql-setup', 'powersync'], - configMount: 'examples/mssql/powersync', - databaseType: 'mssql', - connectionHost: 'mssql-db:1433' - }, - { - name: 'Example Mode: Postgres', - composeFile: 'docker-compose.yaml:examples/postgres/compose.yaml', - projectName: 'write-api-postgres', - services: ['backend', 'frontend', 'mongo', 'mongo-rs-init', 'pg-db', 'powersync'], - configMount: 'examples/postgres/powersync', - databaseType: 'postgres', - connectionHost: 'pg-db:5432' - } -]; - -// Resolving is pure, and every assertion below needs the same handful of resolutions. Without -// this the suite shells out to Compose once per assertion. -const cache = new Map>(); - -const resolve = (composeFile: string) => { - const cached = cache.get(composeFile); - if (cached) return cached; - - const pending = run('docker', ['compose', 'config', '--format', 'json'], { - cwd: repoRoot, - env: { ...process.env, ...ADOPTER_ENV, COMPOSE_FILE: composeFile }, - maxBuffer: 10 * 1024 * 1024 - }).then(({ stdout }) => JSON.parse(stdout)); - - cache.set(composeFile, pending); - return pending; -}; - -const mountedAtConfig = (service: { volumes?: { target: string; source: string }[] }): string[] => - (service.volumes ?? []) - .filter((v) => v.target === '/config') - .map((v) => path.relative(repoRoot, v.source)); - -describe.each(MODES)('$name', (mode) => { - it('resolves to exactly the expected services', async () => { - const resolved = await resolve(mode.composeFile); - - expect(Object.keys(resolved.services).sort()).toEqual(mode.services); - }); - - it('runs under its own project name, so modes cannot share volumes', async () => { - const resolved = await resolve(mode.composeFile); - - expect(resolved.name).toBe(mode.projectName); - }); - - it('mounts exactly one config directory, and it is the right one', async () => { - const resolved = await resolve(mode.composeFile); - - // Exactly one: an overlay that appended rather than replaced would give two, and PowerSync - // would read whichever Docker happened to layer last. - expect(mountedAtConfig(resolved.services.powersync)).toEqual([mode.configMount]); - }); - - it('points the write API and replication at the same database', async () => { - const resolved = await resolve(mode.composeFile); - const backend = resolved.services.backend.environment; - const powersync = resolved.services.powersync.environment; - - expect(backend.DATABASE_TYPE).toBe(mode.databaseType); - expect(backend.DATABASE_URI).toContain(mode.connectionHost); - expect(powersync.PS_DATA_SOURCE_URI).toContain(mode.connectionHost); - }); - - it('keeps bucket storage in a container this project owns', async () => { - const resolved = await resolve(mode.composeFile); - - // The fixed rule: bucket storage never points at the adopter's database, whatever mode it is. - expect(resolved.services.powersync.environment.PS_MONGO_URI).toContain('mongodb://mongo:'); - expect(resolved.services.mongo).toBeDefined(); - }); - - it('reaches the backend over the compose network, not the host', async () => { - const resolved = await resolve(mode.composeFile); - - expect(resolved.services.powersync.environment.PS_JWKS_URL).toBe( - 'http://backend:6060/api/auth/keys' - ); - }); -}); - -/** - * The development overlay is a modifier rather than a mode: it may be appended to either mode and - * must change only how the backend runs. Adding or removing a service here would mean an adopter's - * dev loop differs from what they deploy. - */ -describe('development overlay', () => { - const withDev = (composeFile: string) => `${composeFile}:docker-compose.dev.yaml`; - - it.each(MODES)('adds no services and removes none from $name', async (mode) => { - const plain = await resolve(mode.composeFile); - const dev = await resolve(withDev(mode.composeFile)); - - expect(Object.keys(dev.services).sort()).toEqual(Object.keys(plain.services).sort()); - }); - - it.each(MODES)('mounts the working tree into the backend for $name', async (mode) => { - const dev = await resolve(withDev(mode.composeFile)); - const mounts = (dev.services.backend.volumes ?? []) as { target: string; source?: string }[]; - - const workingTree = mounts.find((v) => v.target === '/app'); - expect(workingTree?.source && path.relative(repoRoot, workingTree.source)).toBe('backend'); - - // An anonymous volume keeps the image's Linux node_modules. Without it the bind mount above - // shadows them with the host's, which on macOS are binaries the container cannot run. - expect(mounts.some((v) => v.target === '/app/node_modules')).toBe(true); - }); - - it.each(MODES)('leaves the config mount untouched for $name', async (mode) => { - const dev = await resolve(withDev(mode.composeFile)); - - expect(mountedAtConfig(dev.services.powersync)).toEqual([mode.configMount]); - }); -});