Skip to content

Commit 8101eeb

Browse files
committed
Encryption example
1 parent 3e96870 commit 8101eeb

File tree

12 files changed

+124
-57
lines changed

12 files changed

+124
-57
lines changed

demos/example-node/.env

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
BACKEND=http://localhost:6060
22
SYNC_SERVICE=http://localhost:8080
33
POWERSYNC_TOKEN=
4-
POWERSYNC_DEBUG=1
4+
POWERSYNC_DEBUG=1
5+
ENCRYPTION_KEY=

demos/example-node/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ Results from the query are printed every time it changes. Try:
1515
1. Updating a row in the backend database and see changes reflected in the running client.
1616
2. Enter `add('my list')` and see the new list show up in the backend database.
1717

18+
## Encryption
19+
20+
This demo can use encrypted databases with the `better-sqlite3-multiple-ciphers` package.
21+
To test encryption, set the `ENCRYPTION_KEY` in `.env` to a non-empty value.
22+
23+
## References
24+
1825
For more details, see the documentation for [the PowerSync node package](https://docs.powersync.com/client-sdk-references/node) and check other examples:
1926

2027
- [example-electron-node](../example-electron-node/): An Electron example that runs PowerSync in the main process using the Node.js SDK.

demos/example-node/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111
},
1212
"dependencies": {
1313
"@powersync/node": "workspace:*",
14+
"better-sqlite3": "^12.2.0",
15+
"better-sqlite3-multiple-ciphers": "^12.2.0",
1416
"dotenv": "^16.4.7",
1517
"undici": "^7.11.0"
1618
},
1719
"devDependencies": {
20+
"@types/better-sqlite3": "^7.6.13",
1821
"ts-node": "^10.9.2",
1922
"typescript": "^5.8.2"
2023
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// This worker uses bindings to sqlite3 multiple ciphers instead of the original better-sqlite3 worker.
2+
//
3+
// It is used in main.ts only when an encryption key is set.
4+
import Database from 'better-sqlite3-multiple-ciphers';
5+
6+
import { startPowerSyncWorker } from '@powersync/node/worker.js';
7+
8+
async function resolveBetterSqlite3() {
9+
return Database;
10+
}
11+
12+
startPowerSyncWorker({ loadBetterSqlite3: resolveBetterSqlite3 });

demos/example-node/src/main.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { once } from 'node:events';
22
import repl_factory from 'node:repl';
3+
import { Worker } from 'node:worker_threads';
34

45
import {
56
createBaseLogger,
@@ -11,11 +12,14 @@ import {
1112
import { exit } from 'node:process';
1213
import { AppSchema, DemoConnector } from './powersync.js';
1314
import { enableUncidiDiagnostics } from './UndiciDiagnostics.js';
15+
import { WorkerOpener } from 'node_modules/@powersync/node/src/db/options.js';
16+
import { LockContext } from 'node_modules/@powersync/node/dist/bundle.cjs';
1417

1518
const main = async () => {
1619
const baseLogger = createBaseLogger();
1720
const logger = createLogger('PowerSyncDemo');
1821
const debug = process.env.POWERSYNC_DEBUG == '1';
22+
const encryptionKey = process.env.ENCRYPTION_KEY ?? '';
1923
baseLogger.useDefaults({ defaultLevel: debug ? logger.TRACE : logger.WARN });
2024

2125
// Enable detailed request/response logging for debugging purposes.
@@ -30,10 +34,27 @@ const main = async () => {
3034
return;
3135
}
3236

37+
let customWorker: WorkerOpener | undefined;
38+
if (encryptionKey.length) {
39+
customWorker = (_, options) => {
40+
return new Worker(new URL('./encryption.worker.js', import.meta.url), options);
41+
};
42+
}
43+
3344
const db = new PowerSyncDatabase({
3445
schema: AppSchema,
3546
database: {
36-
dbFilename: 'test.db'
47+
dbFilename: 'test.db',
48+
openWorker: customWorker,
49+
initializeConnection: async (db) => {
50+
if (encryptionKey.length) {
51+
const escapedKey = encryptionKey.replace("'", "''");
52+
await db.execute(`pragma key = '${escapedKey}'`);
53+
}
54+
55+
// Make sure the database is readable, this fails early if the key is wrong.
56+
await db.execute('pragma user_version');
57+
}
3758
},
3859
logger
3960
});

packages/node/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ The `@powersync/node` package is currently in a Beta release.
2020
## Install Package
2121

2222
```bash
23-
npm install @powersync/node
23+
npm install @powersync/node better-sqlite3
2424
```
2525

2626
Both `@powersync/node` and the `better-sqlite3` packages have install scripts that need to run to compile

packages/node/src/db/BetterSqliteWorker.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,7 @@ class BlockingAsyncDatabase implements AsyncDatabase {
6565
export async function openDatabase(worker: PowerSyncWorkerOptions, options: AsyncDatabaseOpenOptions) {
6666
const BetterSQLite3Database = await worker.loadBetterSqlite3();
6767
const baseDB = new BetterSQLite3Database(options.path);
68-
baseDB.pragma('journal_mode = WAL');
6968
baseDB.loadExtension(worker.extensionPath(), 'sqlite3_powersync_init');
70-
if (!options.isWriter) {
71-
baseDB.pragma('query_only = true');
72-
}
7369

7470
const asyncDb = new BlockingAsyncDatabase(baseDB);
7571
return asyncDb;

packages/node/src/db/NodeSqliteWorker.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,7 @@ export async function openDatabase(worker: PowerSyncWorkerOptions, options: Asyn
5757
const { DatabaseSync } = await dynamicImport('node:sqlite');
5858

5959
const baseDB = new DatabaseSync(options.path, { allowExtension: true });
60-
baseDB.exec('pragma journal_mode = WAL');
6160
baseDB.loadExtension(worker.extensionPath());
62-
if (!options.isWriter) {
63-
baseDB.exec('pragma query_only = true');
64-
}
6561

6662
return new BlockingNodeDatabase(baseDB, options.isWriter);
6763
}

packages/node/src/db/WorkerConnectionPool.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,17 @@ export class WorkerConnectionPool extends BaseObserver<DBAdapterListener> implem
131131
await database.execute("SELECT powersync_update_hooks('install');", []);
132132
}
133133

134-
return new RemoteConnection(worker, comlink, database);
134+
const connection = new RemoteConnection(worker, comlink, database);
135+
if (this.options.initializeConnection) {
136+
await this.options.initializeConnection(connection, isWriter);
137+
}
138+
139+
await connection.execute('pragma journal_mode = WAL');
140+
if (!isWriter) {
141+
await connection.execute('pragma query_only = true');
142+
}
143+
144+
return connection;
135145
};
136146

137147
// Open the writer first to avoid multiple threads enabling WAL concurrently (causing "database is locked" errors).

packages/node/src/db/options.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type Worker } from 'node:worker_threads';
2-
import { SQLOpenOptions } from '@powersync/common';
2+
import { LockContext, SQLOpenOptions } from '@powersync/common';
33

44
export type WorkerOpener = (...args: ConstructorParameters<typeof Worker>) => InstanceType<typeof Worker>;
55

@@ -41,4 +41,11 @@ export interface NodeSQLOpenOptions extends SQLOpenOptions {
4141
* @returns the resolved worker.
4242
*/
4343
openWorker?: WorkerOpener;
44+
45+
/**
46+
* Initializes a created database connection.
47+
*
48+
* This can be used to e.g. set encryption keys, if an encrypted database should be used.
49+
*/
50+
initializeConnection?: (db: LockContext, isWriter: boolean) => Promise<void>;
4451
}

0 commit comments

Comments
 (0)