to select ↑↓ to navigate
mForm Docs

mForm Docs

Open in ChatGPT
Ask ChatGPT about this page
Open in Claude
Ask Claude about this page

2.0.0-beta

Prerelease · Published 30 July 2026 · Requires frappe-mobile-control >= 1.0.0 · Database schema 4

Superseded by SDK 2.0.0-beta.2, published 12 August 2026. Read this page for the offline-first foundation, then read beta.2 for what changed since.

This is the offline-first release. An app built on 2.0 keeps working when the network drops and syncs what it collected once the phone is back online.

It is published as a prerelease for a one-month beta. flutter pub add frappe_mobile_sdk still gives you 1.2.0, so ask for the beta by name:

dependencies:
  frappe_mobile_sdk: ^2.0.0-beta.2

Read this before you upgrade a device

Install Control on the server, set the flag, then ship the app. That order matters.

  1. frappe-mobile-control >= 1.0.0 on the bench
  2. In Mobile Configuration, tick Enabled and set Offline Enabled
  3. Then the SDK build goes to devices

Ship the app first and the device reads offline_enabled as false, because the server has no such field yet. If that device is holding 1.x offline data, the next launch tries to drain it and then drops the local tables. With no signal at that moment the drain fails and the drop still goes ahead, so the data is gone.

There is no way back. sqflite has no downgrade hook and the SDK carries no reverse migration, so a device that moves to schema 4 stays on schema 4. Fix forward with patch releases.

What is new

A real offline database. Every doctype now gets its own SQLite table with typed columns, plus an outbox for writes waiting to go up. In 1.x everything shared one JSON-blob table, which meant an offline list query scanned the whole table and parsed every row. Tables are created on the first pull for that doctype, not at startup.

One switch on the server decides the mode. offline_enabled on Mobile Configuration is the only thing that decides whether a deployment runs offline-first or as a thin online client. The flag rides along on the login response and persists on the device. Wrap your app shell in OfflineTransitionGuard and it handles the moment the phone comes back online.

One read path instead of three. UnifiedResolver serves list screens, Link pickers and fetch_from. It reads the local database first and refreshes in the background when there is a connection, and it goes straight to REST when offline mode is off. Every row tells you where it came from, server or local edit.

Filters work offline too: = != < <= > >= in not in like not like between is is not, plus Frappe Timespan keywords.

Writes go up in the right order. The outbox drains in dependency tiers rather than the order things were typed, so a record that points at another unsent record waits its turn. Three levels of idempotency stop duplicate inserts, including the awkward case where the server saved your write but the response never came back.

Pulls only fetch what changed. A (modified, name) watermark per doctype makes every pull after the first one a delta, and a sync that gets interrupted picks up from the cursor instead of starting over.

Sync you can drive from code. SyncController gives you syncNow, pause and resume, retry, retryAll, resolveConflict, a delete-cascade preview, and a state$ stream. It is on sdk.syncController.

Sync your users can see. SyncStatusBar, SyncProgressScreen, SyncErrorsScreen, DocumentListFilterChip, OfflineTransitionScreen and OfflineTransitionGuard, with dialogs for delete cascade, logout guard and force logout.

Failed pushes report themselves. When the server permanently rejects a write, the SDK captures the request, the response and the session user and posts it to mobile_sync.report_error. There is nothing to wire up.

The logged-in user is available immediately. SessionUser fills on every login path and restores from local storage, so it is on sdk.sessionUser the moment initialize() returns.

Translations work offline. Cached in SQLite and refreshed once per login for every enabled language, with a delegate hook if your app already ships compiled ARB files you would rather use.

Breaking changes

DocumentDao and DocumentEntity are gone. Use OfflineRepository and UnifiedResolver instead. Rows come back as Map<String, Object?> keyed by mobile_uuid, with fields at the top level. createDocument and updateDocumentData are now one method, saveDocument: pass no mobile_uuid to insert, pass one to update.

LinkOptionService takes a resolver and a meta-resolver now instead of a FrappeClient. This is the only constructor change without a default, so it is the one that needs you to edit code. OfflineRepository, SyncService, UnifiedResolver and FrappeSDK.forTesting also gained parameters, but those all default sensibly.

Four things behave differently without the API changing:

  • Pushes go in dependency order, not FIFO. Any test asserting strict insertion order will fail.
  • Forms clear dependent Link fields on their own, so keep FieldChangeHandler callbacks to deriving values.
  • pullSync returns empty for child doctypes and makes no HTTP call. Children arrive embedded in the parent pull.
  • A value shaped like a v4 UUID resolves locally only.

What did not change: the FrappeClient surface, every form widget and field, the login screens, FrappeAppGuard, WorkflowService and the exception hierarchy. If your app only touches those, upgrading is a version bump and a flag flip on the server.

Fixed

  • A field-change handler that patched the field it was reacting to crashed with unbounded recursion.
  • An insert whose response was lost could slip past the conflict guard and overwrite local edits on the next pull.
  • A paused insert and a pending insert for the same document could both dispatch.
  • Phone numbers were saved without their dial code.
  • Conflicts now flag only when the server copy is actually newer.
  • Long field labels no longer overflow sideways.
  • FrappeAppGuard.allowDeferringUpdates did nothing, and its default contradicted the docs.

Known limitations

  • Token refresh does not re-read offline_enabled. Switching a live session between modes needs a full re-login.
  • The schema migration is silent when it works and throws on the next database open when it does not. Instrument around FrappeSDK.initialize() if you want visibility during a rollout.
  • Autoname patterns that produce UUID-shaped names break local-versus-remote resolution. Use autoname=field:mobile_uuid or stamp a prefix on the series.
  • Custom columns on child tables are wiped when the parent is pulled.
  • A doctype has no local table until its first pull.

Upgrading from 1.x

The full checklist and API diff are in the repository: Migrating from 1.x. For the plain-English version, see the SDK 2.0 guide.

Last updated 4 weeks ago
Was this helpful?
Thanks!