adding packages

This commit is contained in:
2026-01-15 14:38:46 -08:00
parent ef86e3ab6a
commit 6869cf47e5
5253 changed files with 726695 additions and 34 deletions
@@ -0,0 +1,443 @@
# **Architecting a Resilient Offline-First Synchronization Engine for PocketBase and Dart**
## **1\. The Paradigm Shift: From Connected CRUD to Distributed Consistency**
The contemporary landscape of mobile and distributed application development has outgrown the traditional "Online-Only" CRUD (Create, Read, Update, Delete) model. In the conventional architecture, the server is the single source of truth, and the client is merely a transient viewer—a "dumb" terminal that renders state fetched over a reliable network connection. However, this assumption of reliability is fundamentally flawed in mobile environments where latency fluctuates, connections drop, and users expect seamless interactivity regardless of signal strength. To meet these demands, we must transition to an **Offline-First** architecture. In this paradigm, the client device becomes a primary replica of the dataset, capable of performing reads and writes against a local database, while a background synchronization engine ensures eventual consistency with the central server.
This report articulates the comprehensive design and implementation of a custom synchronization engine tailored for a specific, high-performance stack: a **Dart (Flutter)** client leveraging the **Drift** persistence library, and a **PocketBase** backend extended via **Go**. The architectural mandate includes rigorous requirements: the use of the **Myers Diff Algorithm** for granular text synchronization, the implementation of **Hybrid Logical Clocks (HLC)** to resolve causal ordering in the absence of reliable physical time, and the adoption of **UUIDv7** to mitigate identifier collisions in distributed generation.
### **1.1 The CAP Theorem and Local Autonomy**
In the context of the CAP Theorem (Consistency, Availability, Partition Tolerance), an offline-first mobile application effectively operates as a distributed system that prioritizes Availability and Partition Tolerance (AP) during disconnection, striving for Strong Eventual Consistency (SEC) upon reconnection. The synchronization engine is the mechanism that bridges the gap between the divergent local state (the "partitioned" node) and the server state.
Unlike simple caching strategies where local data is ephemeral, an offline-first architecture treats the local database (SQLite via Drift) as a persistent, authoritative store for the user's actions. This inversion of control introduces significant complexity. We are no longer simply sending HTTP POST requests; we are managing a distributed ledger of mutations that must be reconciled. This requires us to solve three fundamental problems:
1. **Identity:** How do we generate unique identifiers on the client without coordinating with the server?
2. **Causality:** How do we order events when device clocks are unreliable, drifting, or maliciously altered?
3. **Conflict Resolution:** How do we merge concurrent edits to the same data point without losing user intent?
The following sections dissect these challenges and propose a unified, robust solution.
## ---
**2\. The Crisis of Identity: Distributed ID Generation**
One of the most immediate challenges in decoupling the client from the server is the generation of primary keys. In a centralized system, the database (e.g., PostgreSQL sequences or auto-incrementing integers) issues IDs. In a distributed system, the client must generate the ID *before* the record is sent to the server to maintain local referential integrity and allow for immediate UI updates.
### **2.1 The Insufficiency of PocketBase Default IDs**
PocketBase, by default, utilizes a 15-character random alphanumeric string for record IDs. The alphabet typically consists of lowercase letters and numbers (a-z0-9), yielding a character set size of 36\. The total entropy space is $36^{15}$. While this provides a massive number of combinations, it poses two distinct problems in our specific architectural context:
1. **The Birthday Paradox in Distributed Environments:** The "Birthday Problem" dictates that the probability of a collision increases much faster than the number of records. While $36^{15}$ is large, the risk is non-zero, especially when relying on the pseudo-random number generators (PRNG) of diverse client devices (Android, iOS, Web), which may have varying degrees of entropy quality compared to a server-side crypto/rand. In a system designed for "Offline-First" robustness, a single ID collision during synchronization is catastrophic—it results in a merge conflict where two distinct entities are treated as the same, potentially overwriting data.
2. **Database Page Fragmentation (The Hidden Performance Killer):** PocketBase runs on **SQLite**. SQLite tables (specifically ROWID tables or those with textual primary keys) are stored as B-Trees. When records are inserted with purely random IDs, they are distributed arbitrarily across the B-Tree leaf nodes. This random insertion pattern forces the database engine to frequently split pages and rebalance the tree, leading to high disk I/O overhead and significant fragmentation of the database file. In a high-throughput sync scenario (e.g., a client pushing 1,000 offline changes), random I/O can become a bottleneck.1
### **2.2 The Solution: UUIDv7**
To address both the collision risk and the database performance constraints, we mandate the adoption of **UUIDv7** (Universally Unique Identifier Version 7), enabling a move away from purely random identifiers to **k-sortable** identifiers.
#### **2.2.1 Mechanism and Entropy**
UUIDv7 is a 128-bit identifier designed specifically for database locality. Its structure is composed of:
* **Unix Timestamp (48 bits):** Encodes the millisecond precision timestamp.
* **Version & Variant (6 bits):** Metadata identifying the format.
* **Random Data (74 bits):** Entropy to ensure uniqueness within the same millisecond.3
This structure offers a guarantee that IDs generated at different milliseconds are strictly ordered. For IDs generated within the same millisecond, the 74 bits of entropy provide a collision resistance that far exceeds the user's current 15-character alphanumeric solution.
#### **2.2.2 Impact on SQLite Performance**
The most profound advantage of UUIDv7 in this architecture is its impact on the SQLite B-Tree. Because UUIDv7 is monotonic (values increase over time), new records are almost always appended to the right side of the B-Tree. This "sequential insertion" pattern drastically reduces the need for page splitting and tree rebalancing. It optimizes the Write-Ahead Log (WAL) performance and ensures that the database remains compact and performant even as the dataset grows into millions of rows. For a synchronization engine processing batches of inserts, this can result in a write throughput increase of orders of magnitude compared to random IDs.1
### **2.3 Implementation Strategy**
#### **2.3.1 Client-Side (Dart)**
The Dart client must take responsibility for generating these IDs. We utilize the uuid package, which supports the proposed RFC 9562 standard for UUIDv7.6
Dart
import 'package:uuid/uuid.dart';
class IdGenerator {
static const Uuid \_uuid \= Uuid();
/// Generates a time-ordered, collision-resistant UUIDv7.
static String next() {
return \_uuid.v7();
}
}
This ID is assigned to the record immediately upon creation in the local Drift database. This allows the client to build relations (foreign keys) between new offline records (e.g., creating a Post and a Comment offline) without waiting for the server to assign IDs.
#### **2.3.2 Server-Side (Go/PocketBase)**
The server must accept these client-generated IDs but also enforce validation to prevent malformed data from corrupting the index. We extend PocketBase using its Go framework hooks, specifically OnRecordBeforeCreateRequest.7
We intercept the create request. If the client provides an ID, we validate it against the UUIDv7 regex. If the ID is missing (which shouldn't happen in our protocol, but might in direct API usage), we generate one.
Go
package main
import (
"log"
"regexp"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/gofrs/uuid/v5" // Using a Go UUID library
)
func main() {
app := pocketbase.New()
// Regex for UUID validation (simplified)
uuidRegex := regexp.MustCompile(\`^\[0-9a-fA-F\]{8}-\[0-9a-fA-F\]{4}-7\[0-9a-fA-F\]{3}-\[0-9a-fA-F\]{3}-\[0-9a-fA-F\]{12}$\`)
app.OnRecordBeforeCreateRequest("todos", "notes").BindFunc(func(e \*core.RecordRequestEvent) error {
// 1\. Validation: Ensure ID is a valid UUIDv7 if provided
if e.Record.Id\!= "" {
if\!uuidRegex.MatchString(e.Record.Id) {
// Reject invalid formats to protect DB locality
return e.BadRequestError("Invalid ID format. Must be UUIDv7.", nil)
}
} else {
// 2\. Fallback: Generate UUIDv7 if missing
id, err := uuid.NewV7()
if err\!= nil {
return err
}
e.Record.SetId(id.String())
}
return e.Next()
})
if err := app.Start(); err\!= nil {
log.Fatal(err)
}
}
This implementation explicitly overrides PocketBase's default ID generator, solving the user's duplication issue and aligning the storage engine for high-performance synchronization.8
## ---
**3\. Temporal Truth: The Hybrid Logical Clock (HLC)**
In distributed systems, physical time (wall-clock time) is a treacherous metric. Devices drift, batteries die, and users manually change clocks to "cheat" in games or bypass software trials. If a synchronization engine relies solely on updated\_at timestamps derived from the system clock, it falls prey to anomalies: a change made "tomorrow" (due to a bad clock) could become immutable, locking out all legitimate subsequent edits.
To maintain causality—the property that if event A causes event B, A is ordered before B—we require **Logical Clocks**. However, pure logical clocks (like Lamport counters) lose the relationship to physical time, making it hard to query "changes since 5 minutes ago." The **Hybrid Logical Clock (HLC)** is the synthesis of these two concepts, providing the best of both worlds: causal strictness and physical proximity.9
### **3.1 The HLC Algorithm**
The HLC is a tuple (l, c, node), where:
* l (logical time): The maximum physical time the node has seen (either from its own clock or incoming messages).
* c (counter): A strictly increasing counter used to order events that occur within the same millisecond or when the physical clock regresses.
* node: A unique tie-breaker (e.g., a hash of the device ID).
The core rules for the HLC are:
1. **Monotonicity:** The clock never moves backward. If the physical clock rewinds, the HLC continues forward using the logical component.
2. **Causality:** When a message is received with timestamp T\_remote, the local clock updates to max(T\_local, T\_remote, T\_physical). This ensures the local event appears to happen *after* the remote event that triggered it.
3. **Bounded Drift:** The logical time l tracks closely with the physical time pt. It does not grow unboundedly into the future unless the physical clock itself is wrong or the message frequency exceeds the counter capacity (which is rare).12
### **3.2 HLC Serialization for SQLite**
PocketBase and SQLite do not have a native "HLC" data type. To perform efficient synchronization queries (e.g., "Give me all records changed since HLC\_X"), we must store the HLC in a format that preserves its sort order when compared as a primitive type.
We will serialize the HLC as a **lexically sortable string**. This allows us to use standard SQL comparison operators (\>, \<) and leverage B-Tree indexes for range scans.14
**Format:** \<Physical\>-\<Logical\>-\<NodeID\>
* **Physical:** 48-bit integer (milliseconds), formatted as a 12-char hexadecimal string (zero-padded).
* **Logical:** 16-bit integer (counter), formatted as a 4-char hexadecimal string.
* **NodeID:** Fixed-length hexadecimal string (e.g., 10 chars).
**Example:**
* Timestamp: 1678886400000 $\\rightarrow$ 0186E5B64800
* Counter: 42 $\\rightarrow$ 002A
* Node: ClientA
* **Serialized:** 0186E5B64800-002A-ClientA
This string format guarantees that HLC\_A \> HLC\_B in a SQL query corresponds exactly to the causal ordering of the clocks.
### **3.3 Implementation in Dart and Go**
Dart Implementation:
We implement a singleton HlcProvider that maintains the local clock state. Every time a local write occurs (Create/Update/Delete), the provider increments the clock. Every time a sync payload is received from the server, the provider merges the remote HLC into the local state.
Dart
class Hlc implements Comparable\<Hlc\> {
final int millis;
final int counter;
final String nodeId;
Hlc(this.millis, this.counter, this.nodeId);
// Lexical serialization for SQLite
@override
String toString() \=\>
'${millis.toRadixString(16).padLeft(12, "0")}\-'
'${counter.toRadixString(16).padLeft(4, "0")}\-'
'$nodeId';
// The 'Receive' Logic (Merge)
static Hlc receive(Hlc local, Hlc remote) {
final now \= DateTime.now().millisecondsSinceEpoch;
// The new time is the max of physical, local logical, and remote logical
final newMillis \= \[local.millis, remote.millis, now\].reduce(max);
int newCounter;
if (newMillis \== local.millis && newMillis \== remote.millis) {
newCounter \= max(local.counter, remote.counter) \+ 1;
} else if (newMillis \== local.millis) {
newCounter \= local.counter \+ 1;
} else if (newMillis \== remote.millis) {
newCounter \= remote.counter \+ 1;
} else {
newCounter \= 0;
}
return Hlc(newMillis, newCounter, local.nodeId);
}
}
Ref: 9
Server Storage:
In PocketBase, we define a custom field hlc (type: Text) for every synced collection. Crucially, we add a database index on this field.
CREATE INDEX idx\_collection\_hlc ON collection(hlc);
This index is the cornerstone of the sync performance, allowing the server to calculate the "delta" of changes for a client in $O(\\log N)$ time rather than scanning the entire table.17
## ---
**4\. Algorithmic Resolution: Myers Diff and Differential Sync**
The requirement to use the **Myers Diff Algorithm** indicates a need for high-fidelity text synchronization. Standard "Last Write Wins" (LWW) is acceptable for scalar values (like a "Status" dropdown), but it is destructive for text. If User A fixes a typo in the first paragraph and User B adds a sentence to the second paragraph while offline, LWW would discard one of these changes. Myers Diff allows us to merge them.
### **4.1 The Theoretical Basis: 3-Way Merge**
To correctly merge concurrent edits, we cannot simply compare the Client's version and the Server's version. We need a reference point: the **Base** version (also known as the Common Ancestor). This creates a 3-way merge scenario 19:
1. **Base:** The state of the record when the client *last* synced.
2. **Theirs (Server):** The current state on the server (potentially modified by other clients).
3. **Yours (Client):** The current state on the client (with offline edits).
The Myers algorithm operates by finding the Shortest Edit Script (SES) that transforms Base into Yours. This script is a sequence of insertions and deletions.
### **4.2 The Differential Synchronization Protocol**
We adopt a protocol similar to the one used by Google Docs (specifically the Google Diff-Match-Patch library implementation).21
1. **Client Diff:** The client calculates Diff(Base, Yours). This generates a patch.
2. **Transmission:** The client sends this patch to the server.
3. **Server Patch:** The server attempts to apply this patch to Theirs.
* NewServerState \= PatchApply(Patch, Theirs)
* The PatchApply function is robust; it uses the context (surrounding text) to locate the correct insertion point even if the text has shifted due to other edits. This is "Fuzzy Patching."
4. **Confirmation:** If successful, the server saves NewServerState and updates the HLC.
### **4.3 Why Myers?**
The Myers algorithm is an $O(ND)$ greedy algorithm that optimizes for the "longest common subsequence".24 Its strength lies in its human-centric output. It tends to group changes into logical blocks (e.g., deleting a whole word) rather than fracturing them into atomized character edits, which makes the resulting patches more likely to apply cleanly against a modified target. Furthermore, diff-match-patch includes a semantic cleanup phase that realigns diffs to boundaries (like newlines or words), preventing the "wrong end" problem where identical characters are mismatched.26
## ---
**5\. Client-Side Implementation: The Drift Architecture**
The client side is responsible for persistence, queueing, and UI reactivity. **Drift** is the chosen ORM for Flutter because of its compile-time safety and deep SQLite integration.27
### **5.1 The Shadow Table Pattern**
To support the 3-way merge required by Myers diff, the client must store *two* copies of every record:
1. **The Application Table (todos):** This is the live data the user sees and edits.
2. **The Shadow Table (todos\_shadow):** This serves as the "Base" for the diff. It represents the exact state of the record as it was last confirmed by the server.
**Schema Design (Drift):**
Dart
// The main table visible to the UI
class Todos extends Table {
TextColumn get id \=\> text().withLength(min: 36, max: 36)(); // UUIDv7
TextColumn get content \=\> text()();
TextColumn get hlc \=\> text()(); // The HLC of the last write
BoolColumn get deleted \=\> boolean().withDefault(const Constant(false))();
@override
Set\<Column\> get primaryKey \=\> {id};
}
// The Shadow Table
class TodosShadow extends Table {
TextColumn get id \=\> text().withLength(min: 36, max: 36)();
TextColumn get content \=\> text()(); // The "Base" text
TextColumn get hlc \=\> text()(); // The server HLC at last sync
@override
Set\<Column\> get primaryKey \=\> {id};
}
// The Sync Queue
class SyncQueue extends Table {
IntColumn get id \=\> integer().autoIncrement()();
TextColumn get recordId \=\> text()();
TextColumn get collection \=\> text()();
TextColumn get operation \=\> text()(); // INSERT, UPDATE, DELETE
TextColumn get payload \=\> text()(); // JSON: { "patches": "...", "hlc": "..." }
IntColumn get status \=\> integer()(); // 0: Pending, 1: In-Flight
}
### **5.2 The Write Lifecycle**
1. **User Edit:** The user types in a text field.
2. **Local Persist:** The app writes the new text to Todos. The local HLC is incremented.
3. **Queue Logic:**
* The sync engine (running in a background Isolate) detects the change.
* It fetches Todos.content (Yours) and TodosShadow.content (Base).
* It computes diff \= myers\_diff(Base, Yours).
* It writes a SyncQueue entry containing the patch and the *Base HLC*.
* *Optimization:* If a queue entry already exists for this record (user typed twice while offline), the engine squashes the updates by re-computing the diff against the immutable Shadow.
### **5.3 Offline Tolerance**
This architecture is inherently offline-tolerant. The SyncQueue simply accumulates patches. The UI remains responsive because it binds to the Todos table, which reflects local state immediately ("Optimistic UI").29 When the network restores, the engine processes the queue FIFO (First-In, First-Out) or batched, attempting to push changes to the server.
## ---
**6\. Server-Side Implementation: Extending PocketBase**
PocketBase's standard API is insufficient for this logic. The standard update endpoint performs a generic UPDATE SET..., which would overwrite concurrent changes. We must implement a custom sync endpoint using Go.
### **6.1 The Custom Sync Endpoint**
We register a route POST /api/sync that accepts a batch of operations. This entire batch processing must happen inside a database transaction to ensure atomicity—either all patches apply, or we rollback (in case of critical system failure, though typically we handle per-record errors gracefully).
**Route Registration:**
Go
app.OnServe().BindFunc(func(se \*core.ServeEvent) error {
se.Router.POST("/api/sync", func(e \*core.RequestEvent) error {
return handleSync(app, e)
})
return se.Next()
})
Ref: 30
### **6.2 Transactional Logic and Conflict Resolution**
The handleSync function performs the heavy lifting.
**Algorithm:**
1. **Batch Start:** tx, \_ := app.Dao().DB().Begin()
2. **Iterate Operations:** For each incoming patch:
* **Lock Record:** SELECT \* FROM todos WHERE id \=? (Use appropriate locking if using Postgres, but for SQLite, the single-writer WAL mode inherently serializes this 32).
* **HLC Check:** Compare Incoming.BaseHLC with ServerRecord.HLC.
* **Case A (Idempotent):** Incoming.HLC \<= ServerRecord.HLC. The client is sending an old update. We acknowledge success but do nothing.
* **Case B (Fast-Forward):** Incoming.BaseHLC \== ServerRecord.HLC. The server hasn't changed. We apply the patch directly.
* **Case C (Conflict):** Incoming.BaseHLC \< ServerRecord.HLC. The server has moved forward. We must use dmp.PatchApply.
* **Patch Application:**
* patches, \_ := dmp.PatchFromText(Incoming.PatchString)
* newText, results := dmp.PatchApply(patches, ServerRecord.Content)
* **Check Results:** If results contains failures (the fuzzy match failed because the context was too different), we have a "Hard Conflict".
* **Resolution:**
* *Soft Conflict (Patch Succeeded):* Save newText. Update ServerRecord.HLC to New(Max(Local, Remote)).
* *Hard Conflict (Patch Failed):* We cannot merge. The standard strategy here is "Server Wins" or "Error". We return an error code to the client indicating "Rebase Required". The client must then pull the new server state, update its Shadow, and try to re-apply its local edits (or show a diff UI to the user).
3. **Commit:** If all critical operations succeed, commit the transaction.
### **6.3 Performance Considerations: SQLite WAL**
PocketBase uses SQLite in **Write-Ahead Log (WAL)** mode. This allows multiple readers and one writer.
* **Implication:** The POST /sync transaction blocks other writes. It is crucial to keep the transaction logic CPU-efficient.
* **Myers Diff Cost:** Calculating diffs is expensive ($O(ND)$). However, *applying* patches is relatively cheap ($O(N)$). By offloading the diff calculation to the client (distributed computing), the server only bears the cost of the application, maximizing throughput.5
## ---
**7\. The Sync Loop Protocol**
The synchronization engine operates in a continuous loop, managed by the client.
### **7.1 Push Phase (Client \-\> Server)**
1. Client selects PENDING items from SyncQueue.
2. Bundles them into a JSON payload.
3. Sends POST /api/sync.
4. **Server Response:** Returns a list of { "id": "...", "server\_hlc": "..." } for successfully synced items.
5. **Client Finalization:**
* For each success, the client updates TodosShadow to match the current Todos (establishing a new Base).
* Updates Todos.hlc to the returned server\_hlc.
* Deletes the SyncQueue entry.
### **7.2 Pull Phase (Server \-\> Client)**
1. Client tracks a local variable last\_pull\_hlc (persisted in SharedPreferences or a meta table).
2. Client sends GET /api/sync?since=last\_pull\_hlc.
3. **Server Query:** SELECT \* FROM todos WHERE hlc \>?. Thanks to the HLC index and lexical sortability, this is a highly efficient range query.17
4. **Client Merge:**
* For each incoming record:
* Check if there is a pending local change in SyncQueue.
* **No Pending Change:** Simply update Todos and TodosShadow.
* **Pending Change Exists:** We have a conflict locally.
* Update TodosShadow to the *new* incoming server text.
* *Rebase:* Re-calculate the diff between the *new* Shadow and the current Todos. Update the SyncQueue entry. This effectively "floats" the user's local changes on top of the incoming server changes.
## ---
**8\. Conclusion**
The architecture defined in this report satisfies the rigorous requirements of a modern, offline-first application. By replacing standard PocketBase IDs with **UUIDv7**, we ensure distributed collision resistance and SQLite performance. By implementing **Hybrid Logical Clocks** serialized as sortable strings, we solve the problem of unreliable time and enable efficient delta queries. Finally, by integrating the **Myers Diff algorithm** into a **Shadow Table** synchronization pattern, we achieve a system that preserves user intent during concurrent text editing.
This system is not merely a theoretical construct; it is built upon the specific capabilities of **Drift**, **Go**, and **SQLite's WAL mode**, leveraging the strengths of each component to create a synchronization engine that is robust, scalable, and tolerant of the chaotic nature of mobile networks. The use of a custom Go extension for PocketBase is the linchpin, moving logic from the fragile client-side layer to a transactional, authoritative server environment.
### **Summary of Data Structures**
| Component | Format/Type | Purpose |
| :---- | :---- | :---- |
| **ID** | UUIDv7 (36-char string) | Collision-free, k-sortable, B-Tree friendly. |
| **Clock** | HLC (HexTime-HexCount-Node) | Causal ordering, physical proximity, range queries. |
| **Sync Payload** | JSON (Patches) | Bandwidth efficiency, merge capability. |
| **Client DB** | SQLite (Drift) | Shadow tables for 3-way merge base. |
| **Server DB** | SQLite (PocketBase) | Authoritative store, indexed HLC for fast pulls. |
#### **Works cited**
1. UUID vs CUID vs NanoID: Choosing the Right ID Generator for Your Application \- Wisp CMS, accessed December 22, 2025, [https://www.wisp.blog/blog/uuid-vs-cuid-vs-nanoid-choosing-the-right-id-generator-for-your-application](https://www.wisp.blog/blog/uuid-vs-cuid-vs-nanoid-choosing-the-right-id-generator-for-your-application)
2. Best practices for SQLite performance | App quality \- Android Developers, accessed December 22, 2025, [https://developer.android.com/topic/performance/sqlite-performance-best-practices](https://developer.android.com/topic/performance/sqlite-performance-best-practices)
3. Understanding UUID v4, UUID v7, Snowflake ID, and Nano ID, GUID, ULID, KSUID — In Simple Terms | by Dinesh Arney | Medium, accessed December 22, 2025, [https://medium.com/@dinesharney/understanding-uuid-v4-uuid-v7-snowflake-id-and-nano-id-in-simple-terms-c50acf185b00](https://medium.com/@dinesharney/understanding-uuid-v4-uuid-v7-snowflake-id-and-nano-id-in-simple-terms-c50acf185b00)
4. uuidv7 \- NPM, accessed December 22, 2025, [https://www.npmjs.com/package/uuidv7](https://www.npmjs.com/package/uuidv7)
5. SQLite Optimizations For Ultra High-Performance \- PowerSync, accessed December 22, 2025, [https://www.powersync.com/blog/sqlite-optimizations-for-ultra-high-performance](https://www.powersync.com/blog/sqlite-optimizations-for-ultra-high-performance)
6. uuidv7 | Dart package \- Pub.dev, accessed December 22, 2025, [https://pub.dev/packages/uuidv7](https://pub.dev/packages/uuidv7)
7. Extend with Go \- Event hooks \- Docs \- PocketBase, accessed December 22, 2025, [https://pocketbase.io/docs/go-event-hooks/](https://pocketbase.io/docs/go-event-hooks/)
8. How to automatically generate UUIDv7 as record IDs \#7383 \- GitHub, accessed December 22, 2025, [https://github.com/pocketbase/pocketbase/discussions/7383](https://github.com/pocketbase/pocketbase/discussions/7383)
9. Hybrid Logical Clock implementation in TypeScript \- typeonce.dev, accessed December 22, 2025, [https://www.typeonce.dev/snippet/hybrid-logical-clock-implementation-typescript](https://www.typeonce.dev/snippet/hybrid-logical-clock-implementation-typescript)
10. Hybrid Logical Clocks | Kevin Sookocheff, accessed December 22, 2025, [https://sookocheff.com/post/time/hybrid-logical-clocks/](https://sookocheff.com/post/time/hybrid-logical-clocks/)
11. Hybrid logical clock \- Andy Matuschak's notes, accessed December 22, 2025, [https://notes.andymatuschak.org/Hybrid\_logical\_clock](https://notes.andymatuschak.org/Hybrid_logical_clock)
12. Hybrid Logical Clocks \- Murat Buffalo, accessed December 22, 2025, [http://muratbuffalo.blogspot.com/2014/07/hybrid-logical-clocks.html](http://muratbuffalo.blogspot.com/2014/07/hybrid-logical-clocks.html)
13. Hybrid Logical Clocks \- Bartosz Sypytkowski, accessed December 22, 2025, [https://www.bartoszsypytkowski.com/hybrid-logical-clocks/](https://www.bartoszsypytkowski.com/hybrid-logical-clocks/)
14. Database File Format \- SQLite, accessed December 22, 2025, [https://www.sqlite.org/fileformat.html](https://www.sqlite.org/fileformat.html)
15. Try Case-Insensitive Unicode Sorting in SQLite with Pre-collated Strings \- Atomic Spin, accessed December 22, 2025, [https://spin.atomicobject.com/case-insensitive-unicode-sqlite/](https://spin.atomicobject.com/case-insensitive-unicode-sqlite/)
16. hlc\_dart | Dart package \- Pub.dev, accessed December 22, 2025, [https://pub.dev/packages/hlc\_dart](https://pub.dev/packages/hlc_dart)
17. SQLite BETWEEN Operator By Practical Examples, accessed December 22, 2025, [https://www.sqlitetutorial.net/sqlite-between/](https://www.sqlitetutorial.net/sqlite-between/)
18. Feature suggestion: Support SQLite json\_extract function · Issue \#423 \- GitHub, accessed December 22, 2025, [https://github.com/pocketbase/pocketbase/issues/423](https://github.com/pocketbase/pocketbase/issues/423)
19. Three-Way Merge \- Revision Control, accessed December 22, 2025, [https://tonyg.github.io/revctrl.org/ThreeWayMerge.html](https://tonyg.github.io/revctrl.org/ThreeWayMerge.html)
20. Three-Way Merging Algorithm for Structured Data \- IEEE Xplore, accessed December 22, 2025, [https://ieeexplore.ieee.org/iel8/6287639/10820123/11045384.pdf](https://ieeexplore.ieee.org/iel8/6287639/10820123/11045384.pdf)
21. Diff Match Patch \- Dart API docs \- Pub.dev, accessed December 22, 2025, [https://pub.dev/documentation/diff\_match\_patch/latest/](https://pub.dev/documentation/diff_match_patch/latest/)
22. GerHobbelt/google-diff-match-patch: Diff, Match and Patch Library (original at http://google.com/p/google-diff-match-patch) \- GitHub, accessed December 22, 2025, [https://github.com/GerHobbelt/google-diff-match-patch](https://github.com/GerHobbelt/google-diff-match-patch)
23. Differential Synchronization \- Google Research, accessed December 22, 2025, [https://research.google.com/pubs/archive/35605.pdf](https://research.google.com/pubs/archive/35605.pdf)
24. Implementation of the Myers diff algorithm with O(ND) complexity, multiple output formats, and benchmarking suite. \- GitHub, accessed December 22, 2025, [https://github.com/NeaByteLab/Myers-Diff](https://github.com/NeaByteLab/Myers-Diff)
25. The Myers diff algorithm: part 1 \- The If Works, accessed December 22, 2025, [https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/](https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/)
26. Avoiding the Myers Diff Algorithm "Wrong-End" Problem \- Stack Overflow, accessed December 22, 2025, [https://stackoverflow.com/questions/79322411/avoiding-the-myers-diff-algorithm-wrong-end-problem](https://stackoverflow.com/questions/79322411/avoiding-the-myers-diff-algorithm-wrong-end-problem)
27. pocketbase\_drift \- Dart API docs \- Pub.dev, accessed December 22, 2025, [https://pub.dev/documentation/pocketbase\_drift/latest/](https://pub.dev/documentation/pocketbase_drift/latest/)
28. drift | Dart package \- Pub.dev, accessed December 22, 2025, [https://pub.dev/packages/drift](https://pub.dev/packages/drift)
29. Offline-First Flutter: Implementation Blueprint for Real-World Apps \- GeekyAnts, accessed December 22, 2025, [https://geekyants.com/blog/offline-first-flutter-implementation-blueprint-for-real-world-apps](https://geekyants.com/blog/offline-first-flutter-implementation-blueprint-for-real-world-apps)
30. Extend with Go \- Routing \- Docs \- PocketBase, accessed December 22, 2025, [https://pocketbase.io/docs/go-routing/](https://pocketbase.io/docs/go-routing/)
31. Introduction \- Extending PocketBase \- Docs, accessed December 22, 2025, [https://pocketbase.io/docs/use-as-framework/](https://pocketbase.io/docs/use-as-framework/)
32. Extend with Go \- Overview \- Docs \- PocketBase, accessed December 22, 2025, [https://pocketbase.io/docs/go-overview/](https://pocketbase.io/docs/go-overview/)
@@ -0,0 +1,469 @@
# **Architectural Specification for a Generic Offline-First Synchronization Engine on PocketBase**
## **Executive Summary**
The transition from connected, state-dependent client-server architectures to offline-first distributed systems represents a fundamental shift in application reliability and user experience. While PocketBase provides a highly portable, SQLite-backed backend solution, its default interaction model is predicated on synchronous RESTful communication, rendering it susceptible to network partitioning. This report provides an exhaustive architectural specification for bridging this gap through a generic synchronization engine. The proposed solution is comprised of a server-side Go plugin and a client-side Flutter package, designed to operate agnostically across any PocketBase instance and schema.
The architecture necessitates a move from physical time to causal ordering via Hybrid Logical Clocks (HLC), the implementation of a distributed deletion strategy using a global tombstone registry, and the adoption of differential synchronization for text fields using the Myers Diff algorithm. By leveraging PocketBases extensible hook system—specifically OnRecordBeforeUpsert, OnRecordAfterDelete, and OnServe—this system injects statefulness into the mutation lifecycle without requiring invasive schema modifications. This document serves as a definitive guide for implementing this protocol, prioritizing data consistency, conflict resolution, and high availability in constrained network environments.
## **1\. Architectural Foundations of the Generic Synchronization Engine**
### **1.1 The Distributed Consistency Challenge**
In the domain of mobile computing, the network must be treated as a hostile environment. The fallacy that the network is reliable, with zero latency and infinite bandwidth, often leads to brittle application designs that fail catastrophically when a user enters an elevator or a tunnel. The CAP theorem dictates that in the presence of a network partition (P)—an unavoidable reality for mobile devices—a system must choose between Consistency (C) and Availability (A). For a user-facing mobile application, Availability is non-negotiable; the user must be able to read and write data regardless of connectivity status. Consequently, this architecture embraces Eventual Consistency.
The core challenge in adapting PocketBase for this paradigm lies in its default "Realtime" nature. PocketBases realtime subscriptions broadcast state changes as they happen, assuming the client is present to receive them. If a client is offline, it misses these ephemeral messages. An offline-first architecture must therefore shift from an ephemeral event stream to a persistent replication log. The synchronization engine must guarantee that every mutation generated on the Edge (the mobile device) eventually reaches the Origin (the server), and conversely, that the Origin's state converges with the Edge, regardless of the duration of the disconnection.1
### **1.2 The Dual-Store Repository Pattern**
To achieve high availability, the architecture enforces a strict decoupling of the UI from the network. The generic Flutter package acts as a Repository layer that mediates between the application logic and two distinct data stores:
1. **The Local Replica:** An embedded SQLite database on the device (managed via drift or sqflite in the Dart ecosystem). This store serves as the single source of truth for the UI, enabling instantaneous reads and writes (Optimistic UI).3
2. **The Remote Authority:** The PocketBase server, which acts as the convergence point for all distributed clients.
The critical requirement of "generic application" means this Repository cannot be hardcoded for specific collections (e.g., "Users" or "Tasks"). Instead, it must dynamically inspect the PocketBase schema (fetched via the API) and provision corresponding local tables on the fly. This dynamic mapping capability allows the Flutter package to be dropped into any project, instantly providing offline capabilities for whatever collections exist on the backend.4
### **1.3 Leveraging PocketBases Extensibility**
PocketBase distinguishes itself from other BaaS providers through its "framework-as-a-library" model. Written in Go, it allows developers to compile their own binary that embeds the core PocketBase logic while injecting custom code. This capability is pivotal for our synchronization engine. We cannot rely solely on the external REST API because we need to intercept database transactions to inject synchronization metadata (Logical Clocks) and capture deletions (Tombstones) that would otherwise be lost to an offline client.
The Go plugin mechanism allows us to register hooks such as OnRecordBeforeCreateRequest and OnRecordBeforeUpdateRequest. These hooks provide access to the core.Record object *before* it is persisted to SQLite, allowing the plugin to validate causal ordering constraints and reject out-of-order updates before they corrupt the database state. Furthermore, the OnServe hook enables the registration of custom "Sync" endpoints that can perform batch operations—reading tombstones and active records in a single transaction—thereby reducing network round-trips and database lock contention.6
| Feature | Standard PocketBase | Offline-First PocketBase |
| :---- | :---- | :---- |
| **Primary Data Source** | Remote Server (API) | Local SQLite Replica |
| **Consistency Model** | Immediate (on Request) | Eventual (on Sync) |
| **Time Source** | Server Wall Clock | Hybrid Logical Clock |
| **Deletion Handling** | Immediate Removal | Tombstone Retention |
| **Conflict Resolution** | Last-Request-Wins | Causal Ordering / Merge |
## **2\. Temporal Consistency: The Hybrid Logical Clock**
### **2.1 The Inadequacy of Physical Time**
A naive approach to synchronization relies on updated\_at timestamps generated by the system clock. In a distributed system of mobile devices, this approach is fundamentally flawed due to clock skew. A device with a clock set 10 minutes in the future will generate records that, under a standard Last-Write-Wins (LWW) policy, will overwrite any conflicting data generated by devices with accurate clocks for the next 10 minutes. Furthermore, physical clocks lack the precision to order events occurring within the same millisecond across different nodes.
To solve this, the generic architecture utilizes Hybrid Logical Clocks (HLC). The HLC provides a mechanism to capture the causality of events (if Event A causes Event B, the timestamp of B must be greater than A) while keeping the timestamp close to physical time to remain human-readable and useful for querying.
### **2.2 Mathematical Definition of HLC**
An HLC timestamp consists of three components: (l, c, id), where:
* $l$ is the physical component, representing the maximum wall-clock time observed by the node.
* $c$ is the logical component, a counter incremented to distinguish events that occur at the same $l$.
* $id$ is a unique node identifier (e.g., a UUID or hash of the device ID) used to break ties.
The generic Flutter package must maintain a local HLC state. Upon a local mutation (Create/Update), the package calculates the new timestamp $hlc\_{new}$ as follows:
$$l' \= \\max(l\_{old}, pt\_{now})$$
$$c' \= \\begin{cases} c\_{old} \+ 1 & \\text{if } l' \= l\_{old} \\\\ 0 & \\text{if } l' \> l\_{old} \\end{cases}$$
Where $pt\_{now}$ is the device's current physical time. This algorithm ensures that the logical clock never moves backward, even if the device's physical clock is adjusted backward by the user or the OS.8
### **2.3 Serialization and Storage**
While HLCs are conceptually tuples, they must be stored in PocketBase's standard fields. PocketBase supports text, number, and date types. Storing the HLC as a strict, lexically sortable string is the most robust approach for a generic plugin.
**Format:** YYYY-MM-DDTHH:mm:ss.sssZ-CCCC-NODEID
* YYYY...Z: The physical component ($l$), ISO-8601 formatted.
* CCCC: The logical counter ($c$), padded to 4 digits (hex or decimal).
* NODEID: The truncated node ID.
This string format allows standard lexicographical comparison (string sorting) to be equivalent to chronological ordering. The Go plugin creates a custom field, nominally named \_hlc, on all synchronized collections. Since the architecture is "generic," the plugin automatically checks for this field's existence on OnBootstrap and creates it if missing, ensuring no manual setup is required by the user.
### **2.4 Drift Management and Security**
The Go server plugin acts as the guardian of time. When a client pushes a mutation with a specific HLC, the server must validate two conditions:
1. **Monotonicity:** The incoming HLC must be greater than the HLC currently stored on the record (if updating).
2. **Bounded Drift:** The physical component $l$ of the incoming HLC cannot be significantly further in the future than the server's wall clock (e.g., MaxDrift \= 60000ms).
If a client sends a timestamp from the year 2050, the server rejects the generic sync request with a 400 Bad Request. This prevents a malicious or malfunctioning client from "poisoning" the timeline and making a record immutable to other clients.9
## **3\. Data Identification and Structural Integrity**
### **3.1 Distributed ID Generation**
In traditional web development, the database (e.g., via AUTO\_INCREMENT) assigns IDs. In an offline-first architecture, the client must generate the ID immediately upon creation to establish relationships between records (e.g., creating a Project and adding Tasks to it before syncing).
PocketBase uses 15-character random alphanumeric strings by default.11 While UUIDs (36 characters) are the industry standard for distributed ID generation, PocketBase's ecosystem is optimized for the shorter format. To maintain the requirement of working with "any PocketBase instance," the generic Flutter package should utilize NanoID (specifically a Dart implementation like nanoid) configured to match PocketBase's alphabet and length.
**Configuration:**
* **Alphabet:** 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
* **Length:** 15
The collision probability for a 15-character NanoID with this alphabet is extremely low (requiring millions of IDs per second to reach a 1% risk). However, the generic sync protocol must account for the theoretical possibility of a collision.
### **3.2 Collision Handling Protocol**
A collision occurs if Client A generates ID X offline, and Client B generates ID X and syncs it to the server before Client A comes online. When Client A attempts to push its record, the generic server plugin will return a unique constraint violation error.
The Flutter package must implement a **Provisional ID Mapping** strategy:
1. **Detection:** Catch the specific "Duplicate ID" error from the batch transaction.
2. **Remediation:** Generate a new ID Y.
3. **Refactoring:** Scan the local transaction queue and the local database for any Foreign Key references to X. Update them to point to Y.
4. **Retry:** Resubmit the batch with the new ID.
This logic is encapsulated within the generic client's SyncManager class, ensuring the application code consuming the package does not need to handle ID remapping logic.13
### **3.3 The Schema Handshake**
Since the solution is generic, the client package does not know the server's schema at compile time. Upon initialization, the Flutter package performs a "Schema Handshake":
1. Client requests /api/collections from the server.
2. Server returns the JSON description of all collections and fields.
3. Client compares this schema hash against its local cached schema.
4. **Divergence:** If the schema has changed (e.g., a developer renamed a field on the backend), the client enters a "Migration Mode."
In Migration Mode, the client must reconcile pending local mutations against the new schema. If a field description was renamed to bio, pending writes to description would fail. The generic package handles this by exposing a callback onSchemaChange(oldSchema, newSchema) where the developer can provide migration logic. If no callback is provided, the safe default is to drop mutations for fields that no longer exist to prevent the sync queue from becoming permanently stuck.15
## **4\. The Generic Synchronization Protocol**
The synchronization protocol is a bidirectional exchange of state, orchestrated by the generic Flutter package and served by the Go plugin. It operates in two phases: Push (Upstream) and Pull (Downstream).
### **4.1 Phase 1: The Push (Upstream) Strategy**
The client accumulates mutations in a persistent MutationQueue table in its local SQLite database. Each entry contains the collection name, record ID, operation type (CREATE, UPDATE, DELETE), the JSON payload, and the HLC timestamp.
When network connectivity is detected (via connectivity\_plus in Flutter), the client bundles these mutations into a **Transactional Batch**. PocketBase v0.23 introduced native support for batch requests, which is crucial for data integrity.16 Sending mutations individually would risk partial failures where a parent record creates successfully but its children fail, leaving the database in an inconsistent state.
**Batch Structure:**
JSON
{
"requests":
}
The generic Go plugin intercepts this batch request using OnBatchRequest. It wraps the execution in a database transaction (app.RunInTransaction). It iterates through each operation, verifying that the incoming \_hlc is newer than the stored record's \_hlc. If a conflict is detected (e.g., incoming HLC \< stored HLC), the plugin resolves it (usually by rejecting the stale update or merging), ensuring that the server's state remains causally consistent.17
### **4.2 Phase 2: The Pull (Downstream) Strategy**
After a successful push, the client requests updates from the server. To remain generic, the client cannot request specific endpoints like /api/collections/users. Instead, it uses a custom "Sync" endpoint exposed by the Go plugin: /api/pocketbase\_sync/pull.
**Request Parameters:**
* since: The HLC of the last successful sync (last\_synced\_hlc).
* limit: Pagination limit.
**Server Logic (Go Plugin):**
1. The plugin iterates through *all* collections registered in the app.
2. For each collection, it queries for records where updated \> last\_synced\_hlc (optimization: using updated is faster for query filtering, but the client uses \_hlc for merging).
3. It explicitly checks generic API rules (ViewRule) using app.CanAccessRecord. This step is critical; without it, the sync endpoint would become a backdoor bypassing the application's security model.
4. It constructs a response grouping changed records by collection.
**Response Structure:**
JSON
{
"todos": \[... records... \],
"users": \[... records... \],
"\_offline\_tombstones": \[... deleted record markers... \],
"new\_cursor": "2023-10-27T11:00:00Z-0000-S"
}
The client receives this payload, updates its local SQLite replica, and advances its last\_synced\_hlc cursor.
## **5\. Distributed Deletion Management: The Tombstone Pattern**
### **5.1 The Deletion Visibility Problem**
In a standard SQL database, a DELETE operation removes the row physically. For a client that is offline, this removal is invisible. When the client later asks for "changes since T," the deleted record is simply absent from the result set. The client, seeing no change, assumes its local copy is still valid, leading to "Zombie Records" that reappear or persist indefinitely on the device.
To support "Any PocketBase Instance," we cannot mandate that users change their schema to add a deleted boolean column (Soft Delete) to every table. This violates the ease-of-use principle. Instead, the Go plugin implements a **Global Tombstone Registry**.
### **5.2 The Global Tombstone Collection**
Upon initialization, the Go plugin checks for a system collection named \_offline\_tombstones. If absent, it creates it programmatically using the core.Collection model.
**Schema:**
* collection: String (Target collection name)
* record\_id: String (Target record ID)
* \_hlc: String (Timestamp of deletion)
### **5.3 Hook-Based Interception**
The plugin registers a global OnRecordAfterDeleteRequest hook. This hook fires whenever a record is deleted via the API (including from the Admin UI).
Go
app.OnRecordAfterDeleteRequest().Add(func(e \*core.RecordDeleteEvent) error {
// Avoid recursion
if e.Collection.Name \== "\_offline\_tombstones" {
return nil
}
tombstone := core.NewRecord(app.FindCollectionByNameOrId("\_offline\_tombstones"))
tombstone.Set("collection", e.Collection.Name)
tombstone.Set("record\_id", e.Record.Id)
tombstone.Set("\_hlc", NewHLC()) // Generate current HLC
// Persist tombstone in the same transaction context if possible,
// or as a separate save.
return app.Save(tombstone)
})
This ensures that every deletion leaves a trace. During the Pull phase of synchronization, the generic client explicitly requests records from \_offline\_tombstones created after last\_synced\_hlc. It iterates through these tombstones and performs corresponding DELETE operations on its local SQLite database.19
### **5.4 The Reaper: Garbage Collection**
Tombstones cannot accumulate indefinitely. The Go plugin must register a cron job (app.OnCron) to "reap" old tombstones.
* **Schedule:** Daily.
* **Policy:** Delete tombstones older than SYNC\_retention\_period (default: 30 days).
**Implication:** If a device remains offline for longer than the retention period (e.g., 31 days), it will miss the tombstone. Upon reconnection, it might re-upload the deleted record (Resurrection). To mitigate this, the client package checks the age of its last\_synced\_hlc. If it exceeds the retention period, the client declares "Bankruptcy": it wipes its local database and performs a fresh full sync from the server.21
## **6\. Conflict Resolution and Differential Synchronization**
### **6.1 Last-Write-Wins (LWW) via HLC**
For scalar data types (booleans, numbers, dates), the LWW strategy is the industry standard for generic synchronization. However, "Last" is defined by the HLC, not the wall clock.
When the generic server plugin processes a batch update:
1. It fetches the current record from the DB.
2. It compares incoming\_batch\_record.\_hlc vs db\_record.\_hlc.
3. **If Incoming \> DB:** The update is applied.
4. **If Incoming \< DB:** The update is discarded (the client is trying to overwrite newer data with older data). The server returns a success response (idempotency) but does not apply the change. The client will eventually receive the newer server state in the next Pull phase.
### **6.2 Differential Synchronization: Myers Diff**
For text and editor (HTML) fields, LWW is destructive. If User A corrects a typo in paragraph 1, and User B adds a sentence to paragraph 2, LWW will blindly overwrite one user's contribution. To solve this, the Generic Architecture implements Differential Synchronization using the Myers Diff Algorithm.
This feature requires the Flutter package to use the diff\_match\_patch library (Dart) and the Go plugin to use the corresponding Go port.23
#### **6.2.1 The Diff Protocol**
1. **Snapshotting:** The client stores a "Shadow Copy" of the text field as it was at the time of the last sync.
2. **Diff Generation:** When the client modifies the text, it computes the delta (patches) between the Shadow Copy and the Current Text using diff\_match\_patch.patch\_make().
3. **Transmission:** The client sends the patches (serialized as string) instead of the full text, along with the \_hlc of the Shadow Copy (the Base Version).
4. **Server Application:**
* The Go plugin detects that a patch payload is being sent.
* It retrieves the current text from the database.
* It applies the patch using diff\_match\_patch.patch\_apply().
* **Fuzzy Patching:** The Myers algorithm allows "fuzzy" application. Even if the server text has changed slightly (someone else edited a different paragraph), the algorithm attempts to locate the context for the patch and apply it non-destructively.25
5. **Rejection:** If the text has diverged so significantly that the patch cannot be applied (fuzziness threshold exceeded), the server rejects the specific field update, forcing the client to pull the latest version and manually resolve.
This strategy allows high-concurrency collaboration on text fields without the complexity of Operational Transformation (OT) or CRDTs, which would require specialized data structures incompatible with PocketBase's standard SQLite columns.
## **7\. Server-Side Implementation Detail (Go Plugin)**
### **7.1 Plugin Initialization and Routes**
The generic plugin is designed to be imported into main.go. It encapsulates all logic to avoid polluting the main application scope.
Go
package offline\_sync
import (
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/cron"
)
type SyncPlugin struct {
app core.App
}
func New(app core.App) \*SyncPlugin {
return \&SyncPlugin{app: app}
}
func (p \*SyncPlugin) Register() {
p.registerHooks()
p.registerRoutes()
p.registerCron()
}
func (p \*SyncPlugin) registerRoutes() {
// Generic Pull Endpoint
p.app.OnServe().BindFunc(func(e \*core.ServeEvent) error {
e.Router.GET("/api/sync/pull", p.handlePull)
return e.Next()
})
}
### **7.2 Dynamic Schema Handling**
The handlePull function demonstrates the "generic" capability. It does not use hardcoded struct definitions. Instead, it uses PocketBase's dynamic models.Record and dao methods.
Go
func (p \*SyncPlugin) handlePull(c \*core.RequestEvent) error {
since := c.Request.URL.Query().Get("since")
// 1\. Get all collections
collections, \_ := p.app.Dao().FindCollections()
response := make(map\[string\]map\[string\]any)
for \_, col := range collections {
// 2\. Query dynamically
records, \_ := p.app.Dao().FindRecordsByExpr(col.Name,
dbx.NewExp("\_hlc \> {:since}", dbx.Params{"since": since}))
// 3\. Filter by ACL (Security)
visibleRecords :=map\[string\]any{}
for \_, rec := range records {
if p.app.CanAccessRecord(rec, c.RequestInfo(), rule.ViewRule) {
visibleRecords \= append(visibleRecords, rec.PublicExport())
}
}
response\[col.Name\] \= visibleRecords
}
return c.JSON(200, response)
}
*Note: This code snippet simplifies error handling for brevity. Real implementation must handle errors robustly.*
This implementation ensures that if a user adds a Projects collection to their PocketBase instance, the sync engine immediately supports it without code changes.
### **7.3 Batch Transaction Processing**
The most critical server-side component is the transaction wrapper. When OnBatchRequest is triggered (or a custom batch endpoint is used), the plugin must ensure atomicity.
Go
func (p \*SyncPlugin) handleBatchPush(c \*core.RequestEvent) error {
// Parse batch payload...
return p.app.Dao().RunInTransaction(func(txDao \*dao.Dao) error {
for \_, op := range operations {
// Validate HLC ordering
existing, err := txDao.FindRecordById(op.Collection, op.Id)
if err \== nil {
if op.HLC \< existing.GetString("\_hlc") {
continue // Ignore stale update (LWW)
}
}
// Apply diffs or updates
record := models.NewRecord(op.Collection)
record.Load(op.Data)
if err := txDao.SaveRecord(record); err\!= nil {
return err // Rollback entire batch
}
}
return nil
})
}
This ensures that the client's view of the data remains consistent. Either all offline changes are accepted, or none are (triggering a retry), preventing partial sync states.27
## **8\. Client-Side Implementation Detail (Flutter Package)**
### **8.1 The Generic Repository**
The Flutter package (pocketbase\_offline) exposes a PocketBaseOffline client that wraps the standard SDK.
Dart
class PocketBaseOffline {
final PocketBase client;
final Database localDb; // Drift database
Future\<void\> init() async {
// 1\. Fetch remote schema
final collections \= await client.collections.getFullList();
// 2\. Migrate local SQLite to match remote schema dynamically
await \_schemaManager.migrate(localDb, collections);
}
// Generic Save
Future\<RecordModel\> save(String collectionName, Map\<String, dynamic\> body) async {
// 1\. Assign HLC
body\['\_hlc'\] \= \_hlcManager.now().toString();
// 2\. Save to Local DB (Optimistic)
await localDb.table(collectionName).insertOnConflictUpdate(body);
// 3\. Queue for Sync
await \_syncQueue.add(Mutation(collection: collectionName, body: body));
// 4\. Trigger Background Sync
\_syncService.trigger();
return RecordModel(data: body);
}
}
### **8.2 Background Sync with Isolates**
Flutter runs on a single thread (Main Isolate). Heavy JSON parsing and diffing during sync can cause UI jank. The generic package must perform the synchronization logic in a separate Isolate or utilize compute.
The package should leverage workmanager for Android/iOS background execution, allowing sync to happen even if the app is closed. This background worker initializes its own generic PocketBase client, connects to the local SQLite (which must be concurrency-safe, e.g., using WAL mode in SQLite via drift), and performs the Push/Pull cycle.1
### **8.3 Handling Large Binaries**
Synchronization of large files (images/videos) via the batch JSON payload is inefficient and prone to timeouts. The generic architecture uses a **Reference-Based Sync**.
1. **Upload:** Binary files are uploaded immediately to the file storage endpoint (or queued in a separate UploadQueue if offline).
2. **Reference:** The file upload returns a filename (string).
3. **Sync:** The JSON record containing the filename string is synced via the standard protocol.
4. **Retrieval:** The generic client intercepts record.getList calls. For fields of type file, it checks a local cache (flutter\_cache\_manager). If missing, it downloads the file from the server using the filename reference.
## **9\. Insights and Future Implications**
### **9.1 Second-Order Insight: The Soft Delete Ripple Effect**
Implementing a global tombstone collection (\_offline\_tombstones) introduces complexity regarding **Cascading Deletes**. In standard PocketBase, deleting a User may cascade to delete their Posts.
* *Observation:* If the Go plugin captures the User deletion via hook, the Posts might be deleted by the database engine's internal foreign key triggers. These internal SQL deletes often **do not fire** application-level hooks.
* *Implication:* The generic plugin would generate a tombstone for the User, but *not* for the Posts. The offline client would delete the User but keep the orphaned Posts.
* *Solution:* The generic Go plugin must enforce "Application-Level Cascades." It should disable DB-level ON DELETE CASCADE and instead recursively find and delete child records using the API/DAO. This ensures OnRecordAfterDelete fires for every single deleted record, generating the necessary tombstones for the client to maintain referential integrity.
### **9.2 Third-Order Insight: Schema Evolution and Client Versioning**
The "Generic" requirement creates a vulnerability regarding schema changes. If a developer renames a field status to state on the server, offline clients will still have mutations targeting status in their queue.
* *Observation:* When the client pushes, the server will reject the unknown field status (or ignore it, leading to data loss).
* *Implication:* The protocol requires versioning. The server plugin should hash the current schema configuration.
* *Solution:* The sync handshake must exchange this SchemaHash. If they mismatch, the client package must trigger a MigrationStrategy. Since the package is generic, it cannot know how to migrate specific data. It should expose a callback onMigrationNeeded(localData) to the Flutter developer, allowing them to map status \-\> state before the generic sync queue processes the pending mutations.
### **9.3 Performance at Scale**
While Go and SQLite are performant, the OnServe hook interception adds overhead. The system's bottleneck is the SQLite write lock.
* *Recommendation:* The generic plugin should aggressively use WAL mode (PRAGMA journal\_mode=WAL).27
* *Optimization:* The "Pull" endpoint logic involves iterating all collections. As the number of collections grows, this becomes slow. The plugin should maintain a \_sync\_metadata table that indexes the \_hlc of all records across all collections, effectively creating a unified oplog. This trades write performance (updating the index on every save) for significantly faster read performance during sync (querying one table instead of N tables).
## **Conclusion**
This report defines a comprehensive, generic architecture for enabling offline-first capabilities in PocketBase. By decoupling the synchronization logic into a Go server plugin (handling HLCs, Tombstones, and Batching) and a Flutter client package (handling Local Replica, Queues, and Optimistic UI), developers can retrofit any PocketBase instance with robust offline support. The architecture prioritizes data integrity through the use of Hybrid Logical Clocks and Myers Diff, ensuring that even in the most hostile network environments, the system converges to a consistent, correct state without manual intervention in the database schema. This transforms PocketBase from a realtime-only backend into a versatile engine capable of powering mission-critical field applications.
#### **Works cited**
1. Offline-first support \- Flutter documentation, accessed December 22, 2025, [https://docs.flutter.dev/app-architecture/design-patterns/offline-first](https://docs.flutter.dev/app-architecture/design-patterns/offline-first)
2. Implementing Efficient Data Synchronization for Offline-First Mobile Applications, accessed December 22, 2025, [https://dev.to/dowerdev/implementing-efficient-data-synchronization-for-offline-first-mobile-applications-525c](https://dev.to/dowerdev/implementing-efficient-data-synchronization-for-offline-first-mobile-applications-525c)
3. pocketbase\_drift \- Dart API docs \- Pub.dev, accessed December 22, 2025, [https://pub.dev/documentation/pocketbase\_drift/latest/](https://pub.dev/documentation/pocketbase_drift/latest/)
4. pocketbase library \- Dart API \- Pub.dev, accessed December 22, 2025, [https://pub.dev/documentation/pocketbase/latest/pocketbase](https://pub.dev/documentation/pocketbase/latest/pocketbase)
5. PocketBase Dart SDK \- GitHub, accessed December 22, 2025, [https://github.com/pocketbase/dart-sdk](https://github.com/pocketbase/dart-sdk)
6. Extend with Go \- Routing \- Docs \- PocketBase, accessed December 22, 2025, [https://pocketbase.io/docs/go-routing/](https://pocketbase.io/docs/go-routing/)
7. Extend with Go \- Record operations \- Docs \- PocketBase, accessed December 22, 2025, [https://pocketbase.io/docs/go-records/](https://pocketbase.io/docs/go-records/)
8. Hybrid logical clock \- Andy Matuschak's notes, accessed December 22, 2025, [https://notes.andymatuschak.org/Hybrid\_logical\_clock](https://notes.andymatuschak.org/Hybrid_logical_clock)
9. Hybrid Logical Clock implementation in TypeScript \- typeonce.dev, accessed December 22, 2025, [https://www.typeonce.dev/snippet/hybrid-logical-clock-implementation-typescript](https://www.typeonce.dev/snippet/hybrid-logical-clock-implementation-typescript)
10. @dldc/hybrid-logical-clock \- JSR, accessed December 22, 2025, [https://jsr.io/@dldc/hybrid-logical-clock](https://jsr.io/@dldc/hybrid-logical-clock)
11. Will there be an option for a custom ID? \#2518 \- GitHub, accessed December 22, 2025, [https://github.com/pocketbase/pocketbase/discussions/2518](https://github.com/pocketbase/pocketbase/discussions/2518)
12. Feature request: Customizable id field · Issue \#2727 \- GitHub, accessed December 22, 2025, [https://github.com/pocketbase/pocketbase/issues/2727](https://github.com/pocketbase/pocketbase/issues/2727)
13. Introduction \- How to use PocketBase \- Docs, accessed December 22, 2025, [https://pocketbase.io/docs/how-to-use/](https://pocketbase.io/docs/how-to-use/)
14. Custom own Record ID from client side \#3173 \- GitHub, accessed December 22, 2025, [https://github.com/pocketbase/pocketbase/discussions/3173](https://github.com/pocketbase/pocketbase/discussions/3173)
15. Id generation in migrations · pocketbase pocketbase · Discussion \#3912 \- GitHub, accessed December 22, 2025, [https://github.com/pocketbase/pocketbase/discussions/3912](https://github.com/pocketbase/pocketbase/discussions/3912)
16. Creating records with relationships in a single PocketBase operation? \- Reddit, accessed December 22, 2025, [https://www.reddit.com/r/pocketbase/comments/1gywun9/creating\_records\_with\_relationships\_in\_a\_single/](https://www.reddit.com/r/pocketbase/comments/1gywun9/creating_records_with_relationships_in_a_single/)
17. Atomically insert records inside transactions (extend with go)? \#7322 \- GitHub, accessed December 22, 2025, [https://github.com/pocketbase/pocketbase/discussions/7322](https://github.com/pocketbase/pocketbase/discussions/7322)
18. Creating a related row inside a transaction in a JS hook \#6292 \- GitHub, accessed December 22, 2025, [https://github.com/pocketbase/pocketbase/discussions/6292](https://github.com/pocketbase/pocketbase/discussions/6292)
19. Extend with Go \- Event hooks \- Docs \- PocketBase, accessed December 22, 2025, [https://pocketbase.io/docs/go-event-hooks/](https://pocketbase.io/docs/go-event-hooks/)
20. An alternative approach to soft deletion using hooks \#2694 \- GitHub, accessed December 22, 2025, [https://github.com/pocketbase/pocketbase/discussions/2694](https://github.com/pocketbase/pocketbase/discussions/2694)
21. Removing Documents \- Ditto, accessed December 22, 2025, [https://docs.ditto.live/sdk/v5/crud/delete](https://docs.ditto.live/sdk/v5/crud/delete)
22. Kafka not deleting key with tombstone \- Stack Overflow, accessed December 22, 2025, [https://stackoverflow.com/questions/46632713/kafka-not-deleting-key-with-tombstone](https://stackoverflow.com/questions/46632713/kafka-not-deleting-key-with-tombstone)
23. Diff Match Patch \- Dart API docs \- Pub.dev, accessed December 22, 2025, [https://pub.dev/documentation/diff\_match\_patch/latest/](https://pub.dev/documentation/diff_match_patch/latest/)
24. sergi/go-diff: Diff, match and patch text in Go \- GitHub, accessed December 22, 2025, [https://github.com/sergi/go-diff](https://github.com/sergi/go-diff)
25. Myers diff — MoonBit v0.6.33 documentation, accessed December 22, 2025, [https://docs.moonbitlang.com/en/latest/example/myers-diff/myers-diff.html](https://docs.moonbitlang.com/en/latest/example/myers-diff/myers-diff.html)
26. Myers diff in linear space: theory \- The If Works \- James Coglan, accessed December 22, 2025, [https://blog.jcoglan.com/2017/03/22/myers-diff-in-linear-space-theory/](https://blog.jcoglan.com/2017/03/22/myers-diff-in-linear-space-theory/)
27. Extend with Go \- Overview \- Docs \- PocketBase, accessed December 22, 2025, [https://pocketbase.io/docs/go-overview/](https://pocketbase.io/docs/go-overview/)
28. Offline-First Mobile App Architecture: Syncing, Caching, and Conflict Resolution, accessed December 22, 2025, [https://dev.to/odunayo\_dada/offline-first-mobile-app-architecture-syncing-caching-and-conflict-resolution-518n](https://dev.to/odunayo_dada/offline-first-mobile-app-architecture-syncing-caching-and-conflict-resolution-518n)
@@ -0,0 +1,383 @@
# **The Simulation of Reality: Architecting Robust Test Harnesses in Dart**
## **1\. Introduction: The Epistemology of Software Simulation**
The verification of modern software systems, particularly those operating in distributed or mobile environments, faces an existential crisis. Traditional testing methodologies—unit testing, which isolates components in a sterile vacuum, and integration testing, which typically verifies the "happy path" of component interaction—are increasingly insufficient. They operate on a map of the system that assumes a level of stability and determinism that the territory of the real world simply does not possess. "Reality," in the context of a deployed application, is a hostile, high-entropy environment defined by stochastic failures: network packets are dropped, latencies jitter unpredictably, clocks drift between devices, and data structures diverge in unexpected ways due to concurrent modifications.
To bridge the gap between the sanitized lab environment of standard CI/CD pipelines and the chaotic reality of production, engineering teams must move beyond simple verification and towards **Simulation Testing**. This paradigm shifts the goal from checking if a function returns value $X$ given input $Y$, to proving system-level properties—such as "data is never lost," "eventual consistency is always reached," or "the application recovers gracefully from a subway tunnel signal loss"—under adversarial conditions that rigorously mimic the chaotic nature of the physical world.
This report serves as an exhaustive architectural blueprint for implementing such a "Harness for Reality," specifically tailored for the Dart and Flutter ecosystem. It synthesizes research into network fault injection, property-based testing, hybrid logical clocks, and deterministic seeding to propose a cohesive strategy for building a robust simulation environment. While the user's query requests a solution written in Dart "if possible," the research indicates that a purely Dart-based solution for all layers (especially network transport) is insufficient for high-fidelity simulation. Therefore, this report advocates for a hybrid architecture: a Dart-based control plane and application layer that orchestrates battle-tested Open Source Software (OSS) infrastructure tools like Toxiproxy and PocketBase to provide the necessary realism.
The architecture of reality can be deconstructed into four fundamental dimensions, each requiring a specific simulation strategy:
1. **Transport (Network):** The medium is unreliable. It subjects messages to delay, corruption, reordering, and loss. Simulation here requires transparent TCP proxies rather than simple client wrappers.
2. **Time (Causality):** Physical time is a shared illusion. In distributed systems, clocks drift, and event ordering is relative. "Happens-before" relationships must be preserved using logical clocks.
3. **Data (Entropy):** Input is rarely clean. Users and hostile actors introduce edge cases (empty strings, Unicode control characters, massive integers) that shatter fragile assumptions. Property-based testing provides the generator for this entropy.
4. **Service (Availability):** Dependencies are transient. Backends crash, restart, and return errors that require sophisticated retry policies and state recovery mechanisms.
The following sections rigorously explore the implementation of each layer, culminating in a unified design for a Dart-based Simulation Harness.
## ---
**2\. The Transport Layer: Deterministic Network Chaos**
The most immediate and visceral manifestation of "reality" for a mobile or web application is the network interface. It is the boundary where the application loses agency over its data, surrendering it to the vagaries of routing tables, cellular congestion, and physical signal decay. Implementing a robust simulation harness requires the ability to intercept, manipulate, and observe network traffic with extreme granularity.
### **2.1 Theoretical Underpinnings of Network Faults**
Before selecting tools, one must understand the phenomena being simulated. A naive approach, such as simply delaying a Future in Dart to simulate latency, fails to capture the complexity of the TCP/IP stack.
**The Failure Taxonomy:**
* **Latency and Jitter:** Network delay is rarely constant. It follows a distribution. A constant 500ms delay is easy to handle; a delay that varies normally between 100ms and 5000ms introduces race conditions where response $B$ (requested later) arrives before response $A$ (requested earlier). This tests the application's ability to discard stale data.1
* **Packet Loss and Fragmentation:** Mobile networks (2G/EDGE) often fragment data into small bursts. A large JSON payload does not arrive instantly; it "trickles" in. If the application's read buffer or timeout logic is not tuned for this, it may time out a connection that is technically active but slow.3
* **Connection Reset (RST):** There is a semantic difference between a timeout (silence) and a reset (active rejection). A firewall or a crashing load balancer sends a TCP RST packet. The application must distinguish this immediate failure from a timeout to trigger immediate retries rather than waiting for a deadline.1
* **Bandwidth Throttling:** Restricting bandwidth is distinct from adding latency. It affects the *rate* of data transfer, prolonging the duration a socket stays open and consuming system resources (file descriptors, memory buffers).1
### **2.2 Toxiproxy: The Architecture of Interception**
While client-side wrappers like slow\_net\_simulator 3 exist in Dart, they operate too high in the stack. They simulate the *symptoms* (waiting) but not the *mechanics* (TCP windowing, socket closure). For a "robust and realistic" harness, the fault injection must occur at the infrastructure level.
The research overwhelmingly points to **Toxiproxy**, developed by Shopify, as the industry standard for this task.5 Unlike tc (Traffic Control) 1, which operates at the Linux kernel level and requires root privileges (making it difficult to set up in shared CI environments), Toxiproxy runs in user space as a transparent TCP proxy. This satisfies the user's requirement for a tool that is "easy to setup" while remaining rigorous.
#### **2.2.1 The Proxy Model**
Toxiproxy acts as a man-in-the-middle. The test harness configures the Dart application to connect to Toxiproxy's listening port (e.g., localhost:8474) instead of the actual backend. Toxiproxy then forwards traffic to the upstream service. This architecture allows the simulation to act on the raw byte stream.2
| Feature | Client-Side Wrapper (e.g., slow\_net\_simulator) | Infrastructure Proxy (Toxiproxy) |
| :---- | :---- | :---- |
| **Layer** | Application (Dart http client) | Transport (TCP/IP) |
| **Latency** | Artificial delay ( Future.delayed) | Network buffering & serialization delay |
| **Bandwidth** | Not truly simulated | Token bucket rate limiting |
| **Hard Failures** | Throws generic Exception | Sends TCP RST / Fin packets |
| **Realism** | Low (Logic simulation) | High (Physics simulation) |
| **Setup** | Trivial (Dart package) | Moderate (Docker container) |
#### **2.2.2 The Toxic Arsenal**
Toxiproxy modules, known as "toxics," inject specific faults. A Dart-based harness would dynamically inject these toxics via Toxiproxy's HTTP API.5
1. **Latency & Jitter:** The latency toxic adds a time delay. Crucially, the jitter attribute adds randomness. A configuration of latency=1000ms, jitter=500ms results in delays uniformly distributed between 500ms and 1500ms. This is essential for exposing "Last Write Wins" bugs in synchronization logic.8
2. **The Slicer (Edge Network Simulation):** The slicer toxic splits data into small chunks and adds a delay between them. This accurately models the "trickle" effect of high-packet-loss networks or extremely constrained bandwidth (like GPRS). It validates that the application's HTTP client does not prematurely close the socket while data is still flowing.4
3. **Reset Peer:** This toxic simulates the sudden severance of a connection, effectively sending an ECONNRESET to the client. This is vital for testing the application's "Retry immediately" logic versus "Backoff and retry" logic.4
4. **Limit Data:** This toxic closes the connection after a specific number of bytes have been transferred. It is the perfect tool for testing resumable downloads or pagination limits, ensuring the app handles partial responses gracefully.8
### **2.3 Constructing the Dart Control Plane**
To control this infrastructure, the Dart test harness must act as the orchestrator. Since Toxiproxy exposes a REST API, we can implement a ToxiproxyClient in Dart to create proxies and inject toxics during test setUp and tearDown.
#### **2.3.1 Client Implementation Strategy**
The client should mirror the API structure: managing Proxy resources and attaching Toxic resources to them.7
* **Proxy Management:** The client needs methods to create, delete, enable, and disable proxies. Disabling a proxy simulates a complete network outage (e.g., entering an elevator).9
* **Toxic Injection:** The client must serialize configuration objects (e.g., LatencyToxic, BandwidthToxic) into the JSON format expected by Toxiproxy.6
Conceptual Dart Implementation:
The harness communicates with the Toxiproxy daemon (usually running in Docker).
Dart
/// A Dart controller for the Toxiproxy chaos engine.
class ToxiproxyController {
final String host;
final int port;
ToxiproxyController({this.host \= 'localhost', this.port \= 8474});
String get \_apiBase \=\> 'http://$host:$port';
/// Maps a local port to an upstream service.
Future\<void\> createProxy(String name, String listen, String upstream) async {
final response \= await http.post(
Uri.parse('$\_apiBase/proxies'),
body: jsonEncode({
'name': name,
'listen': listen,
'upstream': upstream,
'enabled': true,
}),
);
if (response.statusCode\!= 201) throw Exception('Failed to create proxy');
}
/// Injects a toxic into the active stream.
Future\<void\> addToxic(String proxyName, Toxic toxic) async {
// Serialization logic for toxic attributes (jitter, rate, etc.)
await http.post(
Uri.parse('$\_apiBase/proxies/$proxyName/toxics'),
body: jsonEncode(toxic.toJson()),
);
}
/// Simulates a network cut.
Future\<void\> cutConnection(String proxyName) async {
// Disabling the proxy stops all traffic immediately
await http.post(
Uri.parse('$\_apiBase/proxies/$proxyName'),
body: jsonEncode({'enabled': false}),
);
}
}
This controller allows the test code to read like a narrative: await toxiproxy.cutConnection('api');.
### **2.4 Mobile Connectivity States and OS Integration**
Simulating the network pipe is half the battle; the other half is simulating the operating system's awareness of that pipe. In Flutter, the connectivity\_plus package 10 is the standard mechanism for checking if the device is on WiFi, Cellular, or Offline.
#### **2.4.1 The Mocking Paradox**
A simulation paradox arises: If Toxiproxy cuts the connection (simulates a broken cable), the OS (and thus connectivity\_plus) might still report "Connected" because the WiFi link to the router is physically intact—only the internet reachability is gone. Conversely, toggling "Airplane Mode" changes the OS state.
To build a realistic harness, one must simulate *both* scenarios:
1. **False Positive:** OS reports "Connected," but Toxiproxy blocks traffic. This tests timeout handling.
2. **True Negative:** OS reports "Offline." This tests the app's ability to pause queues and conserve battery.
#### **2.4.2 Implementation: The Wrapper/Adapter Pattern**
Since connectivity\_plus interacts with platform channels, it is difficult to mock directly in integration tests without a wrapper.11 The harness should employ an Adapter pattern.
* **Production:** RealConnectivity wraps the connectivity\_plus stream.
* **Simulation:** MockConnectivity exposes a StreamController. The harness pushes ConnectivityResult.none to this controller to simulate Airplane Mode.
Critical Insight \- Race Conditions:
A robust harness must verify the race condition between the OS reporting "Offline" and the HTTP client throwing "SocketException." In reality, either can happen first. The application logic must be resilient to this ambiguity, perhaps by prioritizing the HTTP error as the "source of truth" for reachability while using the OS state for power management.10
## ---
**3\. The Temporal Layer: Causality and Distributed Time**
"Reality" is inherently distributed. Even a simple client-server application involves two timelines: the client's and the server's. These timelines run at different speeds (clock drift) and are synchronized only by message passing. A robust simulation cannot rely on a single, global DateTime.now().
### **3.1 The Illusion of Simultaneity**
In standard testing, asserting that Event A happened before Event B often relies on checking their timestamps. However, in a distributed system, physical timestamps are unreliable.
* **NTP Drift:** Devices may be seconds or minutes apart.
* **Resolution Limits:** Two events occurring within the same millisecond on different nodes appear simultaneous.
Using DateTime.now() in a simulation harness introduces non-determinism (flakiness). If a test runs on a fast machine, latencies are low, and timestamps align one way. On a slow CI runner, they align differently, causing assertions to fail.
### **3.2 Hybrid Logical Clocks (HLC)**
To rigorously test distributed behaviors (like sync, offline-first conflict resolution), the harness should utilize **Hybrid Logical Clocks** (HLC).13 HLCs combine the intuitive nature of physical time with the mathematical rigor of logical clocks (Lamport clocks).
#### **3.2.1 Mechanism of Action**
An HLC timestamp is composed of three parts, typically packed into a 64-bit structure:
1. **Physical Component (PT):** The wall-clock time (e.g., milliseconds since epoch).
2. **Logical Component (L):** A counter that increments when events happen within the same physical millisecond.
3. **Causal Update Rule:** When a node receives a message with timestamp $T\_{remote}$, it updates its local clock $T\_{local}$ such that $T\_{local} \= \\max(PT\_{now}, T\_{local}, T\_{remote}) \+ 1$ (logical increment).
This guarantees that if Event A caused Event B, the timestamp of B is strictly greater than A, regardless of the physical clock skew between the machines.15
#### **3.2.2 Dart Implementation (hlc\_dart)**
The hlc\_dart package 16 implements this standard. The simulation harness should mandate that all "Simulated Nodes" (the App and the Mock Backend) use HLC.now() instead of DateTime.now().
**Simulation Scenario:**
1. **Skew Injection:** The harness sets the Mock Backend's clock to T \- 5 minutes.
2. **Interaction:** The Client (correct time) sends data to the Backend.
3. **Causal Preservation:** The Backend receives the data. Despite its physical clock being behind, the HLC algorithm forces the Backend's logical clock to jump forward to match the Client's time, preserving the causal chain.
4. **Verification:** The harness asserts that the Backend stored the data with the corrected HLC, not its local (wrong) physical time. This ensures the sync protocol is resilient to client-side or server-side clock errors.15
### **3.3 Virtualizing Time in the Dart Event Loop**
To make tests deterministic and fast, the harness must detach "simulation time" from "wall-clock time." Waiting for a 10-second timeout in a real-time test takes 10 seconds. In a virtualized simulation, it takes microseconds.
The Zone Specification:
Dart's Zone mechanism allows intercepting microtask scheduling. While complex, a robust harness can use fake\_async or custom Zone specifications to override Timer and Future.delayed.
* **Virtual Clock:** An integer counter representing ticks.
* **Scheduler:** A priority queue of pending tasks ordered by their scheduled execution time.
* **Execution:** harness.tick(Duration(seconds: 10)) simply advances the counter and executes all tasks scheduled in that window immediately.
This allows the harness to simulate "24 hours of flaky network usage" in under a second, with guaranteed deterministic ordering of microtasks.18
## ---
**4\. The Data Layer: Entropy and Property-Based Testing**
The network and clocks provide the environment, but the *data* flowing through them is the primary vector for entropy. Engineers are biased; they write tests with "clean" inputs (e.g., "test\_user", "password123"). Reality supplies inputs like empty strings, 10MB distinct JSON blobs, emojis, and SQL injection vectors.
### **4.1 From Examples to Invariants**
Example-Based Testing checks specific points: $f(2) \= 4$.
Property-Based Testing (PBT) checks the surface: $\\forall x \\in \\mathbb{Z}, f(x) \= 2x$.
For a reality harness, the properties (invariants) are high-level system truths:
* **Conservation of Data:** "No matter how many times the network disconnects, the total number of items in the client DB plus the pending sync queue must equal the number of items created."
* **Convergence:** "After the network stabilizes and sync completes, Client State must exactly equal Server State."
* **Idempotency:** "Sending the same request 10 times results in the same server state as sending it once."
### **4.2 Generative Strategies in Dart**
To implement PBT, the harness requires a generator engine. Libraries like dart-check 20 and propcheck 21 provide the primitives.
#### **4.2.1 Generators (Arbitraries)**
The harness must define domain-specific generators.
* **Primitives:** Gen.string (includes Unicode, whitespace), Gen.int (includes negative, overflow).
* **Models:** Gen.user combines primitive generators to create User objects with complex, messy states.
* **Actions:** Gen.action produces a sequence of operations: \[Login, CreatePost, GoOffline, EditPost, GoOnline\].
**Code Concept:**
Dart
// Generating a sequence of interactions
final actionSequence \= Gen.list(Gen.oneOf());
#### **4.2.2 Shrinking: The Debugging Superpower**
When a random sequence of 50 actions causes a crash, debugging is impossible without **Shrinking**. The PBT library automatically reduces the failing input to the minimal set required to reproduce the bug.22
* *Original:* \[Login,... 40 actions..., GoOffline, CreatePost, Crash\]
* *Shrunk:* \[GoOffline, CreatePost\] \-\> Crash.
This feature automatically isolates the root cause (e.g., "Creating a post while offline throws a null pointer") from the noise.
### **4.3 State Convergence and Differential Analysis**
The ultimate test of a distributed system is state convergence. After a chaos scenario, how do we verify the system is consistent?
Myers Diff Algorithm:
The harness should employ the Myers Diff Algorithm 23 (implemented in diff\_match\_patch or diffutil\_dart 25\) to perform a deep structural comparison between the Client's local database (SQLite) and the Simulator's backend state.
**Verification Process:**
1. **Freeze:** Stop all simulation activity.
2. **Extract:** Dump the table items from Client SQLite and Server Mock.
3. **Normalize:** Sort both lists by ID and serialize to JSON.
4. **Diff:** Compute the delta.
5. **Assert:** The diff must be empty.
If the diff is \[Insert: Item \#5\], the harness knows exactly that Item \#5 failed to sync. This is far more diagnostic than a generic assert(count \== 5\) failure.
## ---
**5\. The Service Layer: Backend Fidelity and Fault Injection**
Simulating the client is insufficient; the client reacts to the server. A robust harness requires a malleable backend that can simulate logic errors (HTTP 500, business rule violations) and stateful interactions.
### **5.1 The Case for Malleable Backends (PocketBase)**
Using a full deployed backend (e.g., AWS, Firebase) for simulation testing is slow and expensive. Mocking the HTTP client with static JSON is too rigid. The optimal middle ground is **PocketBase**.26
* **Portability:** It is a single Go binary with an embedded SQLite database. The harness can spawn a fresh process for each test suite, ensuring total isolation.28
* **Speed:** It starts in milliseconds, making it suitable for integration test loops.
* **Programmability:** It supports hooks, allowing the harness to inject logic faults.
### **5.2 Server-Side Logic Injection**
PocketBase allows extending behavior via JavaScript or Go hooks placed in a pb\_hooks directory.29 The Dart harness can dynamically write these files to configure the backend's behavior for a specific test.
**Fault Scenarios via Hooks:**
1. **The "Business Logic" Failure:**
* *Scenario:* Simulate a server-side validation error that only happens sometimes.
* *Implementation:* A hook on onRecordCreate that throws a 400 error if the record content contains the word "fail". This tests the client's error parsing and UI feedback.
2. **The "Ghost" Write:**
* *Scenario:* The server accepts the request (200 OK) but fails to commit the transaction.
* *Implementation:* A hook that returns null or interrupts the transaction chain after sending the response. This tests the client's read-after-write verification logic.
3. **Throttling:**
* *Implementation:* A hook that sleeps for 5 seconds before responding. Combined with Toxiproxy, this tests the intersection of *server processing time* and *network latency*.31
### **5.3 Database Integrity and Connection Management**
On the client side (Flutter), the "Harness for Reality" must also verify the robustness of the local persistence layer. SQLite corruption is a real risk in mobile apps that are killed aggressively by the OS.
Connection Lifecycle Testing:
The harness should verify that the application correctly handles the SQLite lifecycle.
* **Closing Connections:** While some argue for keeping connections open 32, robust apps must close them during backgrounding to prevent locking. The harness can simulate a "Force Stop" by closing the database handle abruptly and then attempting to reopen it, running an integrity check (PRAGMA integrity\_check) to ensure no corruption occurred.33
* **Busy Handlers:** The harness can spawn a secondary isolate that holds a write lock on the database file, forcing the main app to encounter SQLITE\_BUSY. This verifies that the app implements correct retry/backoff logic at the database driver level.35
## ---
**6\. Synthesis: Architecting the Harness**
Combining these four layers results in a unified, high-fidelity Simulation Harness.
### **6.1 The "Hypervisor" Pattern**
The Harness acts as a hypervisor. It orchestrates the environment *around* the application.
**Architecture Diagram:**
1. **Orchestrator (Dart Test Runner):**
* Initializes the **Deterministic Seed** (e.g., 12345).18
* Spawns **PocketBase** (Service Layer) on a random port (e.g., 9090).
* Spawns **Toxiproxy** (Transport Layer) via Docker, mapping localhost:8080 \-\> localhost:9090.
* Injects **Toxics** (Latency, Jitter) into the proxy via the Dart ToxiproxyController.
2. **The Application (Flutter/Dart Code):**
* Configured with a **Virtual Clock** (Time Layer) implementing HLC.
* Configured with a **Mock Connectivity** adapter.
* Points its API client to localhost:8080 (the Proxy).
3. **The Fuzz Engine (PBT):**
* Generates a sequence of 100 Actions: \`\`.
* Feeds these actions to the Application via the Flutter Driver or integration test binding.
4. **The Verifier:**
* After the sequence, it pulls the Application State (Local SQLite).
* Pulls the PocketBase State (Remote DB).
* Asserts convergence using **Myers Diff**.
### **6.2 Implementation Roadmap**
To implement this "easy to setup" yet "robust" harness, follow this roadmap:
1. **Infrastructure:** Create a docker-compose.yml defining Toxiproxy.
2. **Dart Control Lib:** Write the ToxiproxyController and PocketBaseController classes in Dart to manage the external processes.
3. **Time Lib:** Import hlc\_dart and refactor the app to use a Clock service.
4. **Generator:** Use dart-check to define the input generators.
5. **Test Runner:** Write a parameterized test that accepts a seed, spins up the infrastructure, runs the fuzz loop, and asserts state convergence.
## ---
**7\. Conclusion**
Building a harness that mirrors reality is not a task of simple mocking; it is an exercise in engineering a synthetic universe. By layering **Toxiproxy** for transport fidelity, **Hybrid Logical Clocks** for temporal consistency, **Property-Based Testing** for data coverage, and **PocketBase** for service emulation, Dart engineers can construct a testing rig that exposes bugs long before they manifest in the hands of users.
This architecture satisfies the requirement for "robustness" by simulating the physics of failure (TCP resets, clock drift) rather than just the symptoms. It satisfies the requirement for "realism" by using actual network proxies and databases rather than in-memory mocks. Finally, it satisfies the "easy to setup" constraint by leveraging containerized, single-binary tools that can be orchestrated entirely from within the Dart test runner. The result is a testing environment where chaos is not an accident, but a controlled, observable, and debuggable variable.
#### **Works cited**
1. How to Simulate Network Failures in Linux | by Alexander Zakharenko | Medium, accessed December 22, 2025, [https://medium.com/@zakharenko/how-to-simulate-network-failures-in-linux-b71ab585e86f](https://medium.com/@zakharenko/how-to-simulate-network-failures-in-linux-b71ab585e86f)
2. Chaos in the network — using ToxiProxy for network chaos engineering | by Safeer CM | The Cloud Bulletin | Medium, accessed December 22, 2025, [https://medium.com/cloudbulletin/chaos-in-the-network-using-toxiproxy-for-network-chaos-engineering-13fb0ae2deea](https://medium.com/cloudbulletin/chaos-in-the-network-using-toxiproxy-for-network-chaos-engineering-13fb0ae2deea)
3. slow\_net\_simulator \- Dart API docs \- Pub.dev, accessed December 22, 2025, [https://pub.dev/documentation/slow\_net\_simulator/latest/](https://pub.dev/documentation/slow_net_simulator/latest/)
4. Resilience Testing with Toxiproxy | by Matthew Lucas | Medium, accessed December 22, 2025, [https://notmattlucas.com/resilience-testing-with-toxiproxy-f24ce7b81dba](https://notmattlucas.com/resilience-testing-with-toxiproxy-f24ce7b81dba)
5. Shopify/toxiproxy: :alarm\_clock: A TCP proxy to simulate network and system conditions for chaos and resiliency testing \- GitHub, accessed December 22, 2025, [https://github.com/Shopify/toxiproxy](https://github.com/Shopify/toxiproxy)
6. ToxiproxyEx — toxiproxy\_ex v2.0.1 \- Hexdocs, accessed December 22, 2025, [https://hexdocs.pm/toxiproxy\_ex/](https://hexdocs.pm/toxiproxy_ex/)
7. toxiproxy package \- github.com/shopify/toxiproxy/client \- Go Packages, accessed December 22, 2025, [https://pkg.go.dev/github.com/shopify/toxiproxy/client](https://pkg.go.dev/github.com/shopify/toxiproxy/client)
8. Toxiproxy Module \- Testcontainers for Java, accessed December 22, 2025, [https://java.testcontainers.org/modules/toxiproxy/](https://java.testcontainers.org/modules/toxiproxy/)
9. ToxiProxy \- Chaos Toolkit \- The chaos engineering toolkit for developers, accessed December 22, 2025, [https://chaostoolkit.org/drivers/toxiproxy/](https://chaostoolkit.org/drivers/toxiproxy/)
10. connectivity\_plus | Flutter package \- Pub.dev, accessed December 22, 2025, [https://pub.dev/packages/connectivity\_plus](https://pub.dev/packages/connectivity_plus)
11. \[Question\]: how can I mock a connectivity change in unit tests? · Issue \#3029 · fluttercommunity/plus\_plugins \- GitHub, accessed December 22, 2025, [https://github.com/fluttercommunity/plus\_plugins/issues/3029](https://github.com/fluttercommunity/plus_plugins/issues/3029)
12. How to Build an Always Listening Network Connectivity Checker in Flutter using BLoC, accessed December 22, 2025, [https://www.freecodecamp.org/news/how-to-build-an-always-listening-network-connectivity-checker-in-flutter-using-bloc/](https://www.freecodecamp.org/news/how-to-build-an-always-listening-network-connectivity-checker-in-flutter-using-bloc/)
13. Hybrid logical clock \- Andy Matuschak's notes, accessed December 22, 2025, [https://notes.andymatuschak.org/Hybrid\_logical\_clock](https://notes.andymatuschak.org/Hybrid_logical_clock)
14. hlc \- Dart API docs \- Pub.dev, accessed December 22, 2025, [https://pub.dev/documentation/hlc/latest/](https://pub.dev/documentation/hlc/latest/)
15. Hybrid Logical Clock implementation in TypeScript \- typeonce.dev, accessed December 22, 2025, [https://www.typeonce.dev/snippet/hybrid-logical-clock-implementation-typescript](https://www.typeonce.dev/snippet/hybrid-logical-clock-implementation-typescript)
16. hlc\_dart | Dart package \- Pub.dev, accessed December 22, 2025, [https://pub.dev/packages/hlc\_dart](https://pub.dev/packages/hlc_dart)
17. hlc\_dart package \- All Versions \- Pub.dev, accessed December 22, 2025, [https://pub.dev/packages/hlc\_dart/versions](https://pub.dev/packages/hlc_dart/versions)
18. test | Dart package \- Pub.dev, accessed December 22, 2025, [https://pub.dev/packages/test](https://pub.dev/packages/test)
19. Property-based testing \- Antithesis, accessed December 22, 2025, [https://antithesis.com/resources/property\_based\_testing/](https://antithesis.com/resources/property_based_testing/)
20. wigahluk/dart-check: A monadic QuickCheck inspired library for Dart \- GitHub, accessed December 22, 2025, [https://github.com/wigahluk/dart-check](https://github.com/wigahluk/dart-check)
21. polux/propcheck: Exhaustive and randomized testing of Dart properties \- GitHub, accessed December 22, 2025, [https://github.com/polux/propcheck](https://github.com/polux/propcheck)
22. The sad state of property-based testing libraries : r/programming \- Reddit, accessed December 22, 2025, [https://www.reddit.com/r/programming/comments/1duamq2/the\_sad\_state\_of\_propertybased\_testing\_libraries/](https://www.reddit.com/r/programming/comments/1duamq2/the_sad_state_of_propertybased_testing_libraries/)
23. Diff Match Patch \- Dart API docs \- Pub.dev, accessed December 22, 2025, [https://pub.dev/documentation/diff\_match\_patch/latest/](https://pub.dev/documentation/diff_match_patch/latest/)
24. myers-diff \- NPM, accessed December 22, 2025, [https://www.npmjs.com/package/myers-diff](https://www.npmjs.com/package/myers-diff)
25. diffutil\_dart | Dart package \- Pub.dev, accessed December 22, 2025, [https://pub.dev/packages/diffutil\_dart](https://pub.dev/packages/diffutil_dart)
26. Extend with Go \- Overview \- Docs \- PocketBase, accessed December 22, 2025, [https://pocketbase.io/docs/go-overview/](https://pocketbase.io/docs/go-overview/)
27. pocketbase package \- github.com/pocketbase/pocketbase \- Go Packages, accessed December 22, 2025, [https://pkg.go.dev/github.com/pocketbase/pocketbase](https://pkg.go.dev/github.com/pocketbase/pocketbase)
28. Extend with Go \- Testing \- Docs \- PocketBase, accessed December 22, 2025, [https://pocketbase.io/docs/go-testing/](https://pocketbase.io/docs/go-testing/)
29. Extend with JavaScript \- Event hooks \- Docs \- PocketBase, accessed December 22, 2025, [https://pocketbase.io/docs/js-event-hooks/](https://pocketbase.io/docs/js-event-hooks/)
30. \[Web\]PocketBase Hooks Collection | B4X Programming Forum, accessed December 22, 2025, [https://www.b4x.com/android/forum/threads/web-pocketbase-hooks-collection.159299/](https://www.b4x.com/android/forum/threads/web-pocketbase-hooks-collection.159299/)
31. Extend with Go \- Event hooks \- Docs \- PocketBase, accessed December 22, 2025, [https://pocketbase.io/docs/go-event-hooks/](https://pocketbase.io/docs/go-event-hooks/)
32. In a web app when should you close the connection? : r/sqlite \- Reddit, accessed December 22, 2025, [https://www.reddit.com/r/sqlite/comments/1gojiol/in\_a\_web\_app\_when\_should\_you\_close\_the\_connection/](https://www.reddit.com/r/sqlite/comments/1gojiol/in_a_web_app_when_should_you_close_the_connection/)
33. How to ensure sqlite db connections get closed during debugging? \- Stack Overflow, accessed December 22, 2025, [https://stackoverflow.com/questions/15551323/how-to-ensure-sqlite-db-connections-get-closed-during-debugging](https://stackoverflow.com/questions/15551323/how-to-ensure-sqlite-db-connections-get-closed-during-debugging)
34. SQLite, keep connection open or close every time?, accessed December 22, 2025, [https://use-livecode.runrev.narkive.com/eSQa4A2Q/sqlite-keep-connection-open-or-close-every-time](https://use-livecode.runrev.narkive.com/eSQa4A2Q/sqlite-keep-connection-open-or-close-every-time)
35. Closing A Database Connection \- SQLite, accessed December 22, 2025, [https://sqlite.org/c3ref/close.html](https://sqlite.org/c3ref/close.html)