# How Thunderbolt Implements a Custom SharedWorker for PowerSync Sync Pipeline

> Discover how Thunderbolt uses a custom SharedWorker with encryption middleware for secure, multi-tab data synchronization via PowerSync and SQLite.

- Repository: [Thunderbird/thunderbolt](https://github.com/thunderbird/thunderbolt)
- Tags: internals
- Published: 2026-04-19

---

**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`](https://github.com/thunderbird/thunderbolt/blob/main/src/db/powersync/worker/ThunderboltSharedSyncImplementation.ts).

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

```typescript
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`](https://github.com/thunderbird/thunderbolt/blob/main/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:

```typescript
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`](https://github.com/thunderbird/thunderbolt/blob/main/src/db/powersync/worker/ThunderboltSharedSyncImplementation.worker.ts). This minimal script creates a singleton instance of `ThunderboltSharedSyncImplementation` and handles incoming connections:

```typescript
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`](https://github.com/thunderbird/thunderbolt/blob/main/src/db/powersync/database.ts) (lines 123-138). The `worker` factory function returns a `SharedWorker` instance pointing to the custom implementation file:

```typescript
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`](https://github.com/thunderbird/thunderbolt/blob/main/database.ts) spawns the SharedWorker from [`ThunderboltSharedSyncImplementation.worker.ts`](https://github.com/thunderbird/thunderbolt/blob/main/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

```typescript
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`](https://github.com/thunderbird/thunderbolt/blob/main/ThunderboltSharedSyncImplementation.worker.ts).

### Adding Custom Transformers

```typescript
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

```typescript
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`](https://github.com/thunderbird/thunderbolt/blob/main/src/db/powersync/worker/ThunderboltSharedSyncImplementation.ts) | Core class extending `SharedSyncImplementation` to inject `TransformableBucketStorage` with encryption middleware. |
| [`src/db/powersync/worker/ThunderboltSharedSyncImplementation.worker.ts`](https://github.com/thunderbird/thunderbolt/blob/main/src/db/powersync/worker/ThunderboltSharedSyncImplementation.worker.ts) | SharedWorker entry point that instantiates the custom implementation and handles port connections. |
| [`src/db/powersync/TransformableBucketStorage.ts`](https://github.com/thunderbird/thunderbolt/blob/main/src/db/powersync/TransformableBucketStorage.ts) | Middleware-enabled storage layer that intercepts sync data before SQLite persistence. |
| [`src/db/powersync/database.ts`](https://github.com/thunderbird/thunderbolt/blob/main/src/db/powersync/database.ts) | Configures PowerSync options and wires the custom worker via the `worker` factory function. |
| [`src/db/powersync/middleware/EncryptionMiddleware.ts`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/ThunderboltSharedSyncImplementation.worker.ts)** creates a singleton instance shared across all browser tabs.
- Configuration in **[`database.ts`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/TransformableBucketStorage.ts)). This halts the sync pipeline for that batch, preventing corrupted or unencrypted data from reaching the local database and ensuring data integrity.