Skip to content

Architecture

Locorda’s architecture is designed for offline-first apps that sync automatically using BOS (Bring your Own Storage) — whether that’s Solid Pods, Google Drive, local directories, or any other storage you control. Be the boss of your data. This page explains the components, their responsibilities, and how they work together.

┌─────────────────────────────────────────────────┐
│ Your Flutter App (UI Layer) │
│ • Widgets, State Management, User Interaction │
└────────────────┬────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Your Repository (Data Layer) │
│ • Local Database (Drift/Hive/Isar) │
│ • Query Logic, Transactions │
└────────────────┬────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Locorda Sync Engine (Main Thread) │
│ • Object ↔ RDF Conversion │
│ • Callback Registration │
│ • Main Thread API │
└────────────────┬────────────────────────────────┘
↓ (communicates via isolate)
┌─────────────────────────────────────────────────┐
│ Sync Worker (Background Thread) │
│ • RDF Storage (Quads, Named Graphs) │
│ • Conflict Resolution (CRDT Merge) │
│ • Remote Communication │
└────────────────┬────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Remote Storage Backends │
│ • Solid Pods, Google Drive, Local Dir, etc. │
└─────────────────────────────────────────────────┘

Responsibility: Display data and handle user interactions.

Key Points:

  • Standard Flutter widgets (ListView, TextField, etc.)
  • Works with any state management (Provider, Riverpod, BLoC)
  • No sync awareness needed - just reads/writes via repository

Example:

StreamBuilder<List<Task>>(
stream: repository.watchAll(),
builder: (context, snapshot) {
// Just display the data - sync happens automatically
return ListView(children: snapshot.data!.map(TaskTile.new).toList());
},
)

Responsibility: Connect your local database to Locorda’s sync engine.

Key Points:

  • Owns your local database (Drift, Hive, Isar, etc.)
  • Implements business logic (queries, filters, transactions)
  • Registers callbacks with sync engine
  • Provides reactive streams to UI

Example:

class TaskRepository {
final ObjectSyncEngine _syncEngine;
final MyDatabase _db;
Future<void> save(Task task) => _syncEngine.save<Task>(task);
Stream<List<Task>> watchAll() => _db.watchTasks();
}

See Repository Pattern for implementation details.

Sync Engine Main Thread (Locorda Framework)

Section titled “Sync Engine Main Thread (Locorda Framework)”

Responsibility: Main thread API and object-RDF conversion.

Key Points:

  • Provides the API your app uses (save(), delete(), hydrateWithCallbacks())
  • Converts Dart objects to RDF (via generated mappers)
  • Manages callback registration
  • Forwards operations to worker thread
  • Calls your callbacks when data changes

Not responsible for:

  • Actual sync logic (handled by worker)
  • Remote communication (handled by worker)
  • Storage (handled by worker)

Sync Worker Background Thread (Locorda Framework)

Section titled “Sync Worker Background Thread (Locorda Framework)”

Responsibility: All sync logic in isolation from UI thread.

Key Points:

  • Runs in a separate Dart isolate (web: Web Worker)
  • Stores and retrieves RDF data (using injected storage backend)
  • Implements CRDT merge algorithms
  • Handles remote communication
  • Detects conflicts and resolves them automatically
  • Queues local changes for upload

Why a separate thread?

  • Sync can be slow (network I/O, large merges)
  • Keeps UI responsive during sync
  • Allows sync to continue in background

Responsibility: Persist data to your chosen storage.

Supported BOS Options:

  • Solid Pods - Decentralized personal data stores
  • Google Drive - Cloud storage via Google’s API
  • Local Directory - File system (for testing/debugging)
  • Custom - Implement your own backend (e.g., WebDAV, NAS, etc.)

Interface: All backends implement the same interface, so your code works with any backend without changes.

Storage Format: Backends may store resources as individual files or as RDF Datasets (shard-level files with named graphs). Individual files reduce bandwidth, while datasets reduce remote operation overhead. Some backends let you configure this tradeoff.

This architecture enables:

  • Bidirectional sync: Changes flow both ways (local → remote, remote → local)
  • Worker isolation: Sync operations never block the UI
  • Callback pattern: All changes (local and remote) flow through the same onUpdate/onDelete callbacks
  • Storage flexibility: Use any database you want for your app’s data

Locorda uses two storage layers:

Layer 1: Locorda’s RDF Storage (Worker Thread)

Section titled “Layer 1: Locorda’s RDF Storage (Worker Thread)”

Purpose: Sync metadata and canonical merged state.

Format: RDF triples (each resource in its own named graph)

Managed by: Locorda framework (you don’t access this directly)

Contents:

  • Your data in RDF format
  • Sync metadata (cursors, timestamps, conflict resolution data)
  • Queued local changes waiting for sync

Persistence Options:

  • InMemoryStorage - RAM only (testing)
  • DriftStorage - SQLite via Drift (production)

Purpose: Fast queries, indexes, application-specific structure.

Format: Whatever you want (Drift tables, Hive boxes, Isar collections, etc.)

Managed by: Your repository code

Threading: May run in its own isolate (e.g., Drift) or share a thread - depends on your database choice

Contents:

  • Just your data, structured for your app’s needs
  • No sync metadata

Why two layers?

  • RDF storage: Enables sync, conflict resolution, interoperability
  • Your storage: Enables fast queries, indexes, migrations without RDF knowledge

The main thread and worker thread communicate via message passing:

Main Thread Worker Thread
│ │
├─── save(task) ────────────────────→ │
│ ├─ Store in RDF
│ ├─ Queue for sync
│ │
│ ←──── onUpdate ───── │
├─ Call callback │
│ │

Platform-specific implementation:

  • Dart (mobile/desktop): Dart isolates with SendPort/ReceivePort
  • Web: Web Workers with postMessage()
  1. UI Responsiveness: Sync never blocks the UI
  2. Safety: Worker cannot accidentally access UI state
  3. Resource Control: Worker can use heavy CPU without affecting UI
  4. Crash Isolation: Worker crash doesn’t crash the app

All components are configured in initLocorda():

final locorda = await initLocorda(
// Which remotes to sync with
remotes: [solidPod, googleDrive],
// Where worker stores RDF data
storage: DriftStorageMainHandler(),
// Optional: Worker thread setup
onWorkerSpawn: () => setupLogging(level: Level.INFO),
);

The generated initLocorda() function wires up:

  • All registered resource types
  • RDF mappers
  • CRDT merge strategies
  • Vocabulary definitions

Locorda’s architecture separates concerns:

  1. UI Layer: Display and user interaction (your code)
  2. Repository Layer: Local storage and queries (your code)
  3. Sync Engine Main: Object-RDF conversion (framework)
  4. Sync Worker: Sync logic in background thread (framework)
  5. Remote Backends: Pluggable storage (framework + your choice)

The callback pattern connects these layers, ensuring all changes flow through a single path regardless of their source.