Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# PowerSync Self Hosted Example

## 2025-11-26

- Created the MSSQL Self host demo and configuration.

## 2025-11-25

### Postgres 18 Upgrade
Expand Down
24 changes: 24 additions & 0 deletions demos/nodejs-mssql/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# ==================== MSSQL credentials ================================
ROOT_PASSWORD=321strongROOTpassword!
HOSTNAME=mssql-selfhosted
DATABASE=powersync
DB_USER=powersync_user
DB_USER_PASSWORD=strongPOWERSYNCUSERpassword321!

PS_DATA_SOURCE_URI=mssql://${DB_USER}:${DB_USER_PASSWORD}@${HOSTNAME}:1433/${DATABASE}

# ==================== Demo config =========================================
DEMO_BACKEND_PORT=6060
DEMO_BACKEND_DATABASE_TYPE=mssql
DEMO_BACKEND_DATABASE_URI=${PS_DATA_SOURCE_URI}
# The front-end demo application is accessible at this port on the host machine
DEMO_CLIENT_PORT=3036
PS_JWKS_URL=http://demo-backend:${DEMO_BACKEND_PORT}/api/auth/keys

# These can be generated by following the instructions in the `key-generator` folder
# A temporary key will be used if these are not specified
DEMO_JWKS_PUBLIC_KEY=eyJrdHkiOiJSU0EiLCJuIjoiMlAwTWtUS1RpSmlEcEltZWl2akV6ODJTbERiRHFGblRmR1hnOXQzejZ2MUF0Y0x0X0l1T3VuaUhBQWtFbzU0Sndrc1o2bkR0RTdnbVlpTnd6Z3ROdnJaSjVhT1c1UUkxZkV4STkxc205clFoVkF4dENySlhxdVZMSnB3UmU4QkR1Yjd0QXNQZlpSc0NOYkZJQ1NVLUpoTkpwcGdGZFpUcWFBdVZsN2lRT3pBMHBGVVlONTF0Q2ItOGJUb2p6NFNtSEVRMmc2VjVsVjQwYlJ3aGcycmlpZ1JWWHI4eTdDdGhnYXRDU1p0YV80aGllT0ZUQkxPMUthZExjYzFzM0puVGxRMU5NRWE1T0hMdmFLYzAyVW83S2JKQWNOU3NQTzRidTdPTUVtMWdBeHhRWnVMZUU2OXB1anc2Z25QRXhqemwzRWpTTTlSQUJwSWpTNld4NFphRXZRIiwiZSI6IkFRQUIiLCJhbGciOiJSUzI1NiIsImtpZCI6InBvd2Vyc3luYy1kMjI0NmNiOTU4In0=
DEMO_JWKS_PRIVATE_KEY=eyJrdHkiOiJSU0EiLCJuIjoiMlAwTWtUS1RpSmlEcEltZWl2akV6ODJTbERiRHFGblRmR1hnOXQzejZ2MUF0Y0x0X0l1T3VuaUhBQWtFbzU0Sndrc1o2bkR0RTdnbVlpTnd6Z3ROdnJaSjVhT1c1UUkxZkV4STkxc205clFoVkF4dENySlhxdVZMSnB3UmU4QkR1Yjd0QXNQZlpSc0NOYkZJQ1NVLUpoTkpwcGdGZFpUcWFBdVZsN2lRT3pBMHBGVVlONTF0Q2ItOGJUb2p6NFNtSEVRMmc2VjVsVjQwYlJ3aGcycmlpZ1JWWHI4eTdDdGhnYXRDU1p0YV80aGllT0ZUQkxPMUthZExjYzFzM0puVGxRMU5NRWE1T0hMdmFLYzAyVW83S2JKQWNOU3NQTzRidTdPTUVtMWdBeHhRWnVMZUU2OXB1anc2Z25QRXhqemwzRWpTTTlSQUJwSWpTNld4NFphRXZRIiwiZSI6IkFRQUIiLCJkIjoiQkZhS1RJOG1ITnJaek5LbW82T0xjNVpCQ3dzLUgwMWRqVlJYc05yOGJlXzA1dmpob0hiNG1PWktBVW0zRzNLeHFKS2s0UGxodnpDRWhMcnJMVDN0U25tNDdTcUVUX0xZTjM4MHhmLWJRMFZfZTdmSDlXdDh2c0pvTFAtY05OU29QNUNfVjRaajRXQXBqa21HWXlNanhlRmczXzFYRUFwM1MtQ0lOazluSFMzYmkzZmtieHdET1VnRjI4MWhma0U3bzdfM3JabGJiZkhoY2FCMkgxY25CVTBqcld1ZFJUMDBKQ28walhJUnh2SGt5NldTdTZEWXVHNmh1UktYdWxoQlRDdGJINDd4cVJWQWIxcGRfWnVGSkc3dEtiU3pyT3o5TW1MLXBCTC05YmdVN1JtQzRCY0dHa1dXRlhDam9uOS1tTzJlN3JOenNHUjlWWkpST2RNUWN3IiwicCI6Il80amVZRU5WY2RIX1N4bmV3UGVmQm9oV19hTThObXNpYTRVWFF6ZTN4MThUMFY1Vl9LblRMWFp4ZHFQRi1OZnVPYkZMQjBHd1lXdlZrY1hHNWd0U3dtd090bkE5VTdwQzF6cTBfUW51THc5aGJPOXJneENGdm9pQm81dVZpNHJ0UmV1RnhITTA5czVBLXhuX241Sk9wby1yYWhIdUYyYWp4aFVlb1ZVYmpMcyIsInEiOiIyV0kxeFg2RUptOUdpR3hhMndUWnRMRFlzSGpEWUlvQW9iSFJUOHl3OWVhYzF1U0U1bXZnalZlRTZ4MWM5VUpBTl9vTHlrQktsa05oS095c0R2U3pZSi1JZlpYYzZqVE1VWWxRQ01vSV9ZQTRWMDJxM29XODNGTFNhOEk0V1RGbERINTlpUXRNNjlrQjgwaTA1Y25nYlMzMWdMUTFPdGg4OU14R0hGV0xHT2MiLCJkcCI6InljNGd1T3RVVm9CZTJzUENqS2pDV1ZsaFFnd2hLR1R4bVBKUnpjNzVfNlVSdEo2SXpfS2FpV1BwOWFVZld3ZkU1cUVpdk1kZThZRkUtRXUyYWNUMWhmX3FtcUFIZnRFeHFtSjl2dnlSczI2MUpWX3JpMldJQ2xJcDk3aU9vTmFGemx0VG1ETFgyRFpKVVVWV0FJSi1STUpmd0hRS2tVYUlfbzE5VkRJdmdMTSIsImRxIjoiYm9Wb0lVajVsekRzQTJCVHNSYi1PTWZRNDZnQ2JZcThWM2s0bWdIUDFyV3c5X0NuUVItSHcxVEQxMlhPWlVPUnN1UUdLb1lWWmVCTF9hcVdyLVBwYnk2dERteXJMTWc3T1JrLV83ajRhU3BQZXRPYUZCaWF0TW5IQWRKMUk2UGhaRURMUW1ua2FlU2pBVFh1QXdab2ZCbnB2ODNmWWxPLTlCY0hibEJ2cl9FIiwicWkiOiJEX2sxYVhmTVpscFpOc0E3ZUl3R1RHSlMyN0xwdnpJWjdFUWNEUnNqUnRjZWtPYmJueGNiM2U2WEpPV3ZLXy1IbF90VmxtLTdKZ21KWlpDSy1qVUxUYkVOUTAxMXdmZnBTRzZ5QmZuY0RXLV9ETlBWZ0E2N1g3azQzckNRTm9fck1VNERpczVwaWhoTHpoakIzUVlBbjAwek50MzFReWVXWmtNSm1mSF82YVkiLCJhbGciOiJSUzI1NiIsImtpZCI6InBvd2Vyc3luYy1kMjI0NmNiOTU4In0=
# ==================== PowerSync variables ====================
# The PowerSync API is accessible via this port
PS_PORT=8080
42 changes: 42 additions & 0 deletions demos/nodejs-mssql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# JavaScript PowerSync + MSSQL Self Hosted Demo

This demo contains a NodeJS + MSSQL backend and React frontend which are linked to a self hosted PowerSync instance.

Backend code can be found [here](https://github.com/powersync-ja/powersync-nodejs-backend-todolist-demo)

## Running

The `.env` file contains default configuration for the services. Reference this to connect to any services locally.

This demo can be started by running the following in this demo directory

```bash
docker compose up
```

or in the root directory run

```bash
docker compose -f demos/nodejs-mssql/docker-compose.yaml up
```

The frontend can be accessed at `http://localhost:3036` in a browser.

## Configuration

See [MSSQL Configuration](../../services/mssql/mssql.yaml) for the SQL server configuration
The SQL server is initialized with the [init](../../services/mssql/init.sql) script.

The initialization script (`init.sql`) performs the following setup steps:

1. **Database Creation**: Creates the application database
2. **CDC Setup**: Enables Change Data Capture at the database level
3. **User Creation**: Creates a SQL Server login and database user with appropriate permissions
4. **Create PowerSync Checkpoints table**: Creates the required `_powersync_checkpoints` table.
5. **Self Host Demo Tables**: Creates the demo tables (`lists` and `todos`)
6. **Enable Table CDC**: Enables CDC tracking on the demo tables
7. **Permissions**: Grants `db_datareader` and `cdc_reader` roles to the application user
8. **Sample Data**: Inserts initial test data into the `lists` table

All operations are idempotent, so they can safely be re-run without errors.

32 changes: 32 additions & 0 deletions demos/nodejs-mssql/config/powersync.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# yaml-language-server: $schema=https://unpkg.com/@powersync/service-schema@latest/json-schema/powersync-config.json
telemetry:
# Opt out of reporting anonymized usage metrics to PowerSync telemetry service
disable_telemetry_sharing: false

# Settings for source database replication
replication:
connections:
- type: mssql
uri: !env PS_DATA_SOURCE_URI
schema: dbo
additionalConfig:
trustServerCertificate: true

# Connection settings for sync bucket storage
storage:
type: mongodb
uri: !env PS_MONGO_URI

# The port which the PowerSync API server will listen on
port: !env PS_PORT

# Specify sync rules
sync_rules:
path: sync_rules.yaml

# Client (application end user) authentication settings
client_auth:
# JWKS URIs can be specified here
jwks_uri: !env PS_JWKS_URL

audience: ["powersync-dev", "powersync"]
13 changes: 13 additions & 0 deletions demos/nodejs-mssql/config/sync_rules.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# yaml-language-server: $schema=https://unpkg.com/@powersync/service-sync-rules@latest/schema/sync_rules.json
#
# See Documentation for more information:
# https://docs.powersync.com/usage/sync-rules
#
# Note that changes to this file are not watched.
# The service needs to be restarted for changes to take effect.

bucket_definitions:
global:
data:
- select * from lists
- select * from todos
65 changes: 65 additions & 0 deletions demos/nodejs-mssql/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Include syntax requires Docker compose > 2.20.3
# https://docs.docker.com/compose/release-notes/#2203
include:
# Creates a MongoDB replica set. This is used for internal and data storage
- path: ../../services/mongo.yaml

# MSSQL Data source configuration
- path: ../../services/mssql/mssql.yaml

services:
# Extend PowerSync with Mongo and MSSQL healthchecks
powersync:
extends:
file: ../../services/powersync.yaml
service: powersync
depends_on:
mssql-selfhosted:
condition: service_healthy
mssql-selfhosted-setup:
condition: service_completed_successfully
mongo-rs-init:
condition: service_completed_successfully
volumes:
- ./config:/config

# Demo NodeJS backend server and front-end web client copied from ps-nodejs-demo.yaml
# so that the demo backend depend_on could be overriden to wait for MSSQL to be ready
# An example demo app which is linked to the PowerSync instance above
demo-client:
build:
context: ../nodejs/demo-app
dockerfile: Dockerfile
args:
# This is from the perspective of the client running in a local machine's browser
VITE_POWERSYNC_URL: http://localhost:${PS_PORT}
# From the demo-backend defined below
VITE_BACKEND_URL: http://localhost:${DEMO_BACKEND_PORT}
VITE_CHECKPOINT_MODE: managed
ports:
- ${DEMO_CLIENT_PORT}:4173

# A backend which provides basic authentication and CRUD access to the Postgress DB from the client
demo-backend:
build:
context: https://github.com/powersync-ja/powersync-nodejs-backend-todolist-demo.git
depends_on:
mssql-selfhosted:
condition: service_healthy
mssql-selfhosted-setup:
condition: service_completed_successfully
environment:
DATABASE_TYPE: ${DEMO_BACKEND_DATABASE_TYPE}
DATABASE_URI: ${DEMO_BACKEND_DATABASE_URI}
# From the PowerSync service name
# This is just used to populate the JWT audience
POWERSYNC_URL: powersync-dev

# Keys here for demonstration
POWERSYNC_PUBLIC_KEY: ${DEMO_JWKS_PUBLIC_KEY}
POWERSYNC_PRIVATE_KEY: ${DEMO_JWKS_PRIVATE_KEY}
JWT_ISSUER: powersync-dev

PORT: ${DEMO_BACKEND_PORT}
ports:
- ${DEMO_BACKEND_PORT}:${DEMO_BACKEND_PORT}
4 changes: 4 additions & 0 deletions services/mssql/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ROOT_PASSWORD=321strongROOTpassword!
DATABASE=powersync
DB_USER=powersync_user
DB_USER_PASSWORD=strongPOWERSYNCUSERpassword321!
155 changes: 155 additions & 0 deletions services/mssql/init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
-- Create database (idempotent)
IF DB_ID('$(DATABASE)') IS NULL
BEGIN
CREATE DATABASE [$(DATABASE)];
END
GO

-- Enable CDC at the database level (idempotent)
USE [$(DATABASE)];
IF (SELECT is_cdc_enabled FROM sys.databases WHERE name = '$(DATABASE)') = 0
BEGIN
EXEC sys.sp_cdc_enable_db;
END
GO

-- Create a SQL login (server) if missing
USE [master];
IF NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = '$(DB_USER)')
BEGIN
CREATE LOGIN [$(DB_USER)] WITH PASSWORD = '$(DB_USER_PASSWORD)', CHECK_POLICY = ON;
END
GO

-- Create DB user for the app DB if missing
USE [$(DATABASE)];
IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = '$(DB_USER)')
BEGIN
CREATE USER [$(DB_USER)] FOR LOGIN [$(DB_USER)];
END
GO

-- Required for PowerSync to access the sys.dm_db_log_stats DMV
USE [master];
GRANT VIEW SERVER PERFORMANCE STATE TO [$(DB_USER)];
GO

-- Required for PowerSync to access the sys.dm_db_log_stats DMV and the sys.dm_db_partition_stats DMV
USE [$(DATABASE)];
GRANT VIEW DATABASE PERFORMANCE STATE TO [$(DB_USER)];
GO

-- Create PowerSync checkpoints table
-- Powersync requires this table to ensure regular checkpoints appear in CDC
IF OBJECT_ID('dbo._powersync_checkpoints', 'U') IS NULL
BEGIN
CREATE TABLE dbo._powersync_checkpoints (
id INT IDENTITY PRIMARY KEY,
last_updated DATETIME NOT NULL DEFAULT (GETDATE())
);
END

GRANT INSERT, UPDATE ON dbo._powersync_checkpoints TO [$(DB_USER)];
GO

-- Enable CDC for the powersync checkpoints table
IF NOT EXISTS (SELECT 1 FROM cdc.change_tables WHERE source_object_id = OBJECT_ID(N'dbo._powersync_checkpoints'))
BEGIN
EXEC sys.sp_cdc_enable_table
@source_schema = N'dbo',
@source_name = N'_powersync_checkpoints',
@role_name = N'cdc_reader',
@supports_net_changes = 0;
END
GO

-- Wait until capture job exists - usually takes a few seconds after enabling CDC on a table for the first time
DECLARE @tries int = 10;
WHILE @tries > 0 AND NOT EXISTS (SELECT 1 FROM msdb.dbo.cdc_jobs WHERE job_type = N'capture')
BEGIN
WAITFOR DELAY '00:00:01';
SET @tries -= 1;
END;

-- Set the CDC capture job polling interval to 1 second (default is 5 seconds)
EXEC sys.sp_cdc_change_job @job_type = N'capture', @pollinginterval = 1;
GO

/* -----------------------------------------------------------
Create demo lists and todos tables and enables CDC on them.
CDC must be enabled per table to actually capture changes.
------------------------------------------------------------*/
IF OBJECT_ID('dbo.lists', 'U') IS NULL
BEGIN
CREATE TABLE dbo.lists (
id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
created_at DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
name NVARCHAR(MAX) NOT NULL,
owner_id UNIQUEIDENTIFIER NOT NULL,
CONSTRAINT PK_lists PRIMARY KEY (id)
);
END

GRANT INSERT, UPDATE, DELETE ON dbo.lists TO [$(DB_USER)];
GO

IF OBJECT_ID('dbo.todos', 'U') IS NULL
BEGIN
CREATE TABLE dbo.todos (
id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
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,
CONSTRAINT PK_todos PRIMARY KEY (id),
CONSTRAINT FK_todos_lists FOREIGN KEY (list_id) REFERENCES dbo.lists(id) ON DELETE CASCADE
);
END

GRANT INSERT, UPDATE, DELETE ON dbo.todos TO [$(DB_USER)];
GO

-- Enable CDC for dbo.lists (idempotent guard)
IF NOT EXISTS (SELECT 1 FROM cdc.change_tables WHERE source_object_id = OBJECT_ID(N'dbo.lists'))
BEGIN
EXEC sys.sp_cdc_enable_table
@source_schema = N'dbo',
@source_name = N'lists',
@role_name = N'cdc_reader',
@supports_net_changes = 0;
END
GO

-- Enable CDC for dbo.todos (idempotent guard)
IF NOT EXISTS (SELECT 1 FROM cdc.change_tables WHERE source_object_id = OBJECT_ID(N'dbo.todos'))
BEGIN
EXEC sys.sp_cdc_enable_table
@source_schema = N'dbo',
@source_name = N'todos',
@role_name = N'cdc_reader',
@supports_net_changes = 0;
END
GO

-- Grant minimal rights to read CDC data
IF IS_ROLEMEMBER('db_datareader', '$(DB_USER)') = 0
BEGIN
ALTER ROLE db_datareader ADD MEMBER [$(DB_USER)];
END

IF IS_ROLEMEMBER('cdc_reader', '$(DB_USER)') = 0
BEGIN
ALTER ROLE cdc_reader ADD MEMBER [$(DB_USER)];
END
GO

-- Add demo data
IF NOT EXISTS (SELECT 1 FROM dbo.lists)
BEGIN
INSERT INTO dbo.lists (id, name, owner_id)
VALUES (NEWID(), 'Do a demo', NEWID());
END
GO
39 changes: 39 additions & 0 deletions services/mssql/mssql.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: mssql-selfhosted-db
services:
mssql-selfhosted:
platform: linux/amd64
image: mcr.microsoft.com/mssql/server:2022-latest # 2025 Can also be used, but not on Mac 26 Tahoe due to this issue: https://github.com/microsoft/mssql-docker/issues/942
container_name: mssql-selfhosted
ports:
- "1433:1433"
environment:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: "${ROOT_PASSWORD}"
MSSQL_PID: "Developer"
MSSQL_AGENT_ENABLED: "true" # required for CDC capture/cleanup jobs
volumes:
- data:/var/opt/mssql
healthcheck:
test: [ "CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P \"$${MSSQL_SA_PASSWORD}\" -Q \"SELECT 1;\" || exit 1" ]
interval: 5s
timeout: 3s
retries: 30

mssql-selfhosted-setup:
platform: linux/amd64
image: mcr.microsoft.com/mssql/server:2022-latest
container_name: mssql-selfhosted-setup
depends_on:
mssql-selfhosted:
condition: service_healthy
environment:
MSSQL_SA_PASSWORD: "${ROOT_PASSWORD}"
DATABASE: "${DATABASE}"
DB_USER: "${DB_USER}"
DB_USER_PASSWORD: "${DB_USER_PASSWORD}"
volumes:
- ./init.sql:/scripts/init.sql:ro
entrypoint: ["/bin/bash", "-lc", "/opt/mssql-tools18/bin/sqlcmd -C -S mssql-selfhosted,1433 -U sa -P \"$${MSSQL_SA_PASSWORD}\" -i /scripts/init.sql && echo '✅ MSSQL init done'"]

volumes:
data: