diff --git a/CHANGELOG.md b/CHANGELOG.md index bf9d2b9..cb35789 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/demos/nodejs-mssql/.env b/demos/nodejs-mssql/.env new file mode 100644 index 0000000..600e936 --- /dev/null +++ b/demos/nodejs-mssql/.env @@ -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 diff --git a/demos/nodejs-mssql/README.md b/demos/nodejs-mssql/README.md new file mode 100644 index 0000000..d492510 --- /dev/null +++ b/demos/nodejs-mssql/README.md @@ -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. + diff --git a/demos/nodejs-mssql/config/powersync.yaml b/demos/nodejs-mssql/config/powersync.yaml new file mode 100644 index 0000000..a9bb994 --- /dev/null +++ b/demos/nodejs-mssql/config/powersync.yaml @@ -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"] diff --git a/demos/nodejs-mssql/config/sync_rules.yaml b/demos/nodejs-mssql/config/sync_rules.yaml new file mode 100644 index 0000000..14178e7 --- /dev/null +++ b/demos/nodejs-mssql/config/sync_rules.yaml @@ -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 diff --git a/demos/nodejs-mssql/docker-compose.yaml b/demos/nodejs-mssql/docker-compose.yaml new file mode 100644 index 0000000..e6b5547 --- /dev/null +++ b/demos/nodejs-mssql/docker-compose.yaml @@ -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} diff --git a/services/mssql/.env.template b/services/mssql/.env.template new file mode 100644 index 0000000..2e20e87 --- /dev/null +++ b/services/mssql/.env.template @@ -0,0 +1,4 @@ +ROOT_PASSWORD=321strongROOTpassword! +DATABASE=powersync +DB_USER=powersync_user +DB_USER_PASSWORD=strongPOWERSYNCUSERpassword321! \ No newline at end of file diff --git a/services/mssql/init.sql b/services/mssql/init.sql new file mode 100644 index 0000000..2bb0c63 --- /dev/null +++ b/services/mssql/init.sql @@ -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 \ No newline at end of file diff --git a/services/mssql/mssql.yaml b/services/mssql/mssql.yaml new file mode 100644 index 0000000..31e8b72 --- /dev/null +++ b/services/mssql/mssql.yaml @@ -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: