How Thunderbolt Implements a Custom SharedWorker for PowerSync Sync Pipeline

Thunderbolt implements a custom SharedWorker by extending PowerSync's SharedSyncImplementation to inject a TransformableBucketStorage that applies encryption middleware before data reaches the local SQLite database, enabling secure multi-tab synchronization.

The Thunderbolt project (Thunderbird's next-generation sync architecture) requires a custom SharedWorker implementation to handle PowerSync synchronization while applying end-to-end encryption and supporting multiple browser tabs. Unlike the standard PowerSync SharedWorker that hard-codes SqliteBucketStorage, Thunderbolt's custom implementation allows middleware transformations to run before data persistence.

Why Thunderbolt Needs a Custom SharedWorker Implementation

The upstream SharedSyncImplementation shipped by PowerSync instantiates SqliteBucketStorage directly, which bypasses any adapter-side middleware. Thunderbolt requires the ability to transform sync data—specifically applying end-to-end encryption—before it reaches the local database. A custom SharedWorker implementation provides the necessary hook to inject transformation logic while preserving PowerSync's multi-tab synchronization capabilities.

Core Components of the Custom SharedWorker

ThunderboltSharedSyncImplementation

The ThunderboltSharedSyncImplementation class extends SharedSyncImplementation and overrides the generateStreamingImplementation() method to inject custom storage. This class lives in src/db/powersync/worker/ThunderboltSharedSyncImplementation.ts.

The implementation creates a TransformableBucketStorage instance, registers the encryptionMiddleware, and passes this storage to WebStreamingSyncImplementation:

const storage = new TransformableBucketStorage(this.distributedDB!)
storage.addTransformer(encryptionMiddleware)
// ...
return new WebStreamingSyncImplementation(
  remote,
  this.powersync,
  this.options,
  storage
)

TransformableBucketStorage

TransformableBucketStorage extends SqliteBucketStorage to support a pipeline of DataTransformMiddleware transforms. Located in src/db/powersync/TransformableBucketStorage.ts, this class intercepts sync data before persistence.

The control() method overrides the parent implementation to intercept PROCESS_TEXT_LINE commands:

async control(command: string, data?: string | Uint8Array) {
  if (command === PROCESS_TEXT_LINE) {
    const line = data as string
    const json = JSON.parse(line)
    const bucket = json.data
    const transformed = await this.runTransformers(bucket)
    const newLine = JSON.stringify({ ...json, data: transformed })
    return super.control(command, newLine)
  }
  return super.control(command, data)
}

The runTransformers() method executes each registered middleware in sequence. If any transformer throws, the error is logged and re-thrown, halting the sync pipeline.

Worker Entry Point

The actual SharedWorker bootstrap lives in src/db/powersync/worker/ThunderboltSharedSyncImplementation.worker.ts. This minimal script creates a singleton instance of ThunderboltSharedSyncImplementation and handles incoming connections:

const sharedSyncImplementation = new ThunderboltSharedSyncImplementation()

self.onconnect = (e: MessageEvent) => {
  const port = e.ports[0]
  const client = new WorkerClient(port, sharedSyncImplementation)
}

Each browser tab connects to this SharedWorker, receiving a dedicated MessagePort while sharing the same sync implementation instance.

PowerSync Configuration

The custom worker is wired into PowerSync through getPowerSyncOptions() in src/db/powersync/database.ts (lines 123-138). The worker factory function returns a SharedWorker instance pointing to the custom implementation file:

worker: () => {
  return new SharedWorker(
    new URL(
      './worker/ThunderboltSharedSyncImplementation.worker.ts',
      import.meta.url
    ),
    { type: 'module' }
  )
}

This configuration ensures every PowerSyncDatabase instance uses the custom SharedWorker with transformation support.

Data Flow Through the Custom SharedWorker

  1. Tab Connection: When a browser tab initializes the PowerSync database, the worker factory in database.ts spawns the SharedWorker from ThunderboltSharedSyncImplementation.worker.ts. The tab connects via MessagePort.

  2. Sync Initialization: The ThunderboltSharedSyncImplementation creates a TransformableBucketStorage instance and registers the encryptionMiddleware. This storage is injected into WebStreamingSyncImplementation.

  3. Data Transformation: When sync data arrives from the PowerSync service, TransformableBucketStorage.control() intercepts the PROCESS_TEXT_LINE command. It parses the JSON payload, runs the encryption middleware against the bucket data, and forwards the transformed result to the parent SqliteBucketStorage.

  4. Persistence: The transformed data is written to the local SQLite database via the standard PowerSync storage mechanism.

  5. Multi-Tab Sync: Because the worker is shared, all tabs receive sync updates through the same instance, ensuring consistency while maintaining the transformation pipeline.

Code Examples

Initializing the Database with the Custom Worker

import { PowerSyncDatabaseImpl } from './src/db/powersync/database'

// Initialize the database; the custom SharedWorker is spawned automatically
const db = new PowerSyncDatabaseImpl()
await db.initialize('user-data/thunderbolt.db')

The initialize() method invokes getPowerSyncOptions(), which returns the worker factory configured to load ThunderboltSharedSyncImplementation.worker.ts.

Adding Custom Transformers

import { TransformableBucketStorage } from './src/db/powersync/TransformableBucketStorage'

// Define a logging middleware
const loggingTransformer = {
  async transform(batch) {
    console.log('Processing sync batch:', batch)
    return batch
  }
}

// Register additional transformers (encryptionMiddleware is already added by default)
storage.addTransformer(loggingTransformer)

Transformers execute sequentially in the order they are added, allowing you to chain multiple data transformations before persistence.

Accessing the SharedWorker from UI Tabs

import { getPowerSyncOptions } from './src/db/powersync/database'

const options = getPowerSyncOptions('my-database')
await options.sync?.worker?.()

This spawns the SharedWorker if not already running. Subsequent calls from other tabs connect to the existing instance, enabling shared sync state across browser tabs.

Key Files and Their Roles

File Role
src/db/powersync/worker/ThunderboltSharedSyncImplementation.ts Core class extending SharedSyncImplementation to inject TransformableBucketStorage with encryption middleware.
src/db/powersync/worker/ThunderboltSharedSyncImplementation.worker.ts SharedWorker entry point that instantiates the custom implementation and handles port connections.
src/db/powersync/TransformableBucketStorage.ts Middleware-enabled storage layer that intercepts sync data before SQLite persistence.
src/db/powersync/database.ts Configures PowerSync options and wires the custom worker via the worker factory function.
src/db/powersync/middleware/EncryptionMiddleware.ts The encryption transformer applied to all sync batches in the custom pipeline.

Summary

  • Thunderbolt implements a custom SharedWorker by extending SharedSyncImplementation to overcome the upstream limitation of hard-coded SqliteBucketStorage.
  • The TransformableBucketStorage class injects a middleware pipeline that applies end-to-end encryption before data reaches the local database.
  • The worker entry point in ThunderboltSharedSyncImplementation.worker.ts creates a singleton instance shared across all browser tabs.
  • Configuration in database.ts wires the custom worker into PowerSync via a worker factory function.
  • This architecture preserves multi-tab synchronization while ensuring all sync data passes through custom transformation logic.

Frequently Asked Questions

Why does Thunderbolt need a custom SharedWorker instead of using the standard PowerSync implementation?

The standard SharedSyncImplementation provided by PowerSync instantiates SqliteBucketStorage directly without middleware hooks. Thunderbolt requires end-to-end encryption to run before data persists locally, which necessitates a custom implementation that injects TransformableBucketStorage into the sync pipeline.

How does the encryption middleware integrate with the sync pipeline?

The ThunderboltSharedSyncImplementation creates a TransformableBucketStorage instance and registers the encryptionMiddleware via storage.addTransformer(). When sync data arrives, the TransformableBucketStorage.control() method intercepts the PROCESS_TEXT_LINE command, runs the middleware chain to encrypt the payload, and forwards the transformed data to the parent storage for persistence.

Can multiple transformers be chained in TransformableBucketStorage?

Yes, TransformableBucketStorage supports multiple transformers through the addTransformer() method. The runTransformers() method executes each registered transformer sequentially in the order they were added, allowing you to chain transformations such as logging, encryption, and data validation before the data reaches SQLite.

What happens if a transformer throws an error during sync?

If any transformer throws an error during the runTransformers() execution, the error is logged and re-thrown (lines 89-92 in TransformableBucketStorage.ts). This halts the sync pipeline for that batch, preventing corrupted or unencrypted data from reaching the local database and ensuring data integrity.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →