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
- 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]. - Two open-source ODK XPath engines exist:
openrosa-xpath-evaluator3.2.0 (Apache-2.0, ~9 kB gz, needs a browser DOM/XPath) [12][19] and the newer@getodk/xpath1.0.0 +@getodk/xforms-engine1.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]. - Among generic JS expression engines, the safest and smallest are
json-logic-js2.0.5 (MIT, 1.6 kB gz, JSON AST, ports in 8 languages, no eval) [19][24] andjexl2.3.0 (MIT, 5.3 kB gz, interpreter, filters + transforms, sync/async) [19][21];jsonata2.2.2 (24.6 kB gz) and@marcbachmann/cel-js8.0.0 (25 kB gz) are more powerful but heavier [19][20][25].expr-evalshould be excluded: three advisories in 2025–2026 (CVE-2025-12735 High, CVE-2025-13204 High, CVE-2026-12866 Critical, code execution viatoJSFunction()) [23].filtrex3.1.0 compiles expressions to a JavaScript function at runtime, which conflicts with strict CSP [22]. - 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 (noeval) 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. - 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 offersonValueChanged/onCompleteplus 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. - GPS: browsers'
watchPosition(Baseline since 2015;enableHighAccuracy,timeout,maximumAge; secure context) [28] andexpo-location(SDK 57;Accuracy.High≈ 10 m,Balanced≈ 100 m default;mockedflag 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 +pmtilesprotocol (PMTiles v3, single-file, range requests) on web [40];@maplibre/maplibre-react-native11.3.6 (RN ≥ 0.80, Expo ≥ 54, dev build) on native [41]. - Barcode: native
BarcodeDetectoris 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 thebarcode-detector3.2.1 ponyfill (MIT, ZXing-C++ WASM viazxing-wasm3.1.1; self-host the WASM for offline/CSP) [36].html5-qrcodeand@zxing/libraryare both in maintenance mode [34][35]. Native:expo-cameraCameraViewbarcodeScannerSettings+launchScanner(iOS 16+ VisionKit / ML Kit) [30]. - Photos: prefer
<input type="file" accept="image/*" capture>+browser-image-compression2.0.2 (MIT;maxWidthOrHeight,maxSizeMB,preserveExifdefault false, web-worker) on web [37];expo-camera.takePictureAsync({quality, exif})/expo-image-pickeron native (iOS camera returns no GPS EXIF) [30][31]. Adopt ODK'smax-pixelsidea 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 viaexpo-audio(SDK 57,RecordingPresets) [32]. - 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-submissioncreate/update,baseVersion,trunkVersion,branchIdfor offline conflict handling [2]. Audit logs (events,node,start/endepoch-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;.encfiles; signature) [5][6] are specified and worth mirroring in modern WebCrypto form. - 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-evaluator3.2.0 (Apache-2.0, 25.8 kB min / 9.0 kB gz, no deps) wraps the browser'sdocument.evaluateand expectsElement,Node,XPathResultto exist;digest()requiresnode-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/xpath1.0.0 (Apache-2.0; depscrypto-js,@getodk/common) and@getodk/xforms-engine1.0.2 (Apache-2.0; optional peersolid-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)
| Engine | Version | License | Min / gz | Deps | Model | Safety | Extensibility | Notes |
|---|---|---|---|---|---|---|---|---|
json-logic-js | 2.0.5 | MIT | 5.1 kB / 1.6 kB | 0 | JSON AST interpreted | "We never eval()"; no loops/functions/state | add_operation | Ports in JS, PHP, Python, Ruby, Go, Java, .NET, C++ [24] |
jexl | 2.3.0 | MIT | 19.4 kB / 5.3 kB | 1 (@babel/runtime) | Infix; AST interpreter | Interpreter, no eval; prototype hardening not documented | addTransform, addFunction, addBinaryOp; sync/async; compile() cache [21] | Filters arr[.age>=30], transforms `x |
filtrex | 3.1.0 | MIT | 26.6 kB / 7.7 kB | 0 | Compiles to a JS function at runtime | Sandboxed by whitelist, but runtime code generation ⇒ likely needs unsafe-eval under CSP (unverified) [22] | extraFunctions, customProp | Filter-language feel |
expr-eval | 2.0.2 | MIT | 25.2 kB / 7.6 kB | 0 | Math parser | CVE-2025-12735, CVE-2025-13204, CVE-2026-12866 (critical) [23] | functions in scope | Do not use |
jsonata | 2.2.2 | MIT | 79.6 kB / 24.6 kB | 0 | XPath-3.1-inspired query/transform, HOFs, lambdas | No JS eval (has its own $eval); v2 evaluate is async | registerFunction [20] | Powerful; heavy for a low-end-Android form engine |
@marcbachmann/cel-js | 8.0.0 | MIT | 89.4 kB / 25.0 kB | 0 | CEL AST interpreter, macros has/all/exists/map/filter, timestamps/durations | No eval; typed | registerFunction, registerVariable [25] | ~10× faster than cel-js; Google-portable semantics |
openrosa-xpath-evaluator | 3.2.0 | Apache-2.0 | 25.8 kB / 9.0 kB | 0 (+node-forge for digest) | Browser XPath + OpenRosa fns | Browser DOM only [12] | — | XLSForm-exact |
@getodk/xpath | 1.0.0 | Apache-2.0 | n/a | 2 | Custom XPath 1.0 + ODK | Interpreter | — | Node ≥ 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. Optionalxpathadapter (using@getodk/xpathin 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,labeltemplates). 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 likeindexed-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/instanceread 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];
readonlyquestions still receivecalculate;requiredandconstraintare independent (constraint ignores empty) [1][7];triggerre-fires calculates [7]; dynamic defaults andonce()evaluate once [1][7];repeat_countshrink 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; errorsPERMISSION_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 withremove();LocationObject.coordshasaccuracy,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-accuracyfor external GPS,maps/placement-mapappearances (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-native11.3.6 (MIT; peersreact-native >= 0.80,react >= 19.1, optionalexpo >= 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;getUserMediafor an in-app viewfinder. Compress withbrowser-image-compression2.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-cameraCameraView.takePictureAsync({quality 0–1, base64, exif, additionalExif, skipProcessing, imageType(web)})→{uri,width,height,exif?}; on web returns base64 [30].expo-image-pickerlaunchCameraAsync/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; checkgetSupportedFormats()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-detector3.2.1 (MIT) ponyfill/polyfill onzxing-wasm3.1.1; by default fetches WASM from jsDelivr — callprepareZXingModuleto self-host for offline PWAs and CSP [36]. Avoidhtml5-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-cameraCameraView barcodeScannerSettings={{barcodeTypes:[...]}}+onBarcodeScanned({type,data,bounds,cornerPoints}); supportedaztec, 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-scanneris superseded [30].
4.4 Signature, audio, files
- Signature web:
react-signature-canvas(Apache-2.0) wrapssignature_pad; last stable 1.0.7 (Jan 2025),latesttag currently 1.1.0-alpha.2 (Mar 2025); peers React 0.14–19 [38]. Native:react-native-signature-canvas5.1.1 (MIT; WebView-based, peerreact-native-webview >= 13) [39]. Store trimmed PNG (~10–30 KB); ODK maps this toimage+signatureappearance [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); customRecordingOptions(sampleRate,numberOfChannels,bitRate, AndroidoutputFormat/audioEncoder, iOSaudioQuality); background recording drains battery [32]. Adopt ODK's quality ladder —voice-only12.2 kbps AMR ≈ 5 MB/h,low24 kbps AAC ≈ 11 MB/h,normal64 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_fileover CSV/XML/GeoJSON with mandatorynameandlabelcolumns; lookupsinstance("ds")/root/item[filter]/prop;choice_filterpredicates for cascades; entity lists behave as datasets; "most modern mobile devices (2019+) handle datasets with 50,000+ elements" [8]. Widgets:search/autocompletefiltering by label,minimal,quickauto-advance,columns,no-buttonsimage grids [9]. Entity-list CSVs carryname,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
nameand filter columns); virtualised search-select (debounced, ≥2 chars, RTL-aware, diacritic-insensitive Arabic matching); compilechoice_filterto a REL AST evaluated lazily per row, or push simple equality filters to the store (WHERE parent = ?) for 10k+ rows;pulldata()becomeslookup(dataset, column, keyColumn, key).
6. Longitudinal data, versioning, audit, encryption
- Entities:
entities-version2022.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:savetobinds fields to properties; reserved namesname,label,__*[2]. Rasd Forms should model "entity lists" identically (UUIDname,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
versionattribute 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 itsformVersion+ 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:auditmeta with parameterslocation-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 columnsevent,node,start,end(+latitude,longitude,accuracy,old-value,new-value,change-reason,user); timestamps epoch-ms; eventsform 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">withbase64EncryptedKey,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.
7. Data protection, consent, low literacy
- 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
consentquestion type storing text version, language, timestamp, method (tap/signature/audio), and withdrawal; a form-levellegalBasisfield; per-fieldsensitivitytags 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-buttonsimage grids,columns,quickauto-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
| Capability | Web (PWA) | React Native (Expo SDK 57) | Notes |
|---|---|---|---|
| GPS | watchPosition [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 |
| Barcode | BarcodeDetector → barcode-detector 3.2.1 [26][27][36] | CameraView scanner / launchScanner [30] | avoid html5-qrcode, zxing-js [34][35] |
| Signature | react-signature-canvas 1.0.7 / signature_pad [38] | react-native-signature-canvas 5.1.1 [39] | PNG, trimmed |
| Audio | MediaRecorder [33] | expo-audio [32] | ODK bitrate ladder [9] |
| Offline map | MapLibre GL JS + pmtiles [40] | @maplibre/maplibre-react-native 11.3.6 [41] | PMTiles v3 |
Implications & recommendations for Rasd Forms
- 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. - Do not adopt
expr-eval(three CVEs) orfiltrex(runtime code generation); if you want a stop-gap before REL ships,jexl2.3.0 with a curated function set is the least-risk infix option andjson-logic-jsthe least-risk serialized option [19][21][23][24]. - 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].
- 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].
- Provide
xlsform-importas 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]. - Validation: on-blur + page-advance + finalize summary with jump-to;
error/warningseverities; logconstraint erroraudit events; exposecheckErrorsMode-like config for teams that prefer SurveyJS behaviour [4][18][46]. - 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/geofencefunctions; PMTiles-based offline basemaps on both platforms [9][28][29][40][41]. - Photo pipeline: capture → resize (default long edge 1280 px, ODK
max-pixelsequivalent) → JPEG q≈0.7 → strip EXIF, keep a sidecar{capturedAt, geo}→ attachment store; per-form and per-submission size budgets;new-front,annotate,draw,signatureappearances [9][30][31][37]. - Barcode: feature-detect
BarcodeDetector, fall back tobarcode-detectorwith self-hosted WASM (prepareZXingModule) so scanning works offline and under strict CSP; native viaexpo-camera[26][27][30][36]. - Audio: default mono ~24–32 kbps AAC/Opus (ODK "low"), background-record option, show size estimate; web via MediaRecorder with
isTypeSupportednegotiation; native viaexpo-audiocustomRecordingOptions[9][32][33]. - 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_filtercompiled to REL;lookup()=pulldata(); support ODK entity-list CSV shape (name,label,__version) [2][8][9]. - Entities/case management: model entity lists with UUID
name,label,__version, properties; forms declare create/update withbaseVersion/trunkVersion/branchId; resolve conflicts offline like ODK 2024.1.0 [2]. - 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]. - 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-changesandidentify-userbuilder toggles [3][4]. - 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].
- 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].
- Low-literacy/RTL: per-language image/audio on labels and choices with auto-play, icon-only choice grids,
quickadvance, large hit targets, numeric keypads, Arabic-aware search; test on low-end Android (WebView) with the polyfills above [9][27].
Sources
- ODK XForms Specification — https://getodk.github.io/xforms-spec/ (accessed 2026-08-15)
- ODK XForms Entities sub-spec — https://getodk.github.io/xforms-spec/entities (accessed 2026-08-15)
- ODK XForms Client Audit Logs — https://getodk.github.io/xforms-spec/client-audit-logs.html (accessed 2026-08-15)
- ODK Docs, Form Audit Log — https://docs.getodk.org/form-audit-log/ (accessed 2026-08-15)
- ODK XForms Encryption sub-spec — https://getodk.github.io/xforms-spec/encryption (accessed 2026-08-15)
- ODK Docs, Encrypted Forms — https://docs.getodk.org/encrypted-forms/ (accessed 2026-08-15)
- ODK Docs, Form Logic — https://docs.getodk.org/form-logic/ (accessed 2026-08-15)
- ODK Docs, Form Datasets — https://docs.getodk.org/form-datasets/ (accessed 2026-08-15)
- ODK Docs, Form Question Types — https://docs.getodk.org/form-question-types/ (accessed 2026-08-15)
- ODK Docs, Managing Forms in Central — https://docs.getodk.org/central-forms/ (accessed 2026-08-15)
- ODK Docs, Collect Forms — https://docs.getodk.org/collect-forms/ (accessed 2026-08-15)
- enketo/openrosa-xpath-evaluator (GitHub/npm) — https://github.com/enketo/openrosa-xpath-evaluator (accessed 2026-08-15)
- enketo/enketo monorepo README — https://github.com/enketo/enketo (accessed 2026-08-15)
- npm registry, @getodk/xpath — https://registry.npmjs.org/@getodk/xpath/latest (accessed 2026-08-15)
- npm registry, @getodk/xforms-engine — https://registry.npmjs.org/@getodk/xforms-engine/latest (accessed 2026-08-15)
- getodk/web-forms README — https://github.com/getodk/web-forms (accessed 2026-08-15)
- SurveyJS, Conditional Logic and Dynamic Texts — https://surveyjs.io/form-library/documentation/design-survey/conditional-logic (accessed 2026-08-15)
- SurveyJS, Data Validation — https://surveyjs.io/form-library/documentation/data-validation (accessed 2026-08-15)
- 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)
- JSONata docs — https://docs.jsonata.org/overview.html (accessed 2026-08-15)
- TomFrost/Jexl — https://github.com/TomFrost/Jexl (accessed 2026-08-15)
- joewalnes/filtrex — https://github.com/joewalnes/filtrex (accessed 2026-08-15)
- GitHub Advisory Database, expr-eval — https://github.com/advisories?query=expr-eval (accessed 2026-08-15)
- JsonLogic — https://jsonlogic.com/ (accessed 2026-08-15)
- marcbachmann/cel-js — https://github.com/marcbachmann/cel-js (accessed 2026-08-15)
- MDN, Barcode Detection API — https://developer.mozilla.org/en-US/docs/Web/API/Barcode_Detection_API (accessed 2026-08-15)
- Can I use, BarcodeDetector API — https://caniuse.com/mdn-api_barcodedetector (accessed 2026-08-15)
- MDN, Geolocation.watchPosition — https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/watchPosition (accessed 2026-08-15)
- Expo docs, Location (SDK 57) — https://docs.expo.dev/versions/latest/sdk/location/ (accessed 2026-08-15)
- Expo docs, Camera (SDK 57) — https://docs.expo.dev/versions/latest/sdk/camera/ (accessed 2026-08-15)
- Expo docs, ImagePicker — https://docs.expo.dev/versions/latest/sdk/imagepicker/ (accessed 2026-08-15)
- Expo docs, Audio (expo-audio, SDK 57) — https://docs.expo.dev/versions/latest/sdk/audio/ (accessed 2026-08-15)
- MDN, MediaStream Recording API — https://developer.mozilla.org/en-US/docs/Web/API/MediaStream_Recording_API (accessed 2026-08-15)
- mebjas/html5-qrcode — https://github.com/mebjas/html5-qrcode (accessed 2026-08-15)
- zxing-js/library — https://github.com/zxing-js/library (accessed 2026-08-15)
- Sec-ant/barcode-detector + npm — https://github.com/Sec-ant/barcode-detector ; https://registry.npmjs.org/barcode-detector/latest (accessed 2026-08-15)
- Donaldcwl/browser-image-compression + npm — https://github.com/Donaldcwl/browser-image-compression ; https://registry.npmjs.org/browser-image-compression/latest (accessed 2026-08-15)
- npm registry, react-signature-canvas — https://registry.npmjs.org/react-signature-canvas (accessed 2026-08-15)
- npm registry, react-native-signature-canvas — https://registry.npmjs.org/react-native-signature-canvas/latest (accessed 2026-08-15)
- Protomaps PMTiles docs + MapLibre guide — https://docs.protomaps.com/pmtiles/ ; https://docs.protomaps.com/pmtiles/maplibre (accessed 2026-08-15)
- @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)
- UN System CEB, Personal Data Protection and Privacy Principles — https://unsceb.org/privacy-principles (accessed 2026-08-15)
- ICRC, Handbook on Data Protection in Humanitarian Action, 3rd ed. — https://www.icrc.org/en/data-protection-humanitarian-action-handbook (accessed 2026-08-15)
- GDPR Art. 7 (gdpr-info.eu) — https://gdpr-info.eu/art-7-gdpr/ (accessed 2026-08-15)
- OCHA Centre for Humanitarian Data, Data Responsibility — https://centre.humdata.org/data-responsibility/ (accessed 2026-08-15; IASC and ReliefWeb pages returned HTTP 403)
- Nielsen Norman Group, Error-Message Guidelines for Forms — https://www.nngroup.com/articles/errors-forms-design-guidelines/ (accessed 2026-08-15)