DEV Community

Cover image for How I Built a Zero-Cloud, Air-Gapped Flutter App using C++ FFI, Quant Math, and Optical QR Sync
Alexander Pompili
Alexander Pompili

Posted on

How I Built a Zero-Cloud, Air-Gapped Flutter App using C++ FFI, Quant Math, and Optical QR Sync

Building a modern mobile application usually means relying on the cloud. You hook up a BaaS, send user data to an API, process it through an LLM or a Python backend, and send the results back.

But what happens when you build a personal finance app and absolutely refuse to send a single byte of plaintext data over the internet?

When I started architecting my solo project, KashIQ, I locked myself into a brutal constraint: 100% Offline-First, Zero-Knowledge Architecture (ZKA). No API calls for processing. No plaintext databases.

This created a massive engineering paradox. How do you deliver predictive behavioral analysis, machine learning, and seamless multi-device synchronization without cloud servers, and without freezing the Flutter UI thread?

Here is the architectural deep-dive into how I bypassed the cloud by embedding aerospace algorithms natively via C++ FFI, securing SQLite at the disk level, and building a peer-to-peer optical bridge for E2EE sync.


1. The Storage Layer: AES-256 on SQLite3

Before executing any math, the local storage had to be mathematically un-interceptable. Writing financial data in plaintext on a mobile device is an architectural failure.

  • Reactive State & ORM: The core state management relies on flutter_riverpod for surgical UI rebuilds. For relational persistence, I use Drift, a reactive ORM built on top of SQLite.
  • Military-Grade Disk Encryption: To lock down Drift, I implemented SQLite3MultipleCiphers (SQLCipher). Encryption happens entirely at the disk level using the AES-256 standard. If the device is compromised, the SQLite files yield nothing but random entropy without the hardware-backed Master Key.
  • The Caching Layer: Encrypted SQLite is fast, but hitting the disk for every UI state change is bad practice. I paired it with an ultra-fast, in-memory caching layer using Hive (hive_flutter) to handle non-sensitive UI flags and session preferences, guaranteeing sub-16ms render times.
import 'package:drift/drift.dart';
import 'package:drift/native.dart';

// ...

return NativeDatabase.createInBackground(
  file,
  setup: (db) {
    // Helper function to test if the encryption key is correct
    bool tryWithKey(String key, {String? rekeyTo}) {
      try {
        // Apply the encryption key (AES-256)
        db.execute("PRAGMA key = '$key';");

        // Verify that the database is readable (if the key is wrong, it will throw an exception)
        db.execute("SELECT count(*) FROM sqlite_master;");

        // If a rekey is specified, change the database key on-the-fly
        if (rekeyTo != null && rekeyTo != key) {
          db.execute("PRAGMA rekey = '$rekeyTo';");
        }
        return true;
      } catch (_) {
        return false;
      }
    }

    // 1. Attempt to unlock with the secure key (e.g., hashed with user data)
    if (tryWithKey(secureKey)) return;

    // 2. Attempt with the default "guest" key and, if successful, rekey to the secure key.
    // Great for transitioning a user from offline-only to authenticated.
    if (tryWithKey(rawKeyOnly, rekeyTo: secureKey)) return;

    // Desperate fallback: if we get here, the DB is inaccessible.
    // In a local-first app, if we can't decrypt, we drop the file to recreate it.
    try {
      if (file.existsSync()) {
        file.deleteSync();
        if (tryWithKey(secureKey)) return; // Recreate from scratch with the right key
      }
    } catch (_) {}

    throw Exception("Unable to open the local database: incorrect key or corrupted file.");
  },
);
Enter fullscreen mode Exit fullscreen mode
Future<void> changeEncryptionKey(String newKey) async {
  // Instantly change the database key without having to close the connection
  await customStatement("PRAGMA rekey = '$newKey'");
}
Enter fullscreen mode Exit fullscreen mode

2. The FFI Bridge: Running Quant Math on Mobile

Most "smart" finance apps ship receipt strings to OpenAI to return a JSON category. To do this offline, I couldn't rely on Dart alone—heavy statistical calculations would obliterate the UI thread.

The solution was writing a custom engine in C++ and calling it asynchronously from Flutter using FFI (Foreign Function Interface). This allowed me to embed algorithms derived from algorithmic trading directly into the device's CPU:

  • Local NLP & Self-Healing Data: Entity extraction (amount, date, merchant) runs locally via lightweight heuristic models. If the user corrects a category, the engine updates the statistical weights inside the local SQLite db, plastically adapting to user syntax locally.
  • Kalman Filters (Noise Reduction): Daily spending is full of "white noise" (e.g., an unexpected car repair). I implemented a Kalman Filter—originally used for Apollo 11 trajectory estimation—to smooth out outliers. It compares the budget's predictive trajectory with actual spending, outputting a precise end-of-month liquidity estimation without panicking over a single high-spend anomaly.
  • Markov Chains (State Transitions): Human behavior is a sequence of states. Using stochastic transition matrices, the C++ engine calculates the conditional probability of the next purchase (e.g., if State A = "Gas station", what is the probability of State B = "Dining Out"?). This data feeds the UI to suggest preventive budget blocks.
  • Fast Fourier Transform (FFT): To detect hidden cyclical subscriptions, the FFT takes the chaotic transaction history in the time domain and projects it into the frequency domain. It breaks down expenses into sinusoids, isolating carrier frequencies to uncover seasonal spending patterns invisible to standard SQL GROUP BY queries.

By keeping this math in C++ via FFI, KashIQ processes complex behavioral nudges in under 100 milliseconds without a single network request.

// native/behavioral_engine.cpp
#include <cmath>
#include <cstdlib>

extern "C" {

// 1. Kalman Filter for True Wealth Velocity
// The "visibility" and "used" attributes are crucial to prevent the linker 
// from stripping the symbol during Release builds on iOS/Android.
__attribute__((visibility("default"))) __attribute__((used))
void compute_kalman_wealth_velocity(
    const float* daily_changes, int num_days, 
    float* out_velocity, float* out_variance) {

    if (!daily_changes || num_days <= 0) return;

    float x_est = 0.0f; // Initial state estimate
    float p_est = 1.0f; // Initial estimate uncertainty

    float q = 0.05f; // Process noise covariance
    float r = 50.0f; // Measurement noise covariance

    for (int i = 0; i < num_days; ++i) {
        float z = daily_changes[i];

        // Prediction Phase
        float x_pred = x_est;
        float p_pred = p_est + q;

        // Update Phase (Kalman Gain)
        float k = p_pred / (p_pred + r); 
        x_est = x_pred + k * (z - x_pred);
        p_est = (1.0f - k) * p_pred;
    }

    // Write back to the output pointers to return data to Dart
    if (out_velocity) *out_velocity = x_est;
    if (out_variance) *out_variance = p_est;
}

} // extern "C"
Enter fullscreen mode Exit fullscreen mode
// lib/core/native/behavioral_ffi_bridge.dart
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';

// 1. Typedef Definitions (Mapping the C signature to the Dart signature)
typedef ComputeKalmanWealthVelocityC = Void Function(
    Pointer<Float> dailyChanges, 
    Int32 numDays, 
    Pointer<Float> outVelocity, 
    Pointer<Float> outVariance
);

typedef ComputeKalmanWealthVelocityDart = void Function(
    Pointer<Float> dailyChanges, 
    int numDays, 
    Pointer<Float> outVelocity, 
    Pointer<Float> outVariance
);

class BehavioralFfiBridge {
  static final BehavioralFfiBridge instance = BehavioralFfiBridge._init();

  late final DynamicLibrary _lib;
  ComputeKalmanWealthVelocityDart? computeKalmanWealthVelocity;

  BehavioralFfiBridge._init() {
    // 2. Platform-specific dynamic library loading
    if (Platform.isAndroid) {
      _lib = DynamicLibrary.open('libkashiq_behavioral.so');
    } else if (Platform.isIOS || Platform.isMacOS) {
      // On iOS, C/C++ symbols are statically linked into the executable
      _lib = DynamicLibrary.process(); 
    } else {
      throw UnsupportedError('Unsupported platform for FFI');
    }

    // 3. Binding the C symbol to the Dart function
    try {
      computeKalmanWealthVelocity = _lib
          .lookup<NativeFunction<ComputeKalmanWealthVelocityC>>('compute_kalman_wealth_velocity')
          .asFunction();
    } catch (e) {
      print('Error: Missing FFI symbol for computeKalmanWealthVelocity');
    }
  }
Enter fullscreen mode Exit fullscreen mode
  class KalmanResult {
    final double velocity;
    final double variance;
    KalmanResult({required this.velocity, required this.variance});
  }

  // A clean wrapper exposing a standard Dart API to the rest of the Flutter app
  KalmanResult? getKalmanWealthVelocity(List<double> dailyChanges) {
    if (computeKalmanWealthVelocity == null || dailyChanges.isEmpty) return null;

    // 1. MANUAL ALLOCATION: Allocate unmanaged memory in C (heap) to pass data
    final ptrChanges = calloc<Float>(dailyChanges.length);
    final ptrVelocity = calloc<Float>();
    final ptrVariance = calloc<Float>();

    try {
      // 2. Data copy from managed memory (Dart) to unmanaged memory (C)
      for (int i = 0; i < dailyChanges.length; i++) {
        ptrChanges[i] = dailyChanges[i];
      }

      // 3. Execute the high-performance C function
      computeKalmanWealthVelocity!(ptrChanges, dailyChanges.length, ptrVelocity, ptrVariance);

      // 4. Read back the result
      return KalmanResult(
        velocity: ptrVelocity.value, 
        variance: ptrVariance.value
      );
    } finally {
      // 5. MANUAL DEALLOCATION (CRITICAL! Otherwise, a memory leak will occur)
      calloc.free(ptrChanges);
      calloc.free(ptrVelocity);
      calloc.free(ptrVariance);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

3. The Multi-Device Nightmare: P2P Optical Sync

If there are no cloud servers processing your data, how do you sync a phone with a tablet?

In a true Zero-Knowledge paradigm, cloud storage (like an AWS S3 bucket or Firebase Storage) acts exclusively as a "dumb pipe" holding encrypted blobs. The cloud is cryptographically blind; it cannot hand over the decryption key to a new device.

To eliminate archaic 24-word recovery seeds, I emulated the hardware protocols of air-gapped military networks: The Optical Bridge.

  1. The Air-Gapped Payload: When initiating a sync, the primary device acts as an offline server. It encodes the AES Master Key into a high-density visual payload: a QR Code.
  2. The P2P Handshake: You open the app on the secondary device and scan the QR code via the camera lens. The cryptographic secret transfers opticamente. The Master Key never traverses a TLS tunnel or hits a backend.
  3. Encrypted Blob Ingestion: The exact millisecond the secondary device ingests the visual payload, it connects to the "dumb pipe," downloads the encrypted SQLite backup blob, and decrypts it locally.

From that point forward, both devices maintain asynchronous end-to-end encrypted synchronization. If a conflict occurs, local vector clocks resolve it before the next encrypted blob is pushed to the bucket.

Conclusion: Respecting the User's Thumb (and Data)

You don’t have to trade privacy to build an intelligent, predictive application. By combining flutter_riverpod, Drift, SQLCipher, C++ FFI, and an optical air-gapped QR exchange, it is entirely possible to ship military-grade privacy with a modern, glassmorphism-based UI.

I built KashIQ to prove that a solo developer can construct a bulletproof cognitive exoskeleton that defends a user's wallet without monetizing their financial intimacy. The architecture is the product.

Top comments (2)

Collapse
 
alexshev profile image
Alex Shev

The optical QR sync choice is a great reminder that offline-first is not just “no server.” It needs a deliberate transfer boundary. When the sync mechanism is visible and inspectable, users understand what crossed the gap, which is often more trustworthy than a hidden background channel.

Collapse
 
alexanders-dev profile image
Alexander Pompili

Spot on. You perfectly captured the core philosophy behind this architecture.
The industry is so obsessed with 'seamless' and 'invisible' background syncs that we forgot a basic human truth: invisibility breeds paranoia, especially with financial data. By making the key exchange a physical, optical, and deliberate action, a technical constraint becomes a trust feature. Friction is not always a bug; sometimes it's the exact boundary the user needs to feel safe.
Thanks for reading and for the great insight!