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.
High-Level Overview
Section titled “High-Level Overview”┌─────────────────────────────────────────────────┐│ 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. │└─────────────────────────────────────────────────┘Component Responsibilities
Section titled “Component Responsibilities”UI Layer (Your Code)
Section titled “UI Layer (Your Code)”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()); },)Repository Layer (Your Code)
Section titled “Repository Layer (Your Code)”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
BOS - Remote Storage Backends (Pluggable)
Section titled “BOS - Remote Storage Backends (Pluggable)”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.
Data Flow
Section titled “Data Flow”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/onDeletecallbacks - Storage flexibility: Use any database you want for your app’s data
Storage Layers
Section titled “Storage Layers”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)
Layer 2: Your Application Database
Section titled “Layer 2: Your Application Database”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
Worker Thread Isolation
Section titled “Worker Thread Isolation”Communication Model
Section titled “Communication Model”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()
Benefits of Isolation
Section titled “Benefits of Isolation”- UI Responsiveness: Sync never blocks the UI
- Safety: Worker cannot accidentally access UI state
- Resource Control: Worker can use heavy CPU without affecting UI
- Crash Isolation: Worker crash doesn’t crash the app
Configuration
Section titled “Configuration”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
Next Steps
Section titled “Next Steps”- Sync Lifecycle - How sync states progress
- Conflict Resolution - How conflicts are resolved
- Repository Pattern - Implement repositories
- Data Model - Understanding RDF triples and quads
Summary
Section titled “Summary”Locorda’s architecture separates concerns:
- UI Layer: Display and user interaction (your code)
- Repository Layer: Local storage and queries (your code)
- Sync Engine Main: Object-RDF conversion (framework)
- Sync Worker: Sync logic in background thread (framework)
- 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.