The field does not always have a network

Basements, plant rooms, rooftops: a technician must be able to clock in, take notes and photos without a connection. The Flutter mobile app therefore writes each action to a local database, in a queue (outbox), then synchronises it in batches as soon as the network returns. The local database is for offline work; the server remains the source of truth.

Synchronised events cover the whole intervention:

  • arrival, start, pause, resume and end of work;
  • notes and structured measurements;
  • parts used or unavailable;
  • photos and the customer’s signature;
  • customer absent, access blocked, hazard, return visit needed, making safe.

One identifier per event

Each event gets a clientEventId (UUID v4) on the phone, which is never regenerated on a retry. Replaying exactly the same event has no effect, even after the intervention is closed: the response simply reports it as a duplicate. Reusing the same identifier with different content is refused (409).

The mobile app can therefore resend a whole batch after losing a network response, with no risk of duplicates. A batch holds at most 100 events and is transactional: if one of them is invalid, none is created. The server also checks the clock-in sequence; a pause without a start or two starts in a row are rejected. A declared part decrements the vehicle stock only once, even if the upload is replayed.

Media checked before being accepted

Before sending a photo or a signature, the mobile app declares an upload intent: MIME type, size and SHA-256 fingerprint. The server answers with a URL carrying a random token, valid for fifteen minutes and usable only once.

On receipt, it checks the token, the size, the fingerprint and the file’s actual binary signature. Only a validated medium, belonging to the same organisation and the same intervention, can then be referenced by a field event. Files go to object storage (S3 or compatible, such as MinIO), not to PostgreSQL.

A real-time stream that resumes where it stopped

Dispatch screens and the app receive changes through an authenticated SSE stream isolated per organisation. After an outage, the client resumes from the last event received thanks to the Last-Event-ID header; events are kept for seven days by default. A technician only receives those of the interventions assigned to them.

Push notifications (Firebase Cloud Messaging) are deliberately generic: they only carry identifiers, and the app always reloads business data through the authenticated API.

What idempotency changes

It puts the complexity in the right place. The mobile app does not need to know whether a previous upload succeeded: when in doubt, it resends. The server guarantees that the same fact is recorded only once.