Wise Hustlers — Digital Product & App Development Studio Logo
Get Consultation
By Wise Hustler Admin9/1/202612 min read

Offline-First on FPSOs: Syncing Data When the Satellite Link Drops Mid-Shift

Offline-First on FPSOs: Syncing Data When the Satellite Link Drops Mid-Shift

# Offline-First on FPSOs: Syncing Data When the Satellite Link Drops Mid-Shift

TL;DR: On an FPSO, the field application has to assume the link will drop — the correct architecture always writes locally first, syncs only the differences once the link returns, resolves conflicts field by field (not shift by shift), and uses idempotency keys so a retried request never duplicates a work order or a tank reading.

The problem isn't the internet, it's the assumption

Most enterprise software — ERPs, maintenance systems, HSE platforms — is written on an implicit assumption: the HTTP request reaches the server, the server responds, and if something fails the user tries again a second later. That assumption is reasonable in an office in Luanda with fibre. It is false on an FPSO offshore, where the only link back to shore runs through a satellite connection shared between operational data, voice, crew IPTV and, increasingly, video calls.

When that link drops — heavy rain, the antenna losing lock as the vessel moves with the sea, congestion, or the satellite operator doing maintenance — the crew cannot stop working. A technician keeps closing work orders. A process operator keeps logging tank readings. An HSE supervisor keeps opening safety observations. If the application only knows how to function with the server responding, the result is paper, memory and manual reconciliation days later — the point where an offshore unit's operational data stops being reliable.

The engineering answer is called offline-first: the application treats the network as an intermittent, optional resource rather than a synchronous dependency. This is not a UX detail ("show an offline icon"). It's a data-architecture decision that cuts across the client, the sync queue and the server's conflict model.

It's worth understanding the physics before designing the solution, without forcing numbers that cannot be verified for any given unit's actual setup.

Most FPSOs still rely on geostationary VSAT. A geostationary satellite orbits at 35,786 km, and the distance the signal travels imposes an unavoidable physical propagation delay. Per SatSig, a single hop — up to the satellite and back down, roughly 72,000 km — costs 240 ms in the best case, with the satellite directly overhead; at the edge of the coverage area, between two equally distant sites, it rises to about 280 ms. The number that matters when you are designing software is double that: a request and its reply cross the satellite twice, so 480 to 560 ms, before you add any ground processing or the terrestrial network back to the data centre. That is why applications relying on a synchronous server response "feel" the satellite even when the link is technically up.

Bandwidth has also historically been scarce and expensive offshore: voice, a dedicated data circuit and video conferencing compete for the same channel, and demand for data has grown steadily as more systems come to depend on the connection (Offshore Magazine). Ku- and Ka-band services have increased available capacity, but the link remains subject to interruption from bad weather, antenna mispointing, or satellite-operator maintenance.

Low-earth-orbit constellations such as Starlink Maritime change the latency profile — operating much closer to Earth, typical latency is lower than traditional geostationary systems (Wikipedia — Starlink) — and shipowners and offshore operators are already adding them as a connectivity layer. That doesn't eliminate the problem: it remains a best-effort service with no contractual guarantee of continuous availability, and the unit still needs a resilience layer that doesn't depend on any single link. That layer is the application itself.

The architecture: local write queue first

The core principle of offline-first is easy to state and hard to implement correctly: every user write is confirmed locally before it is confirmed by the server.

In practice this means:

1. A local database on the device or an onboard server. Each technician's tablet, or a local server aggregating the unit's devices, keeps a working copy of the relevant operational data — open work orders, assets, tanks, HSE checklists — in an embedded database (SQLite, or an equivalent mobile engine).

2. A write queue. Every user action — closing a work order, logging a reading, submitting a safety observation — produces an immutable record in the local queue: who, what, when, and the change payload. The application never waits on the network to confirm the action; it confirms as soon as the write lands locally.

3. A background upload process. A separate process drains the queue whenever connectivity exists — even a window of a few minutes — and pushes pending records to the central server, one at a time or in small batches.

4. Acknowledgment and cleanup. Only once the server confirms receipt does the record leave the local queue. Until then it stays "pending," visible to the user if needed.

This means the UI never blocks waiting on the server, and an entire working session — a 12-hour shift with the link down — doesn't lose a single closed work order, as long as the device itself isn't lost or destroyed.

Differential sync: never re-send the whole world

With bandwidth measured in a few shared Mbps across the whole unit, re-uploading the entire database state on every reconnection isn't an option. Sync has to be differential: send only what changed since the last successful sync, in both directions.

This typically combines two mechanisms:

  • Per-record change vectors. Every syncable entity (work order, reading, asset) carries a sequence number or logical timestamp that increments on every change. The client stores the "cursor" of its last successful sync and asks the server only for records with a higher sequence — and conversely, the server only needs to pull the client's still-unacknowledged queue entries.
  • Payload compaction. For long text fields or attachments (inspection photos, for example), send binary deltas or compress aggressively before queuing, and defer heavy attachment uploads until bandwidth allows, without blocking the sync of the structured data that's operationally more urgent — a work order's status, for instance.

This sharply reduces the data volume per sync cycle, which matters both for link cost and because an offshore connectivity window can last only a few minutes before the link drops again — differential sync lets that window be spent on what's operationally critical instead of re-sending data that hasn't changed.

Conflict resolution: LWW, CRDT and field-level merge

Here is the real offline-first problem, and where most amateur implementations fail: two different users can change the same record while both are offline, and when the link returns, the server receives two diverging versions of the same data. "Which one wins?" has no generic answer — it depends on the field type.

A concrete example

Consider Work Order WO-4521, "Replace seal on valve XV-204," on an FPSO with the link down since the start of the shift.

  • At 08:14, Technician A logs on their tablet: status = Completed, and consumes 3 units of seal stock from the onboard store.
  • At 11:40, Supervisor B, unaware that A had already closed the order, logs on their own session: status = Blocked — spare part unavailable, with no material consumption recorded.

The link comes back at 14:00. The server receives both changes nearly simultaneously. What happens?

If the system uses naive last-write-wins (LWW) — the version with the most recent timestamp wins — Supervisor B's record (11:40, later) overwrites Technician A's. The result: the order ends up marked "Blocked," despite having been physically completed, and the 3 seals consumed silently disappear from the consumption history. No one is alerted. It's data loss disguised as automatic resolution — the most common complaint against naive LWW, precisely because the "winner" depends on clocks that can drift out of sync between devices.

The correct alternative combines two strategies, field by field:

1. Additive fields (counters) resolve with a CRDT. Material consumption — "3 units of seal" — is not a value that overwrites, it's a value that accumulates. Modeled as a CRDT counter (a PN-Counter, which separates increments and decrements and always converges to the same total regardless of arrival order), Technician A's consumption is preserved in inventory even if the rest of the record conflicts. That's exactly what CRDTs guarantee: replicas that update independently, offline, out of order, always converge to the same final state without prior coordination (crdt.tech).

2. Business-semantic status fields escalate to human review. "Completed" and "Blocked — spare part unavailable" are not two equivalent values where the most recent one is automatically correct; they are two operationally incompatible statements about the same asset. The defensible practice is neither LWW nor an automatic merge — it's detecting the divergence and surfacing it as an explicit conflict to the maintenance supervisor, with both versions shown side by side, for a human decision. LWW is still acceptable for low-risk fields — a free-text note, for instance — but that's a business choice, not a safe default for every field.

This is the difference between conflict resolution as a generic library feature and conflict resolution as a domain-modeling decision: every field has to be classified — additive, exclusive state, free text, attachment — and each category gets a different strategy. A direct comparison between LWW and CRDTs shows exactly this contrast: LWW is fast and simple but silently discards the losing version, while CRDTs avoid that loss by structuring the operation so the merge is always deterministic, at the cost of more modeling complexity (DZone).

Idempotency: when the client resends the same request

An unstable link doesn't just produce conflicts between users — it also produces duplicate resends from the same user. Technician A's tablet sends the WO-4521 closure, the connection drops before the server's response makes it back, and the local queue — correctly, from the device's point of view — treats the missing acknowledgment as a failure and retries once the link comes back. Without protection, the server processes the same operation twice: two stock deductions, two notifications, possibly two audit entries.

The correct technique is an idempotency key: every operation generated in the local queue gets a unique identifier (a UUID generated at the moment of the user's action, not at the moment of the network send), which travels with the request on every attempt, including resends. On the server, accepting and applying that change has to be an atomic operation: claiming the key and executing the data mutation in the same transaction, so that either both happen or neither does — closing the window where two concurrent attempts could both see the key as "free" at once. Subsequent requests with the same key return the already-processed response instead of repeating the effect. This is the same practice documented in the AWS Builders' Library for safe idempotent APIs under retry (AWS) — the pattern that separates "safely resending" from "resending and duplicating the work order."

One detail that's often missed: the key has to be generated from the operation's intent (the moment the user taps "Complete"), not on every new network attempt. If the key is regenerated on every retry, each retry looks like a brand-new operation to the server, and idempotency protection does nothing.

Where this fits in the infrastructure

None of this works in isolation on the device. The sync queue has a destination — typically a cloud ingestion service, with managed queues, event storage and observability over conflict rates and sync latency — and that destination is as critical to the system's reliability as the logic on the technician's tablet. Designing that layer (resilient queues, duplicate detection, alerting when an asset's conflict rate spikes abnormally, and the network topology between the offshore unit and the data center) is infrastructure work as much as application work — it's the kind of architecture we cover in our Cloud & DevOps service.

Practical implementation checklist

  • Every user write lands locally first and is confirmed in the UI before any network call.
  • The sync queue is durable (survives app and device restarts) and ordered by local timestamp.
  • Sync is differential — it never re-sends the full state, only the deltas since the last acknowledged point.
  • Every field in the data model is classified: additive (CRDT), exclusive state (conflict detection + human review), or low-risk (LWW acceptable).
  • Every mutable operation carries an idempotency key generated at the moment of user intent, not at the moment of network transmission.
  • Claiming the idempotency key and mutating data happen inside the same atomic server-side transaction.
  • There's a dashboard — even a simple one — of conflicts pending human review, visible to whoever manages maintenance or HSE operations.

FAQ

Does offline-first mean the application never needs the network?

No. It means the network is treated as an opportunistic resource: the application works fully without it for the shift's operations, but depends on it to propagate data between the offshore unit, other units, and the central onshore systems (ERP, CMMS, consolidated HSE reporting).

Why not just use last-write-wins on every field, to keep it simple?

Because LWW silently discards the losing version whenever two users change the same field offline — which can mean losing the record that a valve was blocked for lack of a spare part, or erasing material consumption. It's acceptable for low-risk fields, not for operational status or quantities.

Do CRDTs resolve all conflicts automatically, with no human involvement?

They deterministically resolve the conflicts they were designed for — typically additive values. By nature, they do not resolve exclusive-state conflicts with incompatible business semantics (completed vs. blocked); those need explicit detection and a human decision.

What happens if the same request is sent twice by mistake, with no idempotency key?

The server processes the operation twice — the most common scenario is duplicated stock deductions or notifications. With an idempotency key generated at the source and claimed atomically on the server, the second request is recognized as a duplicate and returns the already-processed response.

Sources