Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
25 changes: 25 additions & 0 deletions demos/nodejs-mssql/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# ==================== MSSQL credentials ================================
# "sa" (sysadmin) is the root user for MSSQL required for initial database setup
SA_PASSWORD=321strongROOTpassword!
HOSTNAME=mssql-selfhosted
APP_DB=powersync
APP_LOGIN=powersync_user
APP_PASSWORD=strongPOWERSYNCUSERpassword321!

PS_DATA_SOURCE_URI=mssql://${APP_LOGIN}:${APP_PASSWORD}@${HOSTNAME}:1433/${APP_DB}

# ==================== 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=3035
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
28 changes: 28 additions & 0 deletions demos/nodejs-mssql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 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:3035` 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.
29 changes: 29 additions & 0 deletions demos/nodejs-mssql/config/powersync.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 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

# 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}
192 changes: 192 additions & 0 deletions services/mssql/init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
-- Create database (idempotent)
DECLARE @db sysname = '$(APP_DB)';
IF DB_ID(@db) IS NULL
BEGIN
DECLARE @sql nvarchar(max) = N'CREATE DATABASE [' + @db + N'];';
EXEC(@sql);
END
GO

-- Enable CDC at the database level (idempotent)
DECLARE @db sysname = '$(APP_DB)';
DECLARE @cmd nvarchar(max) = N'USE [' + @db + N'];
IF EXISTS (SELECT 1 FROM sys.databases WHERE name = ''' + @db + N''' AND is_cdc_enabled = 0)
EXEC sys.sp_cdc_enable_db;';
EXEC(@cmd);
GO


-- Create a SQL login (server) and user (db), then grant CDC read access
-- Note: 'cdc_reader' role is auto-created when CDC is enabled on the DB.
DECLARE @db sysname = '$(APP_DB)';
DECLARE @login sysname = '$(APP_LOGIN)';
DECLARE @password nvarchar(128) = '$(APP_PASSWORD)';
-- Create login if missing
IF NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = @login)
BEGIN
DECLARE @mklogin nvarchar(max) = N'CREATE LOGIN [' + @login + N'] WITH PASSWORD = ''' + @password + N''', CHECK_POLICY = ON;';
EXEC(@mklogin);
END;

-- Create user in DB if missing
DECLARE @mkuser nvarchar(max) = N'USE [' + @db + N'];
IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = ''' + @login + N''')
CREATE USER [' + @login + N'] FOR LOGIN [' + @login + N'];';
EXEC(@mkuser);
GO

USE [MASTER];
GRANT VIEW SERVER PERFORMANCE STATE TO [$(APP_LOGIN)];
GO

USE [$(APP_DB)];
GRANT VIEW DATABASE PERFORMANCE STATE TO [$(APP_LOGIN)];
GO

-- Create PowerSync checkpoints table
USE [$(APP_DB)];
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 [$(APP_LOGIN)];

-- Enable CDC for checkpoints table
DECLARE @enableCheckpointsTable nvarchar(max) =
'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 = 1;
END;';
EXEC(@enableCheckpointsTable);
GO

-- Wait until capture job exists
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)
-- Stops cdc job and restarts cdc job for new polling interval to take affect
-- Now it's safe
EXEC sys.sp_cdc_change_job @job_type = N'capture', @pollinginterval = 1;
GO

/* -----------------------------------------------------------
Create tables and enable CDC on them.
You must enable CDC per table to actually capture changes.
Example below creates the demo tables and enables CDC on it.
------------------------------------------------------------*/

DECLARE @db sysname = '$(APP_DB)';
EXEC(N'USE [' + @db + N'];
IF OBJECT_ID(''dbo.lists'', ''U'') IS NULL
BEGIN
CREATE TABLE dbo.lists (
id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(), -- GUID (36 characters),
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 [$(APP_LOGIN)];

EXEC(N'USE [' + @db + N'];
IF OBJECT_ID(''dbo.todos'', ''U'') IS NULL
BEGIN
CREATE TABLE dbo.todos (
id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(), -- GUID (36 characters)
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 [$(APP_LOGIN)];
GO

-- Enable CDC for dbo.lists (idempotent guard)
DECLARE @db sysname = '$(APP_DB)';
DECLARE @login sysname = '$(APP_LOGIN)';
DECLARE @enableListsTable nvarchar(max) = N'USE [' + @db + N'];
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 = 1;
END;';
EXEC(@enableListsTable);

-- Enable CDC for dbo.todos (idempotent guard)
DECLARE @enableTodosTable nvarchar(max) = N'USE [' + @db + N'];
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 = 1;
END;';
EXEC(@enableTodosTable);

-- Grant minimal rights to read CDC data:
-- 1) read access to base tables (db_datareader)
-- 2) membership in cdc_reader (allows selecting from CDC change tables & functions)
-- 3) the cdc_reader role is only available once CDC is enabled on the database and some tables have been enabled for CDC
DECLARE @grant nvarchar(max) = N'USE [' + @db + N'];
IF NOT EXISTS (SELECT 1 FROM sys.database_role_members rm
JOIN sys.database_principals r ON rm.role_principal_id = r.principal_id AND r.name = ''db_datareader''
JOIN sys.database_principals u ON rm.member_principal_id = u.principal_id AND u.name = ''' + @login + N''')
ALTER ROLE db_datareader ADD MEMBER [' + @login + N'];

IF NOT EXISTS (SELECT 1 FROM sys.database_role_members rm
JOIN sys.database_principals r ON rm.role_principal_id = r.principal_id AND r.name = ''cdc_reader''
JOIN sys.database_principals u ON rm.member_principal_id = u.principal_id AND u.name = ''' + @login + N''')
ALTER ROLE cdc_reader ADD MEMBER [' + @login + N'];';
EXEC(@grant);
GO

DECLARE @db sysname = '$(APP_DB)';
EXEC(N'USE [' + @db + N'];
BEGIN
INSERT INTO dbo.lists (id, name, owner_id)
VALUES (NEWID(), ''Do a demo'', NEWID());
END;
');
GO
Loading