Skip to main content

09 — Field-work features and the form logic / expression engine

Research date: 2026-08-15. Scope: implementation options for the Rasd Forms runtime — the expression/logic engine, dependency-graph recomputation, ODK-style bind semantics, repeats, validation timing, field-capture widgets (GPS, photo, barcode, signature, audio, files), large choice lists, longitudinal/entity data, form versioning, audit logs, submission encryption, humanitarian data-protection frameworks and low-literacy accessibility. All package versions and prices were checked against primary sources (npm registry, official docs, specs) on the research date unless flagged "unverified".

Summary

  1. The de-facto standard for humanitarian field forms is the ODK XForms spec (XLSForm authoring). Its bind semantics — relevant, required, constraint, calculate, readonly, jr:requiredMsg/jr:constraintMsg, saveIncomplete, trigger, once() — are precisely specified and battle-tested; Rasd Forms should copy these semantics even if it does not adopt XPath syntax [1][7].
  2. Two open-source ODK XPath engines exist: openrosa-xpath-evaluator 3.2.0 (Apache-2.0, ~9 kB gz, needs a browser DOM/XPath) [12][19] and the newer @getodk/xpath 1.0.0 + @getodk/xforms-engine 1.0.2 (Apache-2.0; custom evaluator; engine reactivity built on solid-js) [14][15][16]. Neither runs natively on React Native without a DOM shim; the ODK engine's Web Forms client also does not support encryption, draft saving or offline entities today [16].
  3. Among generic JS expression engines, the safest and smallest are json-logic-js 2.0.5 (MIT, 1.6 kB gz, JSON AST, ports in 8 languages, no eval) [19][24] and jexl 2.3.0 (MIT, 5.3 kB gz, interpreter, filters + transforms, sync/async) [19][21]; jsonata 2.2.2 (24.6 kB gz) and @marcbachmann/cel-js 8.0.0 (25 kB gz) are more powerful but heavier [19][20][25]. expr-eval should be excluded: three advisories in 2025–2026 (CVE-2025-12735 High, CVE-2025-13204 High, CVE-2026-12866 Critical, code execution via toJSFunction()) [23]. filtrex 3.1.0 compiles expressions to a JavaScript function at runtime, which conflicts with strict CSP [22].
  4. Recommended engine: a purpose-built, dependency-free "Rasd Expression Language" — XLSForm-flavoured infix syntax (${field} references, ODK function names, ./.. context) parsed by a small Pratt parser into a JSON-Logic-style AST that is stored in the form JSON, interpreted (no eval) on web and native, and easy to re-implement server-side. Static reference extraction gives the dependency graph; a signals-style reactive scheduler does incremental recomputation.
  5. Validation timing: ODK evaluates constraints only when the value is non-empty and when the user advances a screen or finalizes [1][7]; SurveyJS defaults to checkErrorsMode: "onNextPage" and also offers onValueChanged/onComplete plus error/warning/info notification levels [18]; NN/g's evidence favours validating when the user leaves a field, not while typing [46]. Adopt "on-blur for the field + on-page-advance + on-finalize summary", with non-blocking warnings.
  6. GPS: browsers' watchPosition (Baseline since 2015; enableHighAccuracy, timeout, maximumAge; secure context) [28] and expo-location (SDK 57; Accuracy.High ≈ 10 m, Balanced ≈ 100 m default; mocked flag on Android) [29]. Copy ODK Collect's UX: auto-accept at ≤ 5 m (capture-accuracy), red warning above 100 m (warning-accuracy), manual map placement, geotrace/geoshape via tap/manual/automatic modes [9]. Offline basemaps: MapLibre GL JS + pmtiles protocol (PMTiles v3, single-file, range requests) on web [40]; @maplibre/maplibre-react-native 11.3.6 (RN ≥ 0.80, Expo ≥ 54, dev build) on native [41].
  7. Barcode: native BarcodeDetector is only ~76% global availability — Chrome/Edge "partial" (macOS-only on desktop; Android needs Google Play Services), Safari disabled by default through 26.5, Firefox absent [26][27]. Use it opportunistically and fall back to the barcode-detector 3.2.1 ponyfill (MIT, ZXing-C++ WASM via zxing-wasm 3.1.1; self-host the WASM for offline/CSP) [36]. html5-qrcode and @zxing/library are both in maintenance mode [34][35]. Native: expo-camera CameraView barcodeScannerSettings + launchScanner (iOS 16+ VisionKit / ML Kit) [30].
  8. Photos: prefer <input type="file" accept="image/*" capture> + browser-image-compression 2.0.2 (MIT; maxWidthOrHeight, maxSizeMB, preserveExif default false, web-worker) on web [37]; expo-camera.takePictureAsync({quality, exif}) / expo-image-picker on native (iOS camera returns no GPS EXIF) [30][31]. Adopt ODK's max-pixels idea with a default long edge of ~1280 px [9]. Audio budgets from ODK: voice-only 12.2 kbps AMR ≈ 5 MB/h, low 24 kbps AAC ≈ 11 MB/h, normal 64 kbps ≈ 30 MB/h [9]; web via MediaRecorder (Baseline 2021) [33], native via expo-audio (SDK 57, RecordingPresets) [32].
  9. Longitudinal data: ODK Entities (spec 2022.1.0 → 2025.1.0) define an entity list as a CSV/secondary instance with name (UUID), label, __version, and per-submission create/update, baseVersion, trunkVersion, branchId for offline conflict handling [2]. Audit logs (events, node, start/end epoch-ms, optional lat/lon/accuracy, old/new value, change-reason, user) [3][4] and per-submission envelope encryption (AES-256 key wrapped with RSA-2048 OAEP-SHA256; AES/CFB/PKCS7; .enc files; signature) [5][6] are specified and worth mirroring in modern WebCrypto form.
  10. Data protection: UN Personal Data Protection and Privacy Principles (HLCM, 11 Oct 2018; 10 principles) [42]; IASC Operational Guidance on Data Responsibility (Feb 2021, revised Apr 2023) and OCHA Data Responsibility Guidelines (Oct 2021, updated Jan 2025) with the Information Sharing Protocol as the most-adopted tool [45]; ICRC Handbook on Data Protection in Humanitarian Action, 3rd ed. 2024, stresses that consent is often not a valid legal basis in emergencies [43]; GDPR Art. 7 requires demonstrable, distinguishable, withdrawable consent [44]. Rasd Forms should ship a versioned consent question type, per-field sensitivity tags, retention/purge policies and audit — but not force "consent" as the only basis.

1. Expression / logic engine landscape

1.1 ODK XPath (the incumbent semantics)

The ODK XForms spec is a subset of W3C XForms 1.0 with OpenRosa extensions. Bind attributes: relevant (when false the node and descendants are removed from the submitted instance), required, readonly, constraint (evaluated only when the node is non-empty), calculate, saveIncomplete, jr:requiredMsg, jr:constraintMsg [1]. Function set (~70): if, coalesce, once, selected, selected-at, count-selected, jr:choice-name, pulldata, instance(), current(), indexed-repeat (up to 3 nesting levels), position(node?), randomize, checklist/weighted-checklist, regex, uuid, digest, date/time (today, now, format-date, decimal-date-time), math (pow, log10, round, …), geo (distance, area, geofence, intersects), jr:itext [1]. Repeats are sized by jr:count (must reference a primary-instance node) and templated with jr:template="" [1]. External secondary instances are CSV (jr://file-csv/, RFC 4180, UTF-8, header row) or GeoJSON (FeatureCollection of Point/LineString/Polygon mapped to geopoint/geotrace/geoshape) and are filtered with predicates such as instance('cities')/root/item[country=/data/country] [1].

ODK docs clarify runtime behaviour: every expression is re-evaluated when the form opens, a value changes, a repeat is added/removed, or the form is saved/finalized; dynamic defaults evaluate once at creation; trigger (Collect ≥ 1.24) forces recalculation when a referenced field changes even if it is not in the expression; when relevant turns false the value is retained in memory but excluded from the submission; reducing repeat_count does not delete instances; position() is 1-based; predicates like ${people}[age < 18]/pet_count are supported; large forms with sums over many repeat instances show noticeable re-evaluation overhead [7].

Engines:

  • openrosa-xpath-evaluator 3.2.0 (Apache-2.0, 25.8 kB min / 9.0 kB gz, no deps) wraps the browser's document.evaluate and expects Element, Node, XPathResult to exist; digest() requires node-forge [12][19]. Enketo (Core/Express/Transformer + this evaluator) is now maintained by the Kobo team since March 2025 after ODK stepped back in May 2024 [13].
  • @getodk/xpath 1.0.0 (Apache-2.0; deps crypto-js, @getodk/common) and @getodk/xforms-engine 1.0.2 (Apache-2.0; optional peer solid-js ^1.9.11, papaparse, temporal-polyfill; dual builds for plain and Solid environments) [14][15]. ODK Web Forms self-reports form logic 100 %, XPath functions 98 %, question types 91 %, appearances 47 %, and 0 % for grid layouts, multi-page, offline entities, submission encryption, draft saving and theming; the repo has moved into the Central Frontend repo, and Web Forms becomes the default for new forms with Central v2026.2 (June 2026) [16]. Server-side rendering "is not presently supported or targeted beyond testing" and Node is used only with jsdom [16] — i.e. React Native support would need a DOM shim (unverified whether it works).

1.2 SurveyJS expressions

Syntax: {question}, {panel.q}, {matrix[0].col}, {row.col}, {-1}, {$survey.prop}, {$self.prop}; operators = <> > < >= <= contains notcontains anyof noneof allof empty notempty and or !; 30+ functions (iif, age, today, dateAdd, dateDiff, sum, avg, sumInArray, countInArray, displayValue, isContainerReady…); custom sync/async functions via FunctionFactory.Instance.register; expression-bearing properties visibleIf, enableIf, requiredIf, setValueIf, setValueExpression, defaultValueExpression, resetValueIf, choicesVisibleIf, choicesEnableIf, rowsVisibleIf, expression validators, calculatedValues, triggers; expressions are parsed at init and re-evaluated whenever referenced values change, some triggers only on page transition [17]. Validation: checkErrorsMode onNextPage (default) / onValueChanged / onComplete, textUpdateMode: "onTyping", validators numeric/text/email/expression/answercount/regex, notificationType error/warning/info, onServerValidateQuestions async, validationAllowSwitchPages [18]. The whole survey-core 3.0.0 is 1.4 MB min / 308 kB gz [19] — the expression engine is not separable and the license is commercial for the builder.

1.3 Generic JS expression engines (all figures from bundlephobia/npm, 2026-08-15)

EngineVersionLicenseMin / gzDepsModelSafetyExtensibilityNotes
json-logic-js2.0.5MIT5.1 kB / 1.6 kB0JSON AST interpreted"We never eval()"; no loops/functions/stateadd_operationPorts in JS, PHP, Python, Ruby, Go, Java, .NET, C++ [24]
jexl2.3.0MIT19.4 kB / 5.3 kB1 (@babel/runtime)Infix; AST interpreterInterpreter, no eval; prototype hardening not documentedaddTransform, addFunction, addBinaryOp; sync/async; compile() cache [21]Filters arr[.age>=30], transforms `x
filtrex3.1.0MIT26.6 kB / 7.7 kB0Compiles to a JS function at runtimeSandboxed by whitelist, but runtime code generation ⇒ likely needs unsafe-eval under CSP (unverified) [22]extraFunctions, customPropFilter-language feel
expr-eval2.0.2MIT25.2 kB / 7.6 kB0Math parserCVE-2025-12735, CVE-2025-13204, CVE-2026-12866 (critical) [23]functions in scopeDo not use
jsonata2.2.2MIT79.6 kB / 24.6 kB0XPath-3.1-inspired query/transform, HOFs, lambdasNo JS eval (has its own $eval); v2 evaluate is asyncregisterFunction [20]Powerful; heavy for a low-end-Android form engine
@marcbachmann/cel-js8.0.0MIT89.4 kB / 25.0 kB0CEL AST interpreter, macros has/all/exists/map/filter, timestamps/durationsNo eval; typedregisterFunction, registerVariable [25]~10× faster than cel-js; Google-portable semantics
openrosa-xpath-evaluator3.2.0Apache-2.025.8 kB / 9.0 kB0 (+node-forge for digest)Browser XPath + OpenRosa fnsBrowser DOM only [12]XLSForm-exact
@getodk/xpath1.0.0Apache-2.0n/a2Custom XPath 1.0 + ODKInterpreterNode ≥ 24 to build [14]

Cross-language portability matters for UN back-ends (Python/Java/PHP): only JSON Logic and CEL have wide server-side implementations; XPath is portable via libxml but not with ODK extensions.

1.4 Recommendation for the engine

None of the off-the-shelf engines gives (a) XLSForm-familiar authoring, (b) repeat/context semantics (./.., position(..), indexed-repeat), (c) tiny size, (d) strict no-eval, (e) server re-implementability, (f) React-Native compatibility. The pragmatic design is a two-layer engine (~6–10 kB gz target):

  • Authoring syntax "REL": XLSForm-style ${name} refs, . (current), .. (parent repeat), infix operators (+ - * div mod = != < <= > >= and or not), and the ODK function names (if, coalesce, once, selected, selected-at, count-selected, choice-name, regex, count, sum, min, max, position, indexed-repeat, today, now, format-date, decimal-date-time, distance, area, geofence, uuid, pulldata/lookup, instance). Hand-written Pratt parser; the builder shows a friendly formula editor.
  • Canonical form: JSON-Logic-inspired AST stored in the form definition ({"fn":"selected","args":[{"ref":"q1"},"yes"]}), versioned ("expr":1). Interpreted on web/RN, and re-implementable server-side in <1 kLOC. Optional xpath adapter (using @getodk/xpath in a Node/jsdom import pipeline) for XLSForm import of expressions the REL parser cannot express.

2. Dependency graph and reactive recomputation

  • Compile-time: extract static refs (${a}, ..) and function-level metadata (volatile: now(), random(), uuid(); once-only: once(); dataset reads: instance()/lookup). Build a DAG per bind property (calculate, relevant, required, readonly, constraint, choice_filter, repeat_count, default, label templates). Detect cycles at load and surface them in the builder (ODK tolerates none).
  • Repeats: keys are instance paths (hh[3].age); aggregate expressions depend on wildcard patterns (hh[*].age) that are re-bound when instances are added/removed; position(..), .., current() are resolved against the evaluation context; nested indices resolve like indexed-repeat (max 3 levels in ODK) [1][7].
  • Runtime: on change, mark dependents dirty, batch within a microtask, recompute in topological (Kahn) order, memoise unchanged results so downstream nodes stop early. ODK's own web engine reaches for solid-js fine-grained signals for exactly this [15][16]; a signals-style primitive (own or a small library) is UI-agnostic and shared by React and React Native. Dynamic dependencies (predicates evaluated over datasets) are tracked at runtime by recording every ref/instance read during evaluation.
  • Semantics to fix (ODK-compatible defaults, overridable): relevance false keeps the value in the draft but drops it from the submission and skips its constraint/required [7]; readonly questions still receive calculate; required and constraint are independent (constraint ignores empty) [1][7]; trigger re-fires calculates [7]; dynamic defaults and once() evaluate once [1][7]; repeat_count shrink hides rather than deletes [7].

3. Validation timing UX

ODK Collect validates on screen advance and finalize [7]; SurveyJS defaults to page-advance validation with optional immediate mode and non-blocking warnings [18]; NN/g advises validating once the user leaves a field, placing the message next to the field, never using a summary alone [46]. Recommended default: (1) constraint/required check on blur and on page-advance; (2) at finalize, run all, list errors with jump-to; (3) severity: "error" | "warning" per rule (warnings do not block, mirroring ODK's warning-accuracy and SurveyJS notificationType); (4) log constraint error events into the audit trail (ODK event) [4].

4. Field capture widgets

4.1 Geolocation

  • Web: navigator.geolocation.watchPosition(success, error, {enableHighAccuracy, timeout, maximumAge})watchID; errors PERMISSION_DENIED, POSITION_UNAVAILABLE, TIMEOUT; HTTPS + Permissions-Policy; Baseline since 2015 [28].
  • Native: expo-location (docs "latest" = SDK 57): Accuracy.Lowest ≈ 3 km, Low ≈ 1 km, Balanced ≈ 100 m (default), High ≈ 10 m, Highest, BestForNavigation; watchPositionAsync(options, cb) returns a subscription with remove(); LocationObject.coords has accuracy, altitude, altitudeAccuracy, heading, speed; mocked (Android) flags mock providers; mayShowUserSettingsDialog (Android) [29].
  • UX per ODK Collect: capture-accuracy (default 5 m auto-save; 0 = manual accept), warning-accuracy (default 100 m red warning, non-blocking), allow-mock-accuracy for external GPS, maps/placement-map appearances (long-press to place), geotrace/geoshape modes tap-to-place / manual reading / automatic interval with accuracy filter [9]. Store {lat, lon, alt, acc, ts, provider, mocked} and export ODK "lat lon alt acc" strings.
  • Offline maps: MapLibre GL JS + pmtiles (maplibregl.addProtocol("pmtiles", new Protocol().tile); register once at app init) with regional PMTiles v3 extracts hosted on object storage or bundled and cached [40]; native @maplibre/maplibre-react-native 11.3.6 (MIT; peers react-native >= 0.80, react >= 19.1, optional expo >= 54; requires a development build) with offline packs [41]. Native PMTiles support in MapLibre Native: unverified here — check per version.

4.2 Photo capture

  • Web: <input type="file" accept="image/*" capture="environment"> is the most robust path on Android/iOS browsers; getUserMedia for an in-app viewfinder. Compress with browser-image-compression 2.0.2 (MIT): maxSizeMB, maxWidthOrHeight, useWebWorker (default true), preserveExif (default false ⇒ GPS/orientation stripped unless you re-attach), initialQuality, fileType [37]. Because EXIF is dropped by default, capture the geotag separately from the geolocation service and store it as sidecar metadata (also better for privacy review).
  • Native: expo-camera CameraView.takePictureAsync({quality 0–1, base64, exif, additionalExif, skipProcessing, imageType(web)}){uri,width,height,exif?}; on web returns base64 [30]. expo-image-picker launchCameraAsync/launchImageLibraryAsync({mediaTypes, allowsEditing, quality (default 1.0), exif, base64, allowsMultipleSelection, selectionLimit, cameraType}){uri,width,height,fileSize,exif,mimeType,fileName}; iOS camera photos carry no GPS EXIF [31].
  • Budgets: mirror ODK max-pixels (proportional resize of the long edge) [9]; default 1280 px / JPEG q≈0.7 (~150–350 KB) with a per-submission attachment cap (e.g. 10 MB) so a submission still syncs on 2G/3G. Appearances to support: new, new-front, annotate, draw, signature [9].

4.3 Barcode / QR

  • BarcodeDetector (Shape Detection API): 13 formats (qr_code, code_128, ean_13, pdf417, data_matrix, aztec, itf, …); secure context; on Android needs Google Play Services; desktop support macOS only; check getSupportedFormats() at runtime [26]. caniuse: 76.3 % global usage; Chrome 83–154 / Edge 83–151 partial; Chrome for Android, Samsung Internet 13+, Android WebView supported; Safari & iOS Safari "disabled by default" through 26.5; Firefox not supported [27].
  • Fallback: barcode-detector 3.2.1 (MIT) ponyfill/polyfill on zxing-wasm 3.1.1; by default fetches WASM from jsDelivr — call prepareZXingModule to self-host for offline PWAs and CSP [36]. Avoid html5-qrcode (Apache-2.0, "maintenance mode until further notice", ZXing-js decoder) [34] and @zxing/library (Apache-2.0, "maintenance mode", recommends commercial STRICH) [35].
  • Native: expo-camera CameraView barcodeScannerSettings={{barcodeTypes:[...]}} + onBarcodeScanned({type,data,bounds,cornerPoints}); supported aztec, ean13, ean8, qr, pdf417, upc_e, datamatrix, code39, code93, itf14, codabar, code128, upc_a; Camera.scanFromURLAsync; launchScanner() native UI (iOS 16+ VisionKit, Android ML Kit); expo-barcode-scanner is superseded [30].

4.4 Signature, audio, files

  • Signature web: react-signature-canvas (Apache-2.0) wraps signature_pad; last stable 1.0.7 (Jan 2025), latest tag currently 1.1.0-alpha.2 (Mar 2025); peers React 0.14–19 [38]. Native: react-native-signature-canvas 5.1.1 (MIT; WebView-based, peer react-native-webview >= 13) [39]. Store trimmed PNG (~10–30 KB); ODK maps this to image + signature appearance [9].
  • Audio web: MediaRecorder (Baseline since Apr 2021; mimeType, audioBitsPerSecond, isTypeSupported, start(timeslice); webm/opus in Chrome/Firefox, Safari 14.1+ limited) [33]; Chrome WebM lacks duration metadata (Expo docs) [32]. Native: expo-audio (SDK 57) useAudioRecorder, RecordingPresets.HIGH_QUALITY (44.1 kHz, 2 ch, 128 kbps m4a) / LOW_QUALITY (64 kbps); custom RecordingOptions (sampleRate, numberOfChannels, bitRate, Android outputFormat/audioEncoder, iOS audioQuality); background recording drains battery [32]. Adopt ODK's quality ladder — voice-only 12.2 kbps AMR ≈ 5 MB/h, low 24 kbps AAC ≈ 11 MB/h, normal 64 kbps ≈ 30 MB/h — defaulting field forms to mono ~24–32 kbps [9].
  • Files: web <input type="file"> with type/size caps; native document picker; store blobs separately from answers (IndexedDB Blob store / SQLite + file system) and sync attachments as independent, resumable units.

5. Large choice lists and cascading selects

  • ODK: select_one_from_file/select_multiple_from_file over CSV/XML/GeoJSON with mandatory name and label columns; lookups instance("ds")/root/item[filter]/prop; choice_filter predicates for cascades; entity lists behave as datasets; "most modern mobile devices (2019+) handle datasets with 50,000+ elements" [8]. Widgets: search/autocomplete filtering by label, minimal, quick auto-advance, columns, no-buttons image grids [9]. Entity-list CSVs carry name, label, __version [2].
  • Design: keep large lists out of the form JSON as separate "dataset" resources (CSV/JSON, versioned, cached in IndexedDB object stores / SQLite tables with indexes on name and filter columns); virtualised search-select (debounced, ≥2 chars, RTL-aware, diacritic-insensitive Arabic matching); compile choice_filter to a REL AST evaluated lazily per row, or push simple equality filters to the store (WHERE parent = ?) for 10k+ rows; pulldata() becomes lookup(dataset, column, keyColumn, key).

6. Longitudinal data, versioning, audit, encryption

  • Entities: entities-version 2022.1.0 (create), 2023.1.0 (update), 2024.1.0 (offline: trunkVersion, branchId), 2025.1.0 (entities inside groups/repeats); <entity dataset id create update baseVersion trunkVersion branchId><label/>; entities:saveto binds fields to properties; reserved names name, label, __* [2]. Rasd Forms should model "entity lists" identically (UUID name, label, __version, properties) so ODK Central and Kobo can be back-ends and offline conflict handling follows base/trunk/branch versions.
  • Form versioning: Central requires the version attribute to change on republish (offers to bump automatically); published field types cannot change (except to text); attachments only in drafts; old submissions export unchanged [10]. Collect edits use "the version of the form that was active when the form was originally filled, NOT the latest one" [11]. Recommendation: pin each draft to its formVersion + definition hash, keep old definitions on device until their drafts are gone, allow opt-in "migrate draft" only for additive/label changes.
  • Audit log: orx:audit meta with parameters location-priority (no-power|low-power|balanced|high-accuracy), location-min-interval, location-max-age, track-changes, track-changes-reasons=on-form-edit, identify-user; CSV columns event,node,start,end (+ latitude,longitude,accuracy, old-value,new-value, change-reason, user); timestamps epoch-ms; events form start/exit/resume/save/finalize, question, group questions, jump, add repeat, delete repeat, end screen, save error, finalize error, constraint error, location tracking enabled/disabled; Central exports audits with submissions [3][4]. Rasd Forms should emit the same event vocabulary (JSONL locally, CSV on export).
  • Encryption: per-record random 256-bit AES key; contents and media encrypted with AES/CFB/PKCS7 (IV = MD5(instanceID + key) with an incrementing byte per file); key wrapped with RSA/NONE/OAEPWithSHA256AndMGF1Padding (2048-bit keys in the docs); manifest <data encrypted="yes"> with base64EncryptedKey, encryptedXmlFile, media/file, base64EncryptedElementSignature; files .enc; decryption only in Briefcase; server cannot view/edit; ODK Web Forms does not support it [5][6][16]. A modern Rasd equivalent: WebCrypto AES-256-GCM per submission, RSA-OAEP-SHA256 (or ECDH-P256 HPKE-style) key wrapping, same manifest concept; RN needs a WebCrypto shim (unverified which package) — ship as an optional module because it disables server-side viewing and Entities.
  • UN PDPP (HLCM, 11 Oct 2018): fair & legitimate processing, purpose specification, proportionality & necessity, retention, accuracy, confidentiality, security, transparency, transfers, accountability [42].
  • IASC OG on Data Responsibility (Feb 2021, revised Apr 2023) and OCHA Data Responsibility Guidelines (Oct 2021, updated Jan 2025); the Information Sharing Protocol is "the primary reference document governing data and information sharing in a response"; adopted in 21 HRP countries [45]. (The IASC/ReliefWeb pages returned 403 during this research; details taken from OCHA's Centre for Humanitarian Data.)
  • ICRC Handbook, 3rd ed. (2024): legal bases (consent, vital interest, public interest, legitimate interest), DPIAs, data sharing, cash, biometrics, messaging, cloud, drones, AI; consent has "significant limitations" in emergencies [43].
  • GDPR Art. 7: consent must be demonstrable, distinguishable, in plain language, as easy to withdraw as to give; Recitals 32/42/43 [44].
  • Product implications: a consent question type storing text version, language, timestamp, method (tap/signature/audio), and withdrawal; a form-level legalBasis field; per-field sensitivity tags driving encryption-at-rest, redaction in exports and screen masking; retention rules (purgeAfterSync, maxDraftAgeDays); audit trail; DPIA checklist in docs.
  • Low literacy: ODK attaches image/audio/video media to questions and choices, no-buttons image grids, columns, quick auto-advance, guidance hints and a built-in recorder that runs while other questions are answered [9]. Rasd Forms should support per-language media on labels/choices, an auto-play audio prompt, icon-only choice grids, big touch targets, numeric keypads and RTL layout as first-class.

Comparison table — capture stack

CapabilityWeb (PWA)React Native (Expo SDK 57)Notes
GPSwatchPosition [28]expo-location watchPositionAsync [29]ODK thresholds 5 m / 100 m [9]
Photo<input capture> + browser-image-compression 2.0.2 [37]expo-camera / expo-image-picker [30][31]strip EXIF by default; sidecar geotag
BarcodeBarcodeDetectorbarcode-detector 3.2.1 [26][27][36]CameraView scanner / launchScanner [30]avoid html5-qrcode, zxing-js [34][35]
Signaturereact-signature-canvas 1.0.7 / signature_pad [38]react-native-signature-canvas 5.1.1 [39]PNG, trimmed
AudioMediaRecorder [33]expo-audio [32]ODK bitrate ladder [9]
Offline mapMapLibre GL JS + pmtiles [40]@maplibre/maplibre-react-native 11.3.6 [41]PMTiles v3

Implications & recommendations for Rasd Forms

  1. Build a dependency-free "REL" expression language: XLSForm-style syntax + ODK function names, Pratt parser → versioned JSON AST stored in the form definition; interpreter only (no eval/new Function), prototype-safe property access, iteration/recursion caps. Target ≤ 10 kB gz including the standard library.
  2. Do not adopt expr-eval (three CVEs) or filtrex (runtime code generation); if you want a stop-gap before REL ships, jexl 2.3.0 with a curated function set is the least-risk infix option and json-logic-js the least-risk serialized option [19][21][23][24].
  3. Copy ODK bind semantics exactly (relevant/required/constraint/calculate/readonly/trigger/once/dynamic defaults; constraint skips empty; relevant-false values kept in draft but excluded from submission; repeat_count shrink hides) so XLSForm importers and ODK/Kobo back-ends work [1][7].
  4. Implement a compile-time dependency DAG with cycle detection surfaced in the builder, plus runtime tracking for dynamic dataset predicates; recompute incrementally in topological order, batched per microtask, with wildcard repeat dependencies. Consider a signals primitive shared by React and RN (ODK's engine uses solid-js) [15][16].
  5. Provide xlsform-import as a separate package that maps XLSForm columns to the JSON payload and uses @getodk/xpath (Apache-2.0) in Node for expressions REL cannot parse; keep the runtime free of XPath/DOM to stay React-Native-clean [12][14].
  6. Validation: on-blur + page-advance + finalize summary with jump-to; error/warning severities; log constraint error audit events; expose checkErrorsMode-like config for teams that prefer SurveyJS behaviour [4][18][46].
  7. Geopoint widget: live accuracy readout, auto-accept ≤ 5 m, warn > 100 m, manual map placement, mock-location flag, timeout/fallback; geotrace/geoshape with tap/manual/auto modes; ship distance/area/geofence functions; PMTiles-based offline basemaps on both platforms [9][28][29][40][41].
  8. Photo pipeline: capture → resize (default long edge 1280 px, ODK max-pixels equivalent) → JPEG q≈0.7 → strip EXIF, keep a sidecar {capturedAt, geo} → attachment store; per-form and per-submission size budgets; new-front, annotate, draw, signature appearances [9][30][31][37].
  9. Barcode: feature-detect BarcodeDetector, fall back to barcode-detector with self-hosted WASM (prepareZXingModule) so scanning works offline and under strict CSP; native via expo-camera [26][27][30][36].
  10. Audio: default mono ~24–32 kbps AAC/Opus (ODK "low"), background-record option, show size estimate; web via MediaRecorder with isTypeSupported negotiation; native via expo-audio custom RecordingOptions [9][32][33].
  11. Datasets/choice lists: separate versioned dataset resources (CSV/JSON/GeoJSON) cached in IndexedDB/SQLite with indexes; virtualised search-select; store-side filtering for 10k+ rows; choice_filter compiled to REL; lookup() = pulldata(); support ODK entity-list CSV shape (name,label,__version) [2][8][9].
  12. Entities/case management: model entity lists with UUID name, label, __version, properties; forms declare create/update with baseVersion/trunkVersion/branchId; resolve conflicts offline like ODK 2024.1.0 [2].
  13. Versioning: formVersion + content hash on every draft/submission; keep old definitions on device; block type changes on published fields (Central rule); opt-in additive draft migration [10][11].
  14. Audit: emit ODK's event vocabulary and column set (epoch-ms, node path, lat/lon/acc, old/new, change-reason, user) as JSONL, export CSV; make track-changes and identify-user builder toggles [3][4].
  15. Encryption (optional module): per-submission AES-256-GCM key, RSA-OAEP-SHA256 wrapped, manifest + signature akin to ODK; document that encrypted data cannot be viewed/edited server-side or used with entities [5][6].
  16. Data-protection features by default: consent question type (versioned text, language, timestamp, method, withdrawal), form-level legal basis, per-field sensitivity tags → encryption/redaction/masking, retention & purge rules, DPIA checklist and ISP-alignment guidance in docs [42][43][44][45].
  17. Low-literacy/RTL: per-language image/audio on labels and choices with auto-play, icon-only choice grids, quick advance, large hit targets, numeric keypads, Arabic-aware search; test on low-end Android (WebView) with the polyfills above [9][27].

Sources

  1. ODK XForms Specification — https://getodk.github.io/xforms-spec/ (accessed 2026-08-15)
  2. ODK XForms Entities sub-spec — https://getodk.github.io/xforms-spec/entities (accessed 2026-08-15)
  3. ODK XForms Client Audit Logs — https://getodk.github.io/xforms-spec/client-audit-logs.html (accessed 2026-08-15)
  4. ODK Docs, Form Audit Log — https://docs.getodk.org/form-audit-log/ (accessed 2026-08-15)
  5. ODK XForms Encryption sub-spec — https://getodk.github.io/xforms-spec/encryption (accessed 2026-08-15)
  6. ODK Docs, Encrypted Forms — https://docs.getodk.org/encrypted-forms/ (accessed 2026-08-15)
  7. ODK Docs, Form Logic — https://docs.getodk.org/form-logic/ (accessed 2026-08-15)
  8. ODK Docs, Form Datasets — https://docs.getodk.org/form-datasets/ (accessed 2026-08-15)
  9. ODK Docs, Form Question Types — https://docs.getodk.org/form-question-types/ (accessed 2026-08-15)
  10. ODK Docs, Managing Forms in Central — https://docs.getodk.org/central-forms/ (accessed 2026-08-15)
  11. ODK Docs, Collect Forms — https://docs.getodk.org/collect-forms/ (accessed 2026-08-15)
  12. enketo/openrosa-xpath-evaluator (GitHub/npm) — https://github.com/enketo/openrosa-xpath-evaluator (accessed 2026-08-15)
  13. enketo/enketo monorepo README — https://github.com/enketo/enketo (accessed 2026-08-15)
  14. npm registry, @getodk/xpath — https://registry.npmjs.org/@getodk/xpath/latest (accessed 2026-08-15)
  15. npm registry, @getodk/xforms-engine — https://registry.npmjs.org/@getodk/xforms-engine/latest (accessed 2026-08-15)
  16. getodk/web-forms README — https://github.com/getodk/web-forms (accessed 2026-08-15)
  17. SurveyJS, Conditional Logic and Dynamic Texts — https://surveyjs.io/form-library/documentation/design-survey/conditional-logic (accessed 2026-08-15)
  18. SurveyJS, Data Validation — https://surveyjs.io/form-library/documentation/data-validation (accessed 2026-08-15)
  19. Bundlephobia API (jsonata, jexl, json-logic-js, filtrex, expr-eval, openrosa-xpath-evaluator, survey-core, @marcbachmann/cel-js) — https://bundlephobia.com/api/size?package=jsonata (etc.) (accessed 2026-08-15)
  20. JSONata docs — https://docs.jsonata.org/overview.html (accessed 2026-08-15)
  21. TomFrost/Jexl — https://github.com/TomFrost/Jexl (accessed 2026-08-15)
  22. joewalnes/filtrex — https://github.com/joewalnes/filtrex (accessed 2026-08-15)
  23. GitHub Advisory Database, expr-eval — https://github.com/advisories?query=expr-eval (accessed 2026-08-15)
  24. JsonLogic — https://jsonlogic.com/ (accessed 2026-08-15)
  25. marcbachmann/cel-js — https://github.com/marcbachmann/cel-js (accessed 2026-08-15)
  26. MDN, Barcode Detection API — https://developer.mozilla.org/en-US/docs/Web/API/Barcode_Detection_API (accessed 2026-08-15)
  27. Can I use, BarcodeDetector API — https://caniuse.com/mdn-api_barcodedetector (accessed 2026-08-15)
  28. MDN, Geolocation.watchPosition — https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/watchPosition (accessed 2026-08-15)
  29. Expo docs, Location (SDK 57) — https://docs.expo.dev/versions/latest/sdk/location/ (accessed 2026-08-15)
  30. Expo docs, Camera (SDK 57) — https://docs.expo.dev/versions/latest/sdk/camera/ (accessed 2026-08-15)
  31. Expo docs, ImagePicker — https://docs.expo.dev/versions/latest/sdk/imagepicker/ (accessed 2026-08-15)
  32. Expo docs, Audio (expo-audio, SDK 57) — https://docs.expo.dev/versions/latest/sdk/audio/ (accessed 2026-08-15)
  33. MDN, MediaStream Recording API — https://developer.mozilla.org/en-US/docs/Web/API/MediaStream_Recording_API (accessed 2026-08-15)
  34. mebjas/html5-qrcode — https://github.com/mebjas/html5-qrcode (accessed 2026-08-15)
  35. zxing-js/library — https://github.com/zxing-js/library (accessed 2026-08-15)
  36. Sec-ant/barcode-detector + npm — https://github.com/Sec-ant/barcode-detector ; https://registry.npmjs.org/barcode-detector/latest (accessed 2026-08-15)
  37. Donaldcwl/browser-image-compression + npm — https://github.com/Donaldcwl/browser-image-compression ; https://registry.npmjs.org/browser-image-compression/latest (accessed 2026-08-15)
  38. npm registry, react-signature-canvas — https://registry.npmjs.org/react-signature-canvas (accessed 2026-08-15)
  39. npm registry, react-native-signature-canvas — https://registry.npmjs.org/react-native-signature-canvas/latest (accessed 2026-08-15)
  40. Protomaps PMTiles docs + MapLibre guide — https://docs.protomaps.com/pmtiles/ ; https://docs.protomaps.com/pmtiles/maplibre (accessed 2026-08-15)
  41. @maplibre/maplibre-react-native (npm + GitHub) — https://registry.npmjs.org/@maplibre/maplibre-react-native/latest ; https://github.com/maplibre/maplibre-react-native (accessed 2026-08-15)
  42. UN System CEB, Personal Data Protection and Privacy Principles — https://unsceb.org/privacy-principles (accessed 2026-08-15)
  43. ICRC, Handbook on Data Protection in Humanitarian Action, 3rd ed. — https://www.icrc.org/en/data-protection-humanitarian-action-handbook (accessed 2026-08-15)
  44. GDPR Art. 7 (gdpr-info.eu) — https://gdpr-info.eu/art-7-gdpr/ (accessed 2026-08-15)
  45. OCHA Centre for Humanitarian Data, Data Responsibility — https://centre.humdata.org/data-responsibility/ (accessed 2026-08-15; IASC and ReliefWeb pages returned HTTP 403)
  46. Nielsen Norman Group, Error-Message Guidelines for Forms — https://www.nngroup.com/articles/errors-forms-design-guidelines/ (accessed 2026-08-15)