Complete technical reference for the Formulus mobile application component.
Formulus is a React Native mobile application that serves as the client-side component of ODE. It provides offline-first data collection capabilities, custom application hosting, and bidirectional synchronization with the Synkronus server.
formulus/
├── src/
│ ├── api/ # Synkronus API client (auto-generated)
│ ├── components/ # React Native UI components
│ ├── contexts/ # React Context providers
│ ├── database/ # WatermelonDB schema and models
│ ├── hooks/ # Custom React hooks
│ ├── navigation/ # Navigation configuration
│ ├── screens/ # Screen components
│ ├── services/ # Business logic services
│ ├── webview/ # WebView integration and bridge
│ └── utils/ # Utility functions
├── android/ # Android native code
├── ios/ # iOS native code
└── assets/ # Static assets
Formulus uses WatermelonDB for local data storage:
Formulus hosts custom web applications in WebViews:
Two-phase synchronization protocol:
Integration with Formplayer for form rendering:
Formulus exposes a JavaScript API to custom applications running in WebViews.
The API is automatically injected into WebViews. Use the helper function to ensure it’s ready:
// Wait for API to be ready
const api = await getFormulus();
// Now use the API
const version = await api.getVersion();
Get the Formulus host version.
const version = await api.getVersion();
// Returns: "1.0.0"
Create a new observation by opening a form.
await api.addObservation('survey', {
participantId: '123',
location: 'Field Site A'
});
Parameters:
formType (string): The form type identifierinitializationData (object): Optional data to pre-populate the formReturns: Promise that resolves when form is opened
Edit an existing observation.
await api.editObservation('survey', 'obs-123');
Parameters:
formType (string): The form type identifierobservationId (string): The observation ID to editReturns: Promise that resolves when form is opened
Delete an observation.
await api.deleteObservation('survey', 'obs-123');
Parameters:
formType (string): The form type identifierobservationId (string): The observation ID to deleteReturns: Promise that resolves when deletion is complete
List observations for a form type (no structured filter).
const observations = await api.getObservations('survey', false, false);
Query observations with a structured filter AST (preferred for custom apps). Declared data.* paths use a local observation index; other paths use json_extract. See Observation queries.
const observations = await api.getObservationsByQuery({
formType: 'hh_person',
includeDeleted: false,
filter: {
op: 'and',
conditions: [
{ field: 'data.village', op: 'eq', value: 'kopria' },
],
},
});
Parameters:
formType (string): Form type identifierincludeDeleted (boolean, optional): Include soft-deleted rowsfilter (ObservationFilter, optional): Structured filter ASTReturns: Promise resolving to an array of observations
Trigger manual synchronization.
const { version } = await api.sync();
// Optional: include attachments (slower)
await api.sync({ includeAttachments: true });
Returns: Promise<{ version: number }> — the server’s data revision after sync completes.
Probe whether the configured Synkronus server answers GET /health. Never rejects for offline devices — returns { online: false }.
const status = await api.getConnectivityStatus();
// { online: boolean, serverUrl: string | null, checkedAt: number }
Use for “verify when online, fall back when offline” workflows in custom apps.
Read the device’s last-known Synkronus data revision (current_version from the most recent successful sync).
const revision = await api.getCurrentDataRevisionCount(); // number, 0 if never synced
Reflects server-stream alignment only — not unsynced local edits. Poll after sync() or on an interval to detect remote changes from other devices.
Persist an observation without opening Formplayer (headless write). Uses the same path as a Formplayer submit.
const result = await api.persistObservation({
formType: 'survey',
finalData: { name: 'Ada', age: 30 },
observationId: null, // omit or null to create; provide id to update
});
// { observationId, formData }
When opening forms programmatically, openFormplayer accepts:
| Option | Description |
|---|---|
subObservationMode |
Nested child form for embedded sub-observations |
skipFinalize |
Omit Finalize page; Done on last content page submits after child-schema validation; returns formData to parent |
skipDraftSelection |
Skip draft picker on new root sessions (custom-app orchestration) |
Form init params reserved keys (not persisted as observation data): defaultData, theme, darkMode, themeColors, context (read-only session context exposed in Formplayer as window.formulusSessionContext), validationMode.
| Column | Type | Description |
|---|---|---|
id |
string | Unique observation identifier |
form_type |
string | Form type identifier |
data |
JSON | Observation data (form responses) |
created_at |
timestamp | Creation timestamp |
updated_at |
timestamp | Last update timestamp |
deleted |
boolean | Soft delete flag |
_status |
string | Sync status (created, updated, deleted) |
_changed |
string | Changed fields tracking |
| Column | Type | Description |
|---|---|---|
id |
string | Unique attachment identifier |
observation_id |
string | Reference to observation |
file_path |
string | Local file path |
mime_type |
string | File MIME type |
size |
number | File size in bytes |
synced |
boolean | Sync status |
The app maintains sync state:
Configured through Settings screen:
See Formulus Development Guide for complete development setup.
# Install dependencies (build @ode/tokens first; see Development Setup)
cd ../packages/tokens && pnpm install && pnpm run build && cd ../formulus
pnpm install
# Start Metro bundler
pnpm start
# Run on Android (vendors Notifee via preandroid)
pnpm run android
# Run on iOS
pnpm run ios
# Generate API client from OpenAPI spec
pnpm run generate:api
# Generate WebView injection script
pnpm run generate
android/ directoryios/ directorysrc/ directoryassets/ directoryFor complete API documentation, see: