PowerSync Sync Middleware in Thunderbolt: Architecture and Extension Guide
PowerSync sync middleware is the transformation layer in Thunderbolt that converts raw database changes into domain-specific actions, implemented via the SyncTransformer interface in src/sync/ThunderboltSyncImplementation.ts and extensible by registering custom transformer classes.
The Thunderbolt email client uses PowerSync to maintain real-time synchronization between local SQLite databases and cloud backends. The PowerSync sync middleware acts as the critical bridge between PowerSync's low-level change stream and Thunderbolt's domain-specific data models, handling everything from encryption to business logic enforcement.
What Is PowerSync Sync Middleware?
The PowerSync sync middleware sits between PowerSync's change stream and the application's domain models. It is implemented as a set of TypeScript classes that conform to PowerSync's SyncImplementation interface.
The middleware handles three primary responsibilities:
- Transforming raw PowerSync changes – Converting generic
INSERT,UPDATE, andDELETEevents into higher-level domain actions that Thunderbolt can process. - Applying business-logic rules – Enforcing soft-delete semantics, filtering transient fields, or merging server-generated timestamps before data reaches the UI.
- Coordinating with the encrypted store – When end-to-end encryption is enabled, the middleware decrypts incoming rows and encrypts outgoing ones before they hit the local SQLite database.
The core implementation lives in src/sync/ThunderboltSyncImplementation.ts, which composes several reusable transformers located in src/sync/middleware/.
How to Extend PowerSync Sync Middleware
Extending the middleware involves implementing the SyncTransformer interface and registering your transformer in the middleware chain.
Create a Custom Transformer
A transformer is a class that implements the SyncTransformer interface with two optional methods:
transformIncoming(change: SyncChange)– Processes changes from server to client.transformOutgoing(change: SyncChange)– Processes changes from client to server before upload.
Both methods receive a SyncChange object containing the operation type (INSERT, UPDATE, or DELETE), table name, and row data.
Register the Transformer
Add your transformer instance to the middleware array when constructing the ThunderboltSyncImplementation. The order matters: transformers are applied sequentially, so place dependencies earlier in the array.
Update the Sync Rule Configuration
If your transformer introduces logic for a new table or column, edit backend/config.yaml under backend/config so PowerSync knows which tables the middleware should handle.
Practical Example: Adding a lastEditedBy Field
Suppose you want every row in the notes table to automatically receive the current user’s ID on every update.
First, create the transformer:
// src/sync/middleware/LastEditedByTransformer.ts
import type { SyncTransformer, SyncChange } from '@powersync/web'
export class LastEditedByTransformer implements SyncTransformer {
constructor(private readonly getCurrentUserId: () => string) {}
// Runs for incoming changes (server → client)
async transformIncoming(change: SyncChange) {
// No modification needed for incoming data
return change
}
// Runs for outgoing changes (client → server)
async transformOutgoing(change: SyncChange) {
if (change.type === 'update' && change.table === 'notes') {
const userId = this.getCurrentUserId()
// Inject the field before the row is sent to the server
change.row.lastEditedBy = userId
}
return change
}
}
Then register it in the sync implementation:
// src/sync/ThunderboltSyncImplementation.ts
import { ThunderboltSyncImplementation } from '@powersync/web'
import { LastEditedByTransformer } from './middleware/LastEditedByTransformer'
// Function that returns the signed‑in user’s ID
function getCurrentUserId() { /* … */ }
export const sync = new ThunderboltSyncImplementation({
// Existing transformers …
middleware: [
// …other transformers,
new LastEditedByTransformer(getCurrentUserId),
],
})
With this transformer registered, every UPDATE operation on the notes table automatically includes the lastEditedBy column, enabling server-side audit trails without modifying business logic elsewhere.
Key Files and Architecture
Understanding the file structure helps navigate the middleware codebase:
src/sync/ThunderboltSyncImplementation.ts– The main sync client that wires PowerSync together with the middleware chain. This file constructs theSyncImplementationand manages the lifecycle of transformers.src/sync/middleware/– Directory containing built-in transformers such as soft-delete handlers and encryption wrappers. This is where you place custom transformer files.docs/powersync-sync-middleware.md– Architectural documentation covering design goals, extension points, and best practices for the middleware layer.backend/config.yaml– PowerSync cloud dashboard configuration defining which tables are synced and their transformation rules. Update this when adding new synced tables.
Summary
- PowerSync sync middleware in Thunderbolt transforms raw database changes into domain-specific actions between PowerSync and the local SQLite store.
- The middleware lives in
src/sync/ThunderboltSyncImplementation.tsand operates through theSyncTransformerinterface. - To extend functionality, implement
SyncTransformerwithtransformIncomingandtransformOutgoingmethods, then register instances in themiddlewarearray. - Transformer order matters; data processes sequentially from first to last in the array.
- Update
backend/config.yamlwhen introducing new tables or sync rules.
Frequently Asked Questions
What is the PowerSync sync middleware responsible for?
The PowerSync sync middleware handles three core tasks: converting raw INSERT, UPDATE, and DELETE events from PowerSync into domain-specific actions, applying business logic like soft-delete semantics or field filtering, and coordinating end-to-end encryption by decrypting incoming rows and encrypting outgoing ones before they reach the local SQLite database.
How do I add custom transformation logic to the sync pipeline?
Create a TypeScript class that implements the SyncTransformer interface with transformIncoming for server-to-client changes and transformOutgoing for client-to-server changes. Instantiate your class and add it to the middleware array in src/sync/ThunderboltSyncImplementation.ts. The transformer operates on SyncChange objects containing the operation type, table name, and row data.
Does the order of transformers in the middleware array matter?
Yes, the order is significant because transformers are applied sequentially. Data flows through the array from first to last, with each transformer receiving the output of the previous one. Place foundational transformers like decryption or validation early in the chain, and application-specific logic like audit fields or business rules later.
Where is the PowerSync sync middleware configured in Thunderbolt?
The middleware implementation is constructed in src/sync/ThunderboltSyncImplementation.ts, where the middleware array is defined. Sync rules that determine which tables are tracked are configured in backend/config.yaml under the backend/config directory. Architectural documentation resides in docs/powersync-sync-middleware.md.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →