This plugin is inspired by capacitor-blob-writer's innovative approach to bypassing Capacitor's bridge limitations.
Capacitor's standard native-to-JavaScript bridge serializes all data, causing:
- Performance degradation with large datasets (8MB file: 10.6s vs 0.4s)
- Memory exhaustion with binary data
- Inefficient serialization (base64 encoding overhead)
Instead of using Capacitor's bridge, both plugins use:
- Local HTTP server running on native side
- Direct fetch() calls from JavaScript
- Efficient binary handling without serialization
┌─────────────────────────────────────┐
│ High-Level API │
│ ┌──────────────────────────────┐ │
│ │ NativeSQL.connect() │ │
│ │ NativeSQL.disconnect() │ │
│ │ NativeSQL.getConnection() │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘
Purpose: Simple connection management
Files:
┌─────────────────────────────────────┐
│ SQLConnection Class │
│ ┌──────────────────────────────┐ │
│ │ execute() │ │
│ │ query() │ │
│ │ run() │ │
│ │ executeBatch() │ │
│ │ transaction() │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘
Purpose: Database operations with HTTP protocol
Key Features:
- Converts operations to HTTP requests
- Handles binary data serialization (base64)
- Manages transactions
- Batch operation support
Files:
JavaScript Native
│ │
│ POST /execute │
├──────────────────────────>│
│ { │
│ statement: "...", │
│ params: [...] │
│ } │
│ │
│ Response │
│<──────────────────────────┤
│ { │
│ rows: [...], │
│ rowsAffected: N │
│ } │
│ │
Endpoints:
POST /execute- Single statementPOST /batch- Multiple statementsPOST /transaction/begin- Start transactionPOST /transaction/commit- CommitPOST /transaction/rollback- Rollback
Authentication:
- Bearer token (generated per connection)
- Database name in
X-Databaseheader
Data Format:
- JSON for structured data
- Base64 for binary data (Uint8Array)
┌─────────────────────────────────────┐
│ CapgoCapacitorNativeSqlPlugin │
│ (Capacitor Plugin Interface) │
└─────────────────────────────────────┘
│
┌─────────────────────────────────────┐
│ SQLHTTPServer │
│ - Listens on localhost:PORT │
│ - Routes requests │
│ - Authenticates via token │
└─────────────────────────────────────┘
│
┌─────────────────────────────────────┐
│ SQLDatabase │
│ - Wraps SQLite3 │
│ - Executes queries │
│ - Manages transactions │
└─────────────────────────────────────┘
Files:
- ios/Sources/.../CapgoCapacitorNativeSqlPlugin.swift
- ios/Sources/.../SQLHTTPServer.swift
- ios/Sources/.../SQLDatabase.swift
Note: Production use requires integrating GCDWebServer or similar
┌─────────────────────────────────────┐
│ CapgoCapacitorNativeSqlPlugin │
│ (Capacitor Plugin Interface) │
└─────────────────────────────────────┘
│
┌─────────────────────────────────────┐
│ SQLHTTPServer (NanoHTTPD) │
│ - Listens on localhost:PORT │
│ - Routes requests │
│ - Authenticates via token │
└─────────────────────────────────────┘
│
┌─────────────────────────────────────┐
│ SQLDatabase │
│ - Wraps Android SQLite │
│ - Executes queries │
│ - Manages transactions │
└─────────────────────────────────────┘
Files:
- android/src/.../CapgoCapacitorNativeSqlPlugin.java
- android/src/.../SQLHTTPServer.java
- android/src/.../SQLDatabase.java
Dependencies:
- NanoHTTPD (lightweight HTTP server)
- Gson (JSON parsing)
┌─────────────────────────────────────┐
│ CapgoCapacitorFastSqlWeb │
│ - @sqlite.org/sqlite-wasm │
│ - OPFS for persistence │
│ - Compatible API │
└─────────────────────────────────────┘
Files:
Note: Web implementation uses standard approach (no HTTP server)
// 1. JavaScript API
const db = await NativeSQL.connect({ database: 'test' });
// 2. SQLConnection sends HTTP request
const result = await db.execute(
'SELECT * FROM users WHERE age > ?',
[18]
);
// 3. HTTP request
POST http://localhost:9123/execute
Authorization: Bearer abc123...
X-Database: test
{
"statement": "SELECT * FROM users WHERE age > ?",
"params": [18]
}
// 4. Native layer processes
SQLHTTPServer receives request
→ Validates auth token
→ Gets database "test"
→ SQLDatabase.execute()
→ Prepares statement
→ Binds parameters
→ Executes query
→ Collects rows
→ Returns results
// 5. Response
{
"rows": [
{ "id": 1, "name": "Alice", "age": 25 },
{ "id": 2, "name": "Bob", "age": 30 }
],
"rowsAffected": 0
}
// 6. JavaScript receives data
console.log(result.rows); // [{ id: 1, ... }, { id: 2, ... }]// JavaScript
const imageData = new Uint8Array([0xFF, 0xD8, ...]);
await db.run('INSERT INTO images (data) VALUES (?)', [imageData]);
// Serialization in SQLConnection
{
"statement": "INSERT INTO images (data) VALUES (?)",
"params": [
{
"_type": "binary",
"_data": "/9j/4AAQSkZJ..." // base64
}
]
}
// Native deserialization
Detects { _type: "binary", _data: "..." }
→ Decodes base64
→ Binds as BLOB to SQLite| Operation | Standard Bridge | HTTP Protocol | Improvement |
|---|---|---|---|
| Small query (<1KB) | ~50ms | ~50ms | 1x |
| Medium query (10KB) | ~200ms | ~80ms | 2.5x |
| Large query (100KB) | ~2000ms | ~150ms | 13x |
| Binary data (1MB) | ~5000ms | ~200ms | 25x |
Ideal for:
- Local sync systems (CRDTs, OT, etc.)
- Large dataset operations
- Binary data storage (images, documents)
- IndexedDB replacement
- High-frequency database operations
Not ideal for:
- Simple key-value storage (use @capgo/capacitor-data-storage-sqlite)
- Remote-only data (use HTTP APIs)
- Occasional small queries (standard bridge is fine)
- Server binds only to
127.0.0.1(localhost) - No external network access
- No remote connections possible
- Unique token per database connection
- Token generated using secure random
- Token required for all requests
- Token transmitted via Authorization header
- Databases stored in app's private directory
- No access from other apps
- Optional encryption (iOS/Android)
- Standard SQLite file permissions
class SyncEngine {
async applyRemoteChanges(changes: Change[]) {
await db.transaction(async (tx) => {
for (const change of changes) {
// Apply change
await tx.run(change.sql, change.params);
// Update sync metadata
await tx.run(
'UPDATE sync_meta SET last_sync = ?',
[change.timestamp]
);
}
});
}
}// Old: IndexedDB
const db = await idb.open('mydb');
await db.put('store', { id: 1, data: '...' });
// New: Native SQL
const db = await NativeSQL.connect({ database: 'mydb' });
await db.run(
'INSERT OR REPLACE INTO store (id, data) VALUES (?, ?)',
[1, '...']
);class OfflineQueue {
async enqueue(operation: Operation) {
await db.run(
'INSERT INTO queue (data, status) VALUES (?, ?)',
[JSON.stringify(operation), 'pending']
);
}
async processQueue() {
const pending = await db.query(
'SELECT * FROM queue WHERE status = ? ORDER BY id',
['pending']
);
for (const item of pending) {
try {
await this.sync(item);
await db.run('UPDATE queue SET status = ? WHERE id = ?',
['synced', item.id]);
} catch (e) {
await db.run('UPDATE queue SET status = ? WHERE id = ?',
['failed', item.id]);
}
}
}
}- Streaming Large Results: Use HTTP chunked transfer for very large result sets
- WebSocket Support: Real-time change notifications
- Compression: Compress large responses
- Query Cache: Cache frequently-used queries
- Connection Pool: Multiple concurrent connections
- Key-Value Enhancements: TTLs, batch set/get, and store-level metadata
- capacitor-blob-writer - Original inspiration
- SQLite Documentation
- Capacitor Documentation
- NanoHTTPD
- @sqlite.org/sqlite-wasm