frappe-mobile-sdk — om/offline_improvement deep-dive
UPDATE 2026-06-02 — Sync bootstrap is on the SDK’s pull pipeline. The Swasti app’s post-login bootstrap used to bypass
SyncService.pullSync(the SDK’s connectivity probe rejectedadb reversetunnels). After diagnosing the gate properly, the bypass was lifted and the bootstrap now drivessdk.sync.pullSync(doctype:)per doctype. The visible win on subsequent logins: surveyors only refetch rows whosemodifiedexceeds the previous sync (delta cursors), with crash-resilient resume mid-pull and 2× page lookahead for free. The non-visible win: zero// ignore: implementation_importsdirectives in the app, ~250 LOC of bypass code retired. Two pull requests are open against the SDKdevelopbranch ondhwani-ris/frappe-mobile-sdk: PR #68 adds anisOnlineOverridehook toSyncService(default null = current behaviour, dev builds set() async => trueto keep the bench reachable throughadb reverse); PR #69 exports the canonicalDependsOnEvaluatoron the SDK barrel so consumers that own their own form layer can applymandatory_depends_onsemantics without crossing thesrc/line. The app builds against both branches today via a local merge branch in the SDK clone.UPDATE 2026-05-27 — MERGED.
om/offline_improvementis now merged intodevelop(tip5dc2a94, 2026-05-22). Swasti now tracksdevelop. develop is a ~31-commit advance over the old feature head — the sync layer was refactored (newschema/parent_schema.dart,child_schema.dart,system_tables.dart) withALTER TABLE ADD COLUMNmigration wrappers, which is the fix for thedocs__table rebuild / data-loss issue (old behaviour dropped + recreateddocs__<doctype>on a metaJson schema diff, wiping unsynced local rows and churningmobile_uuid). Pointing the app atdevelopneeds a compatibility pass (public API moved). The deep-dive below is still accurate on concepts; file paths may have moved on develop.Companion to
02-reliance-frappe-context.md(rel-mis Frappe shape) and04-mform-to-frappe-skill.md(Doctype generation). The SDK is the mobile-side runtime the Swasti Flutter app is built on.Source:
~/Dhwani/swasti-mform-migration/raw/sdk/frappe-mobile-sdk/— track branchdevelop(wasom/offline_improvementhead5bd0a93). Pub package namefrappe_mobile_sdk. Server companion (REQUIRED):frappe-mobile-control, branchdevelop(wasom/mobile-sync-bulk-fetch). See §9 and note 08.
1. What the SDK is
A Flutter package that turns a Frappe site into a metadata-driven mobile backend. It provides:
- Direct Frappe API access — auth (4 flows), CRUD, file upload, custom whitelisted method calls.
- Dynamic form rendering — every form is generated from Frappe
DocTypemetadata (doctype_meta+doc_field); no hand-written form widgets. - Offline-first data layer — SQLite (via
sqflite) with bi-directional sync. Toggleable server-side per deployment (see §8). - Ready-made screens — login, doctype list, document list, form, sync status, sync errors, migration blocked, force-update guard.
- Server-driven app control —
Mobile App Controldoctype on the server gates app status (active / under-maintenance / force-update).
Versioning: pub.dev frappe_mobile_sdk: ^<latest> for releases; git: ref: for branch pinning. rel-mis pins to ~/.pub-cache/git/frappe-mobile-sdk-7d27451ced0fb88f9007e5df70f4948b3afe0a27/ (per rel-mis/docs/archive/plans/2026-03-05-table-multiselect-impl.md:473); Swasti will pin to the merge commit of om/offline_improvement → develop.
2. Architecture
┌──────────────────────── App layer (Swasti Flutter app) ────────────────────────┐
│ AppGuard → Login → MobileHomeScreen ─ DoctypeList ─ DocumentList ─ FormScreen │
└───────────────────────────────┬────────────────────────────────────────────────┘
│ public API (FrappeSDK + FrappeFormBuilder + screens)
┌───────────────────────────────▼────────────────────────────────────────────────┐
│ FrappeSDK (lib/src/sdk/frappe_sdk.dart) │
│ Holds: client, database, services, resolver, sessionUser, syncController │
├─────────────────────────────────────────────────────────────────────────────────┤
│ Services (lib/src/services/) │
│ AuthService MetaService PermissionService TranslationService │
│ SyncService OfflineRepository OfflineTransitionService │
│ LinkOptionService LinkFieldCoordinator ClosureBuilder AppStatusService │
│ LocalWriter AtomicWipe MetaDiffer MetaMigration RetryPriority │
│ SessionUserService WorkflowService BulkWatermarkProbe │
├─────────────────────────────────────────────────────────────────────────────────┤
│ Sync engine (lib/src/sync/) — push & pull, cursor, conflict, attachments │
│ PullEngine ↔ PullPageFetcher ↔ PullApply │
│ PushEngine ↔ PayloadAssembler ↔ ResponseWriteback ↔ AttachmentPipeline │
│ SyncStateNotifier ↔ SyncState ↔ Cursor ↔ ThreeWayMerge ↔ UUIDRewriter │
│ Query (lib/src/query/) — UnifiedResolver, FilterParser, LinkDecorator │
│ Concurrency (lib/src/concurrency/) — SyncMutex, WriteQueue, ConcurrencyPool │
│ ConnectivityWatcher, DeviceTier │
├─────────────────────────────────────────────────────────────────────────────────┤
│ Persistence (lib/src/database/) │
│ AppDatabase (sqflite) │
│ System tables: auth_token, doctype_meta, doctype_permission, sdk_meta, │
│ link_options, outbox, pending_attachments │
│ Per-doctype tables: docs__<doctype> (parent) + docs__<child_doctype> │
│ SchemaApplier — DDL per doctype, idempotent, max 7 indexes │
├─────────────────────────────────────────────────────────────────────────────────┤
│ API layer (lib/src/api/) — FrappeClient, AuthApi, DoctypeService, │
│ DocumentService, AttachmentService, QueryBuilder, RestHelper, OAuth2Helper │
└────────────────────────────────────┬────────────────────────────────────────────┘
│ HTTP
┌────────▼─────────┐
│ Frappe + mobile_ │
│ control + your │
│ Frappe app │
└──────────────────┘
Two operating modes (set per-deployment via server flag, immutable per session):
Online mode (offline_enabled=false, default in 2.1+) | Offline mode (offline_enabled=true) | |
|---|---|---|
| Reads | UnifiedResolver._onlinePassthrough → frappe.client.get_list | LocalWriter / per-doctype tables, fallthrough to network refresh |
| Writes | Direct REST | Outbox queue → push when connectable |
| Sync engine | No-op (SyncResult.empty()) | Active |
docs__<doctype> tables | Not created | Created per-doctype on demand |
outbox / pending_attachments | Empty | Used |
The mode is read once at boot from sdk_meta.offline_enabled and cannot change mid-session. Switching requires logout/login.
3. Public API surface
lib/frappe_mobile_sdk.dart is the barrel. Stable exports:
| Symbol | Source | Purpose |
|---|---|---|
FrappeSDK | src/sdk/frappe_sdk.dart | One-stop init; holds all services. |
FrappeClient, FrappeDocument, QueryBuilder | src/api/* | Direct Frappe HTTP access. |
AuthService | src/services/auth_service.dart | All login flows. |
MetaService | src/services/meta_service.dart | DocType metadata fetch + cache. |
PermissionService | src/services/permission_service.dart | Check & cache per-doctype permissions. |
TranslationService | src/services/translation_service.dart | Pull mobile_auth.get_translations. |
SyncService + SyncController | src/services/sync_service.dart, src/sync/... | Imperative sync surface. |
OfflineRepository | src/services/offline_repository.dart | Read/write through the offline cache. |
OfflineTransitionService | src/services/offline_transition_service.dart | Drain/wipe when toggling modes (P3). |
LinkOptionService, LinkFieldCoordinator | src/services/... | Resolve Link field options + cache. |
SessionUser, SessionUserService | src/models/session_user.dart, src/services/session_user_service.dart | Persisted user identity, roles, perms. |
LinkFilterBuilder, FieldChangeHandler | src/models/link_filter_result.dart, hooks in src/ui/ | Per-doctype customisation hooks (see §5–§7). |
FrappeException, AuthException, ApiException, NetworkException, ValidationException | src/api/exceptions.dart | Typed errors. |
DependsOnEvaluator | src/utils/depends_on_evaluator.dart | Frappe depends_on / mandatory_depends_on evaluator. |
MigrationNeedsNetworkException | src/sdk/frappe_sdk.dart:23 | Thrown by runV1ToV2MigrationIfNeeded when offline. |
Screens: MobileHomeScreen, DoctypeListScreen, DocumentListScreen, FormScreen, FrappeAppGuard, SyncStatusBar, SyncProgressScreen, SyncErrorsScreen, MigrationBlockedScreen | src/screens/..., src/ui/... | Drop-in UI. |
Init pattern (from README):
final sdk = FrappeSDK(baseUrl: 'https://your-frappe-site.com/');
await sdk.initialize(true); // autoRestoreAndSync=true → restore session + initial sync
4. Auth flow
lib/src/services/auth_service.dart (~720 lines) — ALL login flows funnel through mobile_auth.* whitelisted endpoints on the server:
| Flow | Endpoint | Tokens persisted | SessionUser populated from |
|---|---|---|---|
| Username + password | mobile_auth.login | access_token (24h) + refresh_token (30d) | login response |
| Mobile OTP | mobile_auth.send_login_otp → verify_login_otp | same | login response |
| API key | loginWithApiKey | api_key + api_secret in secure storage | mobile_auth.me |
| OAuth 2.0 + PKCE | OAuth2Helper.exchangeCodeForToken (full flow) | bearer + refresh + clientId/clientSecret | mobile_auth.me |
| Social login (Google etc.) | mobile_auth.get_social_login_providers → get_social_authorize_url → OAuth | same as OAuth | mobile_auth.me |
Refresh: mobile_auth.refresh_token rotates the refresh token on every refresh (30-day rolling window). The offline_enabled flag is only updated on full login, not refresh.
Storage: flutter_secure_storage for tokens + API keys + OAuth credentials. auth_token table mirrors for SDK use. No biometric layer in the SDK itself — wire local_auth in the consumer app if needed.
Multi-account: not first-class. Logout fully wipes session; AtomicWipe (lib/src/services/atomic_wipe.dart) is used during force-logout, schema reset, and offline→online drain.
5. Doctype binding & form rendering
No code generation. Every Doctype is pulled at runtime as DocTypeMeta (lib/src/models/doc_type_meta.dart) and rendered by FrappeFormBuilder (lib/src/ui/form_renderer_helper.dart + per-field widgets in lib/src/ui/widgets/fields/).
Field-type → widget mapping lives in lib/src/database/field_type_mapping.dart and lib/src/constants/field_types.dart. Documented in doc/FIELD_TYPES.md:
| Frappe fieldtype | Widget | Notes |
|---|---|---|
Data, Small Text, Text, Long Text | TextField variants | per-fieldtype lengths, regex from server validators |
Int, Float, Currency, Percent | NumericField | decimal precision honoured |
Date, Datetime, Time | DateField | min/max from mandatory_depends_on etc. |
Check | CheckField | bool with proper 1/0 emission |
Select | SelectField | inline options |
Link | LinkField (uses SearchableSelect) | resolves via LinkOptionService; supports server-side options + LinkFilterBuilder overrides |
Table MultiSelect | TableMultiSelectField | requires Link child + Option Master (the rel-mis anti-pattern, see §15) |
Attach, Attach Image | upload via AttachmentService | offline → pending_attachments |
Geolocation | GeolocationField | GeoJSON, reads from device GPS |
Phone | PhoneField | intl_phone_field |
Button | ButtonField | triggers server method |
Skip-logic / depends_on: DependsOnEvaluator (lib/src/utils/depends_on_evaluator.dart) interprets Frappe’s JS-flavoured expressions (eval:doc.field == "X", ===, !==, etc.). Recent commit 5bd0a93 added paren handling for depends_on. The SDK side handles all depends_on / mandatory_depends_on / read_only_depends_on — the consumer app does not implement skip logic.
fetch_from: declared on the Doctype field; the SDK auto-fetches the linked parent on Link selection. This is how the PM design sheet’s “Auto populate from Member Form” rows materialise — the consumer app does not wire prefill manually (cross-reference 02-reliance-frappe-context.md anti-pattern: “don’t add Dart handlers for geography”).
Computed fields: read-only, computed in server validate() + mirrored in client script. Validation _id "5" in the conversion skill (04-mform-to-frappe-skill.md §4.2) → these arrive as read_only=1 fields with eval: expressions.
6. Geography handling
The SDK auto-cascades geography Link chains based on fetch_from declarations on the parent Link’s target Doctype. There is no Dart-side handler to write — the rel-mis “Geography Column Break breaking GP autofill” and “double-clear” bugs documented in 02-reliance-frappe-context.md are exactly what happens when consumers try to override the cascade manually.
For Swasti (per feedback_swasti_rel_mis_reference.md and the PM design sheet’s “Lookup for the Member with this Name for the Location of the User”):
- The Member doctype carries a single Link to Geography (state/district/block/GP/village hierarchy).
- Child program forms have a
Linkto Member;fetch_frompulls geography fields automatically. - Surveyor’s location filtering on Member list = a
LinkFilterBuilderregistered onMobileHomeScreenfor the Member doctype, scoping bysession_user.user_defaults.locationor similar. - Do not lift rel-mis’s
User Program Assignment/UPA Geographyinfrastructure.
7. Offline / sync model — what om/offline_improvement adds
This branch is the second-generation offline-first layer. The first generation (the version rel-mis uses, ref 7d27451) had a simpler write-then-sync flow with known issues around concurrent push/pull, attachment relinking, and toggle/migration paths. The new branch ships in 6 phased commits (P1–P6, see git log):
| Commit | Phase | What it adds |
|---|---|---|
11aba92 | P1 | Persist server-driven offline_enabled flag → sdk_meta |
c9f751c | P2 | Gate read/write paths on the flag (online thin client mode) |
89036dc | P3 | Drain/wipe transition handler + UI when toggling modes |
162f238 | P4–P6 | Wire pull, push, write-path, UX scaffolding |
04c6674 | — | SyncMutex, push/pull coordination (SIG-2), dispose + SIG-1/9/12 fixes |
3a1c8ba / ceadfd8 | — | Replace orphan sentinel docname with v16 relink flow for child-row attachments |
5bd0a93 | — | UnifiedResolver for list reads, count API, depends_on parens |
Pull (offline mode)
PullEngine (lib/src/sync/pull_engine.dart) → PullPageFetcher → PullApply:
- Cursor-based pagination keyed on
(modified, name). Pages fetchedmodified asc, name asc. Persisted withcomplete:falsemarker; flips tocomplete:trueon the final short page. - Resume skips rows
<= cursorto avoid double-applying tie-group rows. - Batch pull via
pullSyncMany(doctypes:..., concurrency: 4)runs up to 45 doctypes through a 4-worker pool. Per-doctype failures don’t abort the batch. - Server endpoint:
mobile_sync.get_docs_with_children(infrappe-mobile-control). Accepts{doctype, names: [...]}(max 200/call) — returns parent + embedded child rows in one trip. Falls back to per-name GET if endpoint missing (slow).
Push (offline mode)
PushEngine → PayloadAssembler → ResponseWriteback → AttachmentPipeline:
- Outbox (
outboxtable,lib/src/database/daos/outbox_dao.dart) stores pending writes (create/update/delete) withmobile_uuidas the local primary key. - UUID → server-name resolution on push: server returns the assigned
name; SDK rewrites the local row + relinks dependents viaUUIDRewriter(lib/src/sync/uuid_rewriter.dart). - Three-way merge (
lib/src/sync/three_way_merge.dart) on conflict — base + local + remote → diff.ConflictAction.keepRemote/keepLocalAndRetry. - Delete cascade preview —
SyncController.previewDeleteCascadereturns a plan listingblockedByrows;acceptDeleteCascaderesets to pending and re-pushes. - SyncMutex (
lib/src/concurrency/sync_mutex.dart) prevents concurrent push/pull (SIG-2 fix) — push-then-pull semantics enforced per-cycle.
SyncController API
final ctrl = sdk.syncController;
await ctrl.syncNow(); // full pull + push cycle
await ctrl.pause(); await ctrl.resume();
await ctrl.cancelInitialSync();
await ctrl.retryRow(outboxId);
await ctrl.resolveConflict(outboxId, ConflictAction.keepRemote);
final plan = await ctrl.previewDeleteCascade(outboxId);
await ctrl.acceptDeleteCascade(outboxId);
final SyncState current = ctrl.state;
final Stream<SyncState> stream = ctrl.state$;
SyncState carries isPaused, isInitialSync, perDoctype: Map<String, DoctypeProgress>. Drives the SyncStatusBar / SyncProgressScreen / SyncErrorsScreen UI.
Triggers (corrected, per om/offline_improvement design spec — doc/OFFLINE_FIRST.md)
- Post-login
_initialMetaAndDataSync. - Post-save when online: app calls
pushSync(doctype:)afterOfflineRepository.createDocumentetc. - Manual user trigger from
SyncStatusBar. - No 5-minute background timer. No
OrderedSyncService— push order is application-controlled.
Local cache: SQLite via sqflite
- One DB per app (
AppDatabase,lib/src/database/app_database.dart). - System tables (always):
auth_tokens,doctype_meta,doctype_permission,sdk_meta,link_options,outbox,pending_attachments. - Per-doctype tables (offline mode only):
docs__<doctype>for parent +docs__<child_doctype>for each child table. Created on first pull viaSchemaApplier(lib/src/database/schema_applier.dart), idempotent — skips if exists. Max 7 indexes per table. Enforcesis_child_tableflag indoctype_meta. - Migrations:
lib/src/database/migrations/v1_to_v2.dart— runs once when an upgrade detects v1 schema. Requires network — throwsMigrationNeedsNetworkExceptionif offline; consumer showsMigrationBlockedScreenand retries.
8. Server-driven offline-mode toggle (P1+P2+P3)
doc/OFFLINE_MODE_TOGGLE.md is the canonical reference. Headline:
- Server has a
Mobile Configurationsingle Doctype with anoffline_enabledCheck field (default0). - Login responses (every flow, except token refresh) include the flag at the top level.
- SDK persists it to
sdk_meta; reads at nextinitialize(); immutable for the session. - A missing key → defaults to
false(online mode). Oldermobile_controlversions therefore get online-mode behaviour for free. - Switching modes mid-deployment triggers
OfflineTransitionServiceto drain pending writes (online→offline gives nothing to drain; offline→online uploads outbox and wipes per-doctype tables).
For Swasti: Decide upfront. The Swasti V3 plan implies offline-first (field workforce, often offline). Default the flag to 1 after the data-import pipeline lands; until then keep it 0 to avoid sync-engine churn while the schema is in flux.
9. Bulk fetch & frappe-mobile-control
Server endpoint mobile_sync.get_docs_with_children lives in the companion app frappe-mobile-control, branch om/mobile-sync-bulk-fetch. The SDK calls it from PullPageFetcher. Required for any doctype with child tables to pull efficiently — without it the SDK falls back to per-name fetch which is unusable at Swasti’s expected member counts (533+ per surveyor as of kickoff screenshot 31).
The companion app also provides:
mobile_control.attachment_relink.relink_mobile_files— needed to attach SDK-uploaded files to child-row Attach fields (parent Attach fields work via stock Frappe).mobile_auth.*whitelisted methods (login, OTP, refresh, permissions, translations, social login).Mobile ConfigurationDoctype (carriesoffline_enabled).
Swasti’s Frappe app must include frappe-mobile-control from the om/mobile-sync-bulk-fetch branch (which will merge to develop) — pinning here matters as much as on the SDK side.
10. File uploads / attachments
lib/src/services/attachment_pipeline.dart (referenced; full path under lib/src/sync/attachment_pipeline.dart).
Online: direct AttachmentService POST to /api/method/upload_file and link to the doctype.
Offline: queued in pending_attachments table — rows hold:
parent_uuid/parent_doctype— the row that owns the Attach field (parent OR child).top_parent_uuid/top_parent_doctype— the outbox row that carries it to the server (always the parent doc UUID, even for child-row attachments).local_path,file_name,mime_type,is_private,state(pending|uploading|done|failed), retry/error metadata.
Push flow: files upload before the parent doc is pushed → server returns file URLs → relink hook attaches files to child rows by mobile_uuid → parent doc push includes the resolved file refs.
Failure modes:
- Upload retries: 2s/5s/10s × 3 →
failed→ outbox row goesblocked. User reattaches; SDK clears the failed row, re-queues. - Parent push permanently fails: file row stays orphan, all
attached_to_*NULL. Frappe’s stockcleanup_unattached_filescron reaps on schedule. mobile_controlnot installed → child-row Attach fields stay orphan. Parent-only Attach still works.
11. Notifications / push
Not in the SDK. Wire FCM (or your preferred provider) in the consumer app. Follow-up reminders (per Swasti decision D6 — push notifications as paid feature enhancement) are a separate workstream — would integrate with FCM topic subscriptions keyed on user roles + due dates.
12. Audit log support (Swasti D3)
The SDK does not synthesize Frappe’s document change log into a structured audit-log Doctype. For Swasti’s “replace mForm self-linking with Frappe-native audit log” (D3), the audit-log Doctype lives server-side (per 02-reliance-frappe-context.md — rel-mis already has Submission Approval Log + workflow_utils.log_workflow_action). The SDK pulls it like any other Doctype and renders it via the standard list/form screens.
Implication for Swasti: the followup_log Doctype is built server-side; the Flutter app reads it through OfflineRepository like Member or any other doctype. No SDK changes needed.
13. Error handling / logging
- Typed exceptions:
FrappeException(base),AuthException,ApiException,NetworkException,ValidationException(lib/src/api/exceptions.dart). - API tracer (
api_tracer) for HTTP request/response logging in debug. - No bundled Sentry / Crashlytics — wire in the consumer app.
- Codeburn (org telemetry per the new memory entry) is not wired at SDK level — handled by the harness, not the runtime.
- Push errors:
PushError(lib/src/sync/push_error.dart) carriesoutbox_id,error_code,retryable,validation_payload— surfaced inSyncErrorsScreen.
14. Testing
test/ has ~30 test files covering:
- Concurrency:
concurrency_pool,connectivity_watcher,device_tier,isolate_parser,sync_mutex,write_queue. - Database:
app_database_v3,field_type_mapping,migrations/,normalize_for_search,schema_applier,sdk_meta_dao,sdk_meta_migration,table_name,wipe_offline_document_tables, all DAOs. - Models:
dep_graph,document,meta_diff,offline_mode,session_user. - Query:
filter_parser_basic/like/null_parity/or,query_builder. - Form:
depends_on_evaluator,form_builder_submit,mobile_home_screen,meta_service,migration_integration,frappe_sdk.
Patterns: heavy use of @visibleForTesting getters on FrappeSDK (e.g. offlineModeForTesting); SQL fakes via sqflite_common_ffi. widget_test.dart in example/ for the demo app. Swasti should add: full integration test against a staging Frappe instance running frappe-mobile-control.
15. Migration notes — what changed since rel-mis
rel-mis pins frappe-mobile-sdk ref 7d27451 (per its plan archives). Differences vs om/offline_improvement head 5bd0a93:
| Area | rel-mis (old) | om/offline_improvement (new) |
|---|---|---|
| Offline toggle | None — always offline-first | offline_enabled flag, online-mode is the new default for 2.1+ |
| Mode transitions | Manual, no drain | OfflineTransitionService with drain/wipe + MigrationBlockedScreen |
| Push/pull coordination | Race conditions on overlap | SyncMutex enforces single-cycle semantics (SIG-2) |
| Attachments (child rows) | Orphan sentinel docname | v16 relink flow via mobile_control.attachment_relink |
| List reads | Direct query of local table | UnifiedResolver (online passthrough OR offline cache, with background refresh) |
| Count API | None | New count operation in resolver |
depends_on parser | Substring-prone (== vs ===) | Strict-equality + paren handling |
| Hooks for app | FieldChangeHandler only | + LinkFilterBuilder (PR #35) |
SessionUser | Partial | Complete — populated for ALL auth paths via mobile_auth.me |
| Schema migration | v1 only | v1→v2 path, network-required, throws MigrationNeedsNetworkException |
| Version | 1.x | 2.1.0 |
Breaking for rel-mis if upgraded: the offline_enabled=false default. rel-mis assumes offline-first; upgrading without flipping the server flag would silently switch it to thin-client mode and drop the offline cache. Swasti starts fresh on 2.1, so not a concern — just be aware when reviewing rel-mis code patterns.
16. Known limitations / open issues (mined from commit msgs + branch docs)
- Token refresh does not re-emit
offline_enabled. The flag only updates on full login. If admin flips it server-side, users won’t pick up the change until next login — by design (in-session immutability). previewDeleteCascaderequires the dependency graph to be fresh. If the user makes local edits between preview and accept, the cascade plan may be stale.pullSyncManyconcurrency=4 is conservative. Tunable, but bumping past 4 has race-condition risk with link option caching (LinkOptionService).- SearchableSelect / TableMultiSelect —
disease_category-style child Doctypes need a Link child + Option Master populated server-side. The conversion skill (04-mform-to-frappe-skill.md) handles this; consumer code that hand-rolls Table MultiSelect will fail with silent ValidationError. SyncMutexdispose ordering — fixed in04c6674(SIG-1/9/12) but worth attention when adding new sync paths.- Attachments without
mobile_control— child-row Attach fields stay orphan. Hard requirement. - No biometric auth, no multi-account — wire in consumer app if needed.
17. How a Swasti Flutter form gets wired up
Worked example: 5.1 Health Screening form (V2 form 1010, redesigned per PM Health_Screening tab → ~138-row spec).
Server-side prerequisite (one-time)
- Conversion skill produces
health_screeningDoctype +.py+.js+.dart(4-files-in-parity, per04-mform-to-frappe-skill.md §1). - Skill encodes BMI/HB/BP/RBS auto-calc as
read_only=1fields witheval:expressions; closure rules become server-sidevalidate()raises. - Doctype is registered in
Mobile Configurationso it appears inmobile_form_namesfor the surveyor role.
App-side (no widget code beyond hooks)
// 1. Boot
final sdk = FrappeSDK(baseUrl: AppConfig.baseUrl);
await sdk.initialize(true);
// 2. Wrap app in guard
MaterialApp(
home: FrappeAppGuard(
baseUrl: AppConfig.baseUrl,
child: MobileHomeScreen(
sdk: sdk,
// Per-doctype hook: when 'member' link is picked on Health Screening,
// patch member-derived fields. (Almost never needed — fetch_from on
// the Doctype handles auto-populate. Use only when the design sheet
// says "Auto Calculate" or there's a derivation the server can't do.)
fieldChangeHandlerFor: (doctype) {
if (doctype == 'Health Screening') {
return (snapshot, fieldname) {
// example: derive age from dob (only if not server-side)
return null;
};
}
return null;
},
// Filter Member dropdown to current surveyor's location
linkFilterBuilderFor: (doctype) {
if (doctype == 'Health Screening') {
return (lookup, fieldname, formData) {
if (fieldname == 'member') {
return LinkFilterResult.replace([
['location', '=', sdk.sessionUser?.userDefaults?['location']],
]);
}
return null; // SDK uses meta filters
};
}
return null;
},
),
),
);
What Swasti developers do NOT write
- Form widgets (the SDK renders from Doctype meta).
- Skip-logic dispatchers (
DependsOnEvaluatordoes it). - Geography cascade handlers (
fetch_fromdoes it). - Sync triggers (sync runs post-login + post-save).
- Outbox / conflict UI (
SyncStatusBar/SyncProgressScreen/SyncErrorsScreenare drop-in). - Login screen (
LoginScreenfrom the SDK; configurable styles perdoc/CUSTOMIZATION.md).
What Swasti developers DO write
- Theme / branding / config (
example/lib/config/app_config.darttemplate). - Per-doctype hooks (
LinkFilterBuilder,FieldChangeHandler) only when the server-side / fetch_from / depends_on aren’t enough. - The Livelihood Doctype Dart-side companion file (one of the 4-files-in-parity) — but the conversion skill generates it; manual touches go in only for genuinely Dart-only logic.
- FCM notification glue (Swasti push-notifications feature).
18. Open questions for SDK author (Om / Sunandan)
offline_enabled=falseas 2.1 default — for Swasti’s offline-heavy field workforce, should we ship withoffline_enabled=truefrom day 1, or stage in via online-mode first then flip after the import pipeline stabilises?SyncMutexgranularity — is it global, per-doctype, or per-connection? Concurrency limits forpullSyncManyinterplay with this.MigrationNeedsNetworkExceptionretry UX — doesMigrationBlockedScreenauto-retry on connectivity recovery, or does the consumer poll? (Code reading suggests user-triggered retry only.)v1_to_v2.dartmigration — Swasti starts at v2 fresh, so no migration runs. Confirm there’s no half-state where a fresh install touches the v1 path.- Translation pull cadence —
mobile_auth.get_translationsis pulled once on login. If A14a (Swasti Kannada/Telugu) ships post-launch, do users need to log out / log in to get new strings? - Bulk fetch limit —
mobile_sync.get_docs_with_childrenaccepts max 200 names per call. Documented; is the SDK already chunking, or does the consumer app need to? LinkFilterBuilderprecedence withlink_filtersJSON-in-JSON escaping — rel-mis paid for this anti-pattern. Has the new branch added a guard / lint?closedItselfimport — when we collapse mForm 1011 self-linked chains into single records + audit-log entries, is there an SDK-side helper to backfill audit-log rows, or does the import pipeline write them directly to the server?- Geography hierarchy for Swasti — design says “single Link to Geography on Member”, which then fetches state/district/block/GP/village via
fetch_from. Confirm the cascade depth limit (rel-mis hit the “Geography Column Break breaking GP autofill” — is the fix in 2.1?). - Workflow doctype detection (
doc/WORKFLOWS.md) — does it auto-detect any Doctype with aworkflow_statefield, or does the Doctype need to be flagged?
19. References
- Branch:
~/Dhwani/swasti-mform-migration/raw/sdk/frappe-mobile-sdk/@om/offline_improvement(head5bd0a93) - Canonical docs in the SDK repo:
doc/OFFLINE_FIRST.md— sync architecturedoc/OFFLINE_MODE_TOGGLE.md— server-driven flagdoc/FIELD_TYPES.md— widget mappingdoc/LINK_FILTER_BUILDER.md— runtime Link filter hookdoc/FIELD_CHANGE_HANDLER.md— patch-map hookdoc/WORKFLOWS.md— workflow detectiondoc/CUSTOMIZATION.md— theming + screen overridesdoc/SETUP.md,doc/QUICK_TEST.md,doc/TESTING.md,doc/DOCUMENTATION.md
- Server companion:
frappe-mobile-controlbranchom/mobile-sync-bulk-fetch(will merge todevelop) - Cross-references in this kickoff:
02-reliance-frappe-context.md— rel-mis Frappe shape; SDK anti-patterns from CODEBASE_GUIDE04-mform-to-frappe-skill.md— Doctype + .py + .js + .dart generation, validation_idtable05-pm-design-doc.md— design authority (HS form, Livelihood, BP New Logic)feedback_swasti_rel_mis_reference.md— don’t lift UPA/multi-geography model