How Thunderbolt's Offline-First Architecture Uses SQLite and PowerSync
Thunderbolt achieves offline-first functionality by maintaining SQLite as the local source of truth while using PowerSync as a bidirectional replication layer that synchronizes changes with a cloud PostgreSQL database.
Thunderbolt, the open-source email client from the Thunderbird team, implements a robust offline-first architecture that ensures users can access and modify their data without an internet connection. This article examines how the thunderbird/thunderbolt repository combines SQLite for local persistence with PowerSync for seamless synchronization across devices and platforms.
Core Architecture: SQLite as the Local Source of Truth
Thunderbolt stores all user data locally in an SQLite database that lives inside the Tauri desktop or web runtime. This SQLite instance serves as the single source of truth for the UI, queries, and any local-only features, ensuring the application functions completely offline.
The architecture abstracts SQLite access through Drizzle ORM, using wrapPowerSyncWithDrizzle to provide type-safe queries across the codebase.
Web vs Desktop SQLite Implementations
Thunderbolt supports multiple SQLite backends depending on the runtime environment:
WaSQLiteDatabase(src/db/wa-sqlite-database.ts): WebAssembly-based SQLite implementation used in web browsersBunSQLiteDatabase(src/db/bun-sqlite-database.ts): Native SQLite implementation for Bun runtime on desktop
Both implementations provide the same interface, allowing the rest of the application to remain platform-agnostic.
PowerSync Integration for Bidirectional Sync
When users enable synchronization, PowerSync bridges the local SQLite store and the cloud-side PostgreSQL database. PowerSync operates as a continuous replication layer that streams changes in both directions while maintaining ACID compliance at the local level.
Initializing the Sync Engine
The PowerSyncDatabaseImpl class in src/db/powersync/database.ts handles initialization and configuration:
import { PowerSyncDatabaseImpl } from '@/db/powersync/database'
// Path can be any writable location; "thunderbolt.db" works for both desktop & web
await new PowerSyncDatabaseImpl().initialize('thunderbolt.db')
The initialize() method creates the SQLite instance and wraps it with PowerSync, as implemented in lines 97-104 of src/db/powersync/database.ts.
Enabling and Disabling Sync
Sync functionality is toggled through user preferences using setSyncEnabled():
import { setSyncEnabled } from '@/db/powersync/database'
await setSyncEnabled(true) // connects and starts streaming
await setSyncEnabled(false) // disconnects cleanly
This function updates localStorage and calls connectToSync() or disconnectFromSync() (lines 90-115 of src/db/powersync/database.ts).
Custom Middleware: TransformableBucketStorage
Thunderbolt extends PowerSync's default behavior through a custom middleware layer called TransformableBucketStorage. This adapter intercepts PowerSync's writes and applies transformations before data persists to SQLite.
Encryption and Data Transformation
The ThunderboltPowerSyncDatabase class in src/db/powersync/ThunderboltPowerSyncDatabase.ts overrides generateBucketStorageAdapter() to inject the middleware:
import { ThunderboltPowerSyncDatabase } from '@/db/powersync/ThunderboltPowerSyncDatabase'
import { encryptionMiddleware } from '@/db/powersync/middleware/EncryptionMiddleware'
export const getPowerSyncOptions = (path: string) => ({
database: { dbFilename: path },
schema: AppSchema,
transformers: [encryptionMiddleware], // Applied before SQLite writes
sync: { /* shared-worker config … */ },
})
The TransformableBucketStorage (in src/db/powersync/TransformableBucketStorage.ts) runs registered transformers such as encryptionMiddleware to decrypt columns during sync, enabling end-to-end encryption without breaking offline functionality.
SharedWorker Implementation for Multi-Tab Support
For Chrome, Edge, and Firefox, Thunderbolt uses a custom SharedWorker to maintain a single sync connection across multiple tabs. The default PowerSync worker hard-codes SqliteBucketStorage, which would bypass the middleware layer.
Thunderbolt's ThunderboltSharedSyncImplementation.worker.ts preserves the middleware pipeline:
// Inside src/db/powersync/database.ts – default config
sync: {
worker: () =>
new SharedWorker(
new URL('./worker/ThunderboltSharedSyncImplementation.worker.ts', import.meta.url),
{ type: 'module', name: `shared-sync-${dbFilename}` },
),
},
This ensures that TransformableBucketStorage and encryption middleware remain active even when sync runs in a separate thread, as documented in docs/powersync-sync-middleware.md.
Handling Safari and Tauri with Visibility-Based Reconnect
Safari and Tauri environments lack SharedWorker support, requiring a fallback mechanism to maintain sync reliability. Thunderbolt implements a visibility-based reconnect strategy in src/db/powersync/database.ts.
The startVisibilityReconnect() method (lines 82-104) monitors the page visibility state:
// Automatically added by PowerSyncDatabaseImpl.startVisibilityReconnect()
document.addEventListener('visibilitychange', () => {
// Hidden > 15s → force reconnect
})
When the page remains hidden for more than 15 seconds, the system forces a disconnect and reconnect cycle. This prevents stale connections and ensures that users receive the latest updates when returning to the application, working around Safari's limitations while maintaining the offline-first integrity.
Summary
- SQLite serves as the single source of truth for all user data, stored locally via
WaSQLiteDatabase(web) orBunSQLiteDatabase(desktop) to enable complete offline functionality. - PowerSync provides bidirectional synchronization between the local SQLite store and cloud PostgreSQL, streaming changes continuously when connectivity is available.
- TransformableBucketStorage enables custom middleware such as
encryptionMiddlewareto transform data (e.g., decryption) before it persists to the local database. - Platform-specific optimizations include a custom
SharedWorkerfor multi-tab support in Chrome/Edge/Firefox and visibility-based reconnect logic for Safari and Tauri environments.
Frequently Asked Questions
How does Thunderbolt maintain offline-first functionality without an internet connection?
Thunderbolt stores all user data in a local SQLite database using either WaSQLiteDatabase for web environments or BunSQLiteDatabase for desktop. This SQLite instance acts as the single source of truth for the entire application, allowing users to read, write, and query data regardless of connectivity status. The UI interacts with this local database through Drizzle ORM, ensuring type-safe access to offline data.
What is the difference between WA-SQLite and Bun-SQLite in Thunderbolt's architecture?
WaSQLiteDatabase (implemented in src/db/wa-sqlite-database.ts) uses WebAssembly to run SQLite within browsers, providing a cross-platform solution for web deployments. BunSQLiteDatabase (in src/db/bun-sqlite-database.ts) leverages Bun's native SQLite bindings for desktop environments, offering better performance through direct system integration. Both implementations expose identical interfaces, allowing the sync layer to remain platform-agnostic while optimizing for each runtime's capabilities.
How does TransformableBucketStorage enable end-to-end encryption?
TransformableBucketStorage (defined in src/db/powersync/TransformableBucketStorage.ts) intercepts all data writes from PowerSync before they reach the SQLite database. When configured with transformers like encryptionMiddleware, it processes incoming sync data through the transformation pipeline—decrypting encrypted columns or performing other mutations—before persistence. This architecture ensures that sensitive data remains encrypted in transit and at rest in the cloud, while being decrypted locally for offline use, without breaking the sync protocol.
Why does Thunderbolt use a custom SharedWorker implementation?
The default PowerSync SharedWorker hard-codes SqliteBucketStorage, which would bypass Thunderbolt's TransformableBucketStorage middleware and disable encryption transformations. Thunderbolt's custom ThunderboltSharedSyncImplementation.worker.ts (configured in src/db/powersync/database.ts) preserves the middleware pipeline when running sync in a separate thread. This implementation enables efficient multi-tab synchronization in Chrome, Edge, and Firefox while maintaining end-to-end encryption and custom data transformations across all browser contexts.
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 →