Skip to main content
Flutter Offline-First Apps for Odoo: Sync Queues, Conflicts, and Safe Retries
12 min read
Flutter

Flutter Offline-First Apps for Odoo: Sync Queues, Conflicts, and Safe Retries

Build a Flutter app that works offline with Odoo: durable sync queues, idempotent writes, conflict handling, mobile security, and practical recovery tests.

A Flutter app can keep field work moving when the connection to Odoo disappears. It should save a local draft, queue the intended action, and synchronize when the server is reachable. It should not tell a warehouse operator that stock has been transferred, or a technician that a service order has been completed in Odoo, until the server validates and commits that action.

That difference—captured locally versus accepted by the ERP—is the core of reliable offline-first design. It also explains why caching screens or listening for Wi-Fi changes is not enough.

Flutter's official offline-first architecture guide describes repositories that coordinate local storage and remote services, with different strategies for offline reads and writes. Odoo's version 20 JSON-2 API documentation explains the other side of the boundary: each API request executes in its own transaction, so multi-step business operations must be handled carefully.

Here is a design approach for warehouse, field-service, delivery and employee apps that need both dependable local capture and authoritative Odoo records.

Decide which operations are allowed offline

Do this before choosing a Flutter database or sync package. Offline capability should follow business risk.

Mobile actionSafe offline behaviorWhat still needs Odoo
View an assigned jobShow the last authorized cached version, labelled with its ageFresh assignment, cancellation and access checks
Draft a service reportSave text, checklist answers and attachment references locallyValidate job state, ownership and completion rules
Scan a product and locationQueue a proposed scan against the downloaded taskConfirm availability, lot/serial rules, reserved quantity and destination
Capture proof of deliveryPreserve evidence locally and mark the upload pendingCheck order state and accept the evidence
Clock in at a siteShow a captured event pending verification if offline capture is permittedValidate identity, policy, timestamps and location evidence
Post a stock adjustment or financial documentNormally block final posting offlinePerform the authoritative business transaction

An offline field app may support some of these actions and intentionally reject others. For example, a proof-of-delivery photo can be captured offline, while a completed delivery may depend on stock moves and other records that changed while the phone was disconnected.

The Octivex mobile development overview discusses how to define those workflow boundaries. The question here is narrower: how should the synchronization mechanism implement them?

The architecture: one local repository, one durable outbox, one server authority

A practical application has five responsibilities:

  1. Flutter view and view model show the locally known state, including unsynchronized changes.
  2. Feature repository coordinates reads and writes so widgets never improvise a network call.
  3. Local database stores permitted cached records, drafts, and an outbox of pending commands.
  4. Sync worker sends eligible commands, interprets responses, and updates local state.
  5. Odoo integration endpoint or service authenticates the actor, checks business rules, applies a command once, and returns an authoritative result.

Flutter's guidance calls repositories the application's data-access boundary; its app architecture recommendations also favor separation between UI and data concerns.

The phone's local database is not another ERP. It is a temporary working copy with limited permissions and a queue of work waiting for the system of record. Odoo remains responsible for stock, payroll, approvals, pricing, customer commitments, and business transitions.

Put the draft and outbox event in one local transaction

Consider a technician who finishes a job in a basement with no signal. If the app saves the form but crashes before it records the pending sync action, the form may never reach the server. If it queues the command but does not save the form, the retry may contain incomplete data.

Instead, make one local database transaction do both things:

  • save the technician's revised local draft;
  • insert an immutable outbox command with a stable unique identifier;
  • set the local status to pending.

Only then show “Saved on this device · Waiting to sync.” A device restart should not erase either half.

An illustrative command envelope might look like this:

json
{
  "command_id": "uuid-created-once-and-persisted",
  "command_type": "submit_service_report",
  "record_ref": "assigned-job-427",
  "base_revision": "last-server-revision-seen",
  "payload": {
    "checklist_complete": true,
    "notes": "Inspection completed"
  }
}

Illustrative design only: these are not standard Odoo fields or a built-in JSON-2 method. They require an application contract and server-side implementation. In particular, the local draft and the command should be stored together using a transactional persistence layer appropriate for the supported devices.

Design the sync queue as a state machine

A reliable queue has more than an isSynced boolean. A useful minimum is:

pending → sending → acknowledged

with branches from sending to retry_wait, conflict, or rejected.

  • pending: saved locally, never confirmed by Odoo.
  • sending: an attempt is in progress. If the app dies here, retry only under an idempotent server contract.
  • retry_wait: transient failure, retained with the next eligible retry time.
  • acknowledged: Odoo has accepted the command and returned its record/result identifier.
  • conflict: Odoo found a newer or incompatible server state; a user or explicit policy must resolve it.
  • rejected: validation or authorization failed and an automatic retry would not fix the cause.

Persist the command ID, operation type, actor context, creation time, number of attempts, last safe error code, and server acknowledgment. Avoid saving secrets in the queue or dumping payloads into analytics logs.

Commands concerning the same business record may need strict ordering: a report cannot be submitted before its required attachment has been uploaded, and a cancellation must not overtake a creation it depends on. Unrelated records may be synchronized independently if the backend contract permits it.

Show all three meaningful user outcomes: stored on device, waiting for server, and confirmed by Odoo. A green success check should mean the third state, not merely the first.

Network availability is a hint, not a successful API call

The maintainers of connectivity_plus explicitly warn that a Wi-Fi or cellular connection does not guarantee internet access. A captive portal, DNS error, expiring session, firewall or unavailable Odoo server may still prevent synchronization.

Use connectivity signals to schedule an attempt, not to declare it successful. A practical trigger set includes:

  • app resume;
  • user-initiated “Sync now”;
  • a network-change event;
  • a carefully constrained background opportunity;
  • a short backoff retry following a temporary failure.

The retry loop needs request timeouts, exponential backoff with jitter, and a maximum automated retry policy for repeated terminal failures. Keep a visible queue when the network comes back but the Odoo endpoint is still unavailable.

Background execution is not guaranteed to occur at a particular moment. Flutter's official guide warns about battery and platform constraints. Android WorkManager supports network and battery constraints, but tasks can be deferred. Design as though foreground sync on next launch may be the first reliable opportunity.

Prevent duplicate orders, check-ins and job completions

The hardest failure happens when Odoo commits a request but the phone never receives the response. The mobile client observes a timeout and cannot tell whether the server saved anything.

Simply retrying a generic create call can create a second record.

Use a stable command ID generated once on the phone and persisted with the original action. Send that same identifier on every retry. The Odoo endpoint should atomically recognize an already-processed identifier and return the original outcome instead of performing the action again.

A robust server contract includes:

  • one unique command ID within an appropriate business scope;
  • an authenticated, authorized actor;
  • a durable server record of command execution or result;
  • a uniqueness constraint or equivalent transactional protection;
  • a business operation executed within the relevant transaction;
  • the same safe response for a repeated command ID.

A client-side duplicate check alone is insufficient. Two requests can arrive concurrently, and an earlier response may have been lost.

Odoo 20's JSON-2 transaction documentation states that each API call runs in its own SQL transaction. It recommends a single server-side method for business work that must complete atomically. A dedicated reviewed Odoo module may therefore be appropriate when one offline action must validate a job, change its state, create evidence, and store the command result together.

Do not confuse a stable command ID with a guarantee that every Odoo method is idempotent. The server implementation must enforce that behavior.

Define conflicts by business operation—not by timestamp alone

Suppose a technician downloads a job in the morning, fills a report offline, and reconnects after a dispatcher has cancelled the job. Silently posting the old report as “completed” would erase a meaningful business decision.

For each mutation, persist the last known server revision or another contractually agreed version marker. Before applying the command, the server compares that state with the current record and chooses a documented policy.

SituationRecommended approach
New immutable observation linked to an active jobAppend if still permitted; do not overwrite unrelated history
Edited free-text draftMerge nonoverlapping fields only under a defined policy; otherwise review
Cancelled job being marked completeReject as a conflict and request a decision
Stock quantity or reservation changedRevalidate centrally; do not use last-write-wins
Leave approval or financial postingKeep approval and posting authoritative on Odoo

Device clock timestamps are useful evidence but not a universal conflict resolver. Clocks drift, updates arrive out of order, and approval or inventory state cannot safely be overwritten based on “newest wins.”

A conflict should display what the field worker submitted, what Odoo now says, and what to do next. Keep the original offline work recoverable for appropriate operators rather than deleting it after a 409-style conflict response. The exact HTTP response design is application-specific.

Odoo 20 integration: JSON-2, custom controllers, and secrets

Odoo's official JSON-2 documentation for 20.0 describes endpoint paths such as /json/2/<model>/<method>, API-key authentication, method-level transactions, and record-rule enforcement under the authenticated user. It also states external JSON-2 API access is available on Custom pricing plans, not One App Free or Standard; confirm the actual edition, hosting and commercial plan before relying on that route.

These facts do not mean a privileged API key belongs inside a distributable Flutter app. OWASP's mobile storage guidance and hard-coded secret guidance explain why secrets embedded in app packages are recoverable.

For mobile integrations, use an appropriate end-user authentication architecture or an authenticated middleware layer. A custom Odoo controller can expose a narrowly scoped, permission-checked business action; JSON-2 may suit trusted server-to-server operations. A mobile sync endpoint should never accept user_id, company_id, or “approved” flags as authority simply because the client supplies them.

Check current user permissions, companies, record rules, and business state on the server for every replay. If a user loses access while offline, the pending action may be rejected on reconnection. That is correct behavior, not a sync-engine failure.

There are also valid reasons to use a responsive native Odoo experience rather than a custom Flutter app. Evaluate supported workflows, device peripherals, offline requirements, and ownership before introducing another client. For background on Odoo's external API direction, see Octivex's Odoo 20 JSON-2 integration guide.

Attachments need their own recovery design

Photos, signatures and PDFs add a separate failure mode. A report may be a small JSON command while its evidence is several megabytes.

Define whether a report can be submitted before attachments arrive, whether an upload can resume, and how an orphaned upload is cleaned up. Store attachment state in the same durable operation graph, with local references retained until the server confirms both file acceptance and business association.

The server must validate content type, size, ownership and related Odoo record access. Do not assume “file uploaded” means “proof of delivery accepted.”

Offline caches also need a retention plan. Payroll documents, identity files and customer addresses should not live indefinitely in plaintext on a device that may be lost. Use platform-backed credential storage where appropriate, protect local sensitive data, constrain backups and redact logs. OWASP's MASVS storage controls cover intentional and accidental storage exposure.

A test matrix worth running before rollout

A demo with airplane mode toggled once is not an offline acceptance test. Run the following with representative employee roles, different devices and a production-like Odoo staging database.

TestExpected outcome
Save a draft with no signal; force-close; relaunchDraft and outbox command are still present
Odoo commits, but response is droppedRetried command returns one business result, never a duplicate
Network oscillates repeatedlyNo concurrent double submission; queue remains inspectable
Another user changes the assigned jobVersion conflict is surfaced rather than overwritten
Auth expires while offlinePending work stays local; upload waits for valid authorization
Company or role access is revokedServer rejects unauthorized replay; UI retains an appropriate error state
Attachment upload succeeds but association failsApp resumes or safely resolves the incomplete operation
Hundreds of queued actions recover togetherBounded concurrency, sensible retries, no server overload
App is terminated during sendingRecovery determines whether it can retry safely using the stable command ID
Device storage is fullUser sees a failure; app never reports an unsaved draft as stored
Logout, lost device, or account removalSensitive offline data follows the documented retention and purge policy

Measure the outcomes that matter: time from local capture to authoritative acknowledgment, backlog size and age, permanent rejection rate, conflict rate, and duplicate prevention. Do not promise universal “instant sync”—the network and platform scheduler are not under the app's control.

What this means for a real field rollout

Start with one bounded workflow. For a service team, that could be “download assigned jobs, complete a checklist and attach one photo, then receive a server confirmation.” Avoid beginning with every HR, stock, sales and project module at once.

Before release, define the server contract and which steps the field worker may complete without connectivity. Demonstrate the network-loss cases above in front of the operations owner, not just the development team. Document who resolves rejected events and what information support can safely inspect.

Octivex builds mobile and web applications connected to Odoo, including field-oriented employee workflows. If your next mobile workflow depends on intermittent connectivity, the best starting point is a short offline/online transaction map and a testable sync contract—not a package recommendation.

An offline-first app is successful when users can keep working without misleading them about what the ERP has accepted. Local durability, replay safety and explicit conflicts matter more than a green connectivity icon.