Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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.

31 changes: 31 additions & 0 deletions demos/nodejs-mssql/config/powersync.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 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
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!
154 changes: 154 additions & 0 deletions services/mssql/init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
-- 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
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: