- Node.js 18+
- npm or bun
- Xcode (for iOS development)
- Android Studio (for Android development)
- Install dependencies:
cd capacitor-native-sql
npm install- Build the plugin:
npm run buildThe iOS implementation uses Swift and requires a proper HTTP server library for production use.
Add one of these to your Package.swift:
Option 1: GCDWebServer (Recommended)
.package(url: "https://github.com/swisspol/GCDWebServer.git", from: "3.5.4")Option 2: Telegraph
.package(url: "https://github.com/Building42/Telegraph.git", from: "0.30.0")Option 3: Swifter
.package(url: "https://github.com/httpswift/swifter.git", from: "1.5.0")- The current iOS implementation includes placeholder HTTP server code
- For production, integrate one of the recommended libraries in
SQLHTTPServer.swift - See comments in the file for GCDWebServer integration example
The Android implementation uses NanoHTTPD which is already configured in build.gradle.
npm run verify:androidThe web implementation uses sql.js (SQLite compiled to WebAssembly) loaded from CDN.
- Navigate to example app:
cd example-app
npm install- Run development server:
npm run dev- Open browser to
http://localhost:3000
npm install @capgo/capacitor-native-sql
npx cap sync- Required: Allow local networking in
Info.plist. This plugin runs a local HTTP server that iOS ATS blocks by default:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>This only permits cleartext to loopback addresses (localhost / 127.0.0.1) — it does not weaken ATS for external connections.
-
If using CocoaPods, the plugin will be automatically linked
-
If using SPM, add to your project's Package Dependencies
-
The plugin is automatically linked via Gradle
-
Required: Allow cleartext traffic to
localhost. This plugin runs a local HTTP server that Android 9+ blocks by default.Option A — Scoped to localhost only (recommended):
Create
android/app/src/main/res/xml/network_security_config.xml:<?xml version="1.0" encoding="utf-8"?> <network-security-config> <domain-config cleartextTrafficPermitted="true"> <domain includeSubdomains="false">localhost</domain> <domain includeSubdomains="false">127.0.0.1</domain> </domain-config> </network-security-config>
Then reference it in your
AndroidManifest.xml:<application android:networkSecurityConfig="@xml/network_security_config"> ... </application>
Option B — Allow all cleartext (simpler but less secure):
<application android:usesCleartextTraffic="true"> ... </application>
The plugin uses a custom HTTP-based protocol:
- JavaScript Layer: Uses
fetch()API to communicate with native - Native Layer: Runs a local HTTP server on
127.0.0.1:PORT - Authentication: Bearer token generated per connection
- Data Format: JSON with special handling for binary data (base64)
POST /execute- Execute single SQL statementPOST /batch- Execute multiple statementsPOST /transaction/begin- Begin transactionPOST /transaction/commit- Commit transactionPOST /transaction/rollback- Rollback transaction
- Server only listens on localhost (127.0.0.1)
- Each connection generates a unique authentication token
- Token must be provided in
Authorizationheader - Database name must be provided in
X-Databaseheader
Always use batch operations for multiple inserts:
// Slow - multiple round trips
for (let i = 0; i < 1000; i++) {
await db.run('INSERT INTO items (name) VALUES (?)', [`Item ${i}`]);
}
// Fast - single batch operation
const operations = [];
for (let i = 0; i < 1000; i++) {
operations.push({
statement: 'INSERT INTO items (name) VALUES (?)',
params: [`Item ${i}`]
});
}
await db.executeBatch(operations);Use transactions for related operations:
await db.transaction(async (tx) => {
// All operations here are atomic
await tx.run('...');
await tx.run('...');
});Store binary data directly (no base64 encoding needed in your code):
const imageData = new Uint8Array([...]);
await db.run('INSERT INTO images (data) VALUES (?)', [imageData]);interface CRDTOperation {
id: string;
timestamp: number;
table: string;
operation: 'INSERT' | 'UPDATE' | 'DELETE';
data: any;
vector_clock: Record<string, number>;
}
class CRDTSyncEngine {
async applyRemoteOperation(op: CRDTOperation) {
await db.transaction(async (tx) => {
// Check if operation already applied
const existing = await tx.query(
'SELECT * FROM operations WHERE id = ?',
[op.id]
);
if (existing.length > 0) {
// Resolve conflict using vector clock
// ...
}
// Apply operation
await tx.run(op.operation, op.data);
// Record operation
await tx.run(
'INSERT INTO operations (id, timestamp, data) VALUES (?, ?, ?)',
[op.id, op.timestamp, JSON.stringify(op)]
);
});
}
}class OTSyncEngine {
async applyOperation(op: Operation) {
await db.transaction(async (tx) => {
// Get all operations since this one's parent
const concurrent = await tx.query(
'SELECT * FROM operations WHERE timestamp > ? ORDER BY timestamp',
[op.parentTimestamp]
);
// Transform operation against concurrent operations
let transformed = op;
for (const concurrentOp of concurrent) {
transformed = this.transform(transformed, concurrentOp);
}
// Apply transformed operation
await tx.run(transformed.sql, transformed.params);
});
}
}Encryption is optional and uses SQLCipher. The plugin does not bundle SQLCipher — add it to your app only when you need encryption.
Add the SQLCipher runtime dependency to your app-level android/app/build.gradle:
dependencies {
implementation 'net.zetetic:sqlcipher-android:4.13.0'
}Then connect with encryption:
const db = await FastSQL.connect({
database: 'secure',
encrypted: true,
encryptionKey: 'my-secret-key',
});SQLCipher is optional on iOS. To enable it with CocoaPods, update your ios/App/Podfile to use the SQLCipher subspec:
pod 'CapgoCapacitorFastSql/SQLCipher', :path => '../../node_modules/@capgo/capacitor-fast-sql'Then run pod install from ios/App. If you skip this subspec, keep encrypted: false — encryption calls will return a descriptive error.
If you pass encrypted: true without the SQLCipher dependency, the plugin returns a descriptive error:
Encryption is not available. Add SQLCipher to your build to enable encryption.
The app will not crash — Android additionally catches failures at two levels:
- Class check: Before attempting to load
EncryptedSQLDatabase, the plugin verifies the SQLCipher class is on the classpath viaClass.forName. - Native library check:
EncryptedSQLDatabasecatchesUnsatisfiedLinkErrorfromSystem.loadLibrary("sqlcipher")and converts it to a descriptive exception.
- Check that your Info.plist allows local networking
- Verify that a proper HTTP server library is integrated
- Check console logs for port binding errors
- Verify cleartext traffic is allowed in AndroidManifest.xml
- Check that NanoHTTPD dependency is properly included
- Check logcat for server startup errors
- Check browser console for CDN load errors
- Verify network connectivity
- Consider hosting sql.js locally for offline use
- Use batch operations instead of individual statements
- Wrap related operations in transactions
- Consider indexing frequently queried columns
- Profile with browser DevTools or native profilers
npm run testnpm run verify:iosnpm run verify:android- Update version in
package.json - Build the plugin:
npm run build - Publish to npm:
npm publish
- Fork the repository
- Create a feature branch
- Make your changes
- Write tests
- Submit a pull request
MIT - See LICENSE file for details