docs

Formulus Component Reference

Complete technical reference for the Formulus mobile application component.

Overview

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.

Architecture

Technology Stack

Component Structure

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

Core Features

Offline-First Data Storage

Formulus uses WatermelonDB for local data storage:

Custom Application Hosting

Formulus hosts custom web applications in WebViews:

Synchronization Engine

Two-phase synchronization protocol:

  1. Observation Sync: JSON metadata synchronization
  2. Attachment Sync: Binary file synchronization

Form Rendering

Integration with Formplayer for form rendering:

JavaScript Interface

Formulus exposes a JavaScript API to custom applications running in WebViews.

API Access

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();

Core Methods

getVersion()

Get the Formulus host version.

const version = await api.getVersion();
// Returns: "1.0.0"

addObservation(formType, initializationData)

Create a new observation by opening a form.

await api.addObservation('survey', {
  participantId: '123',
  location: 'Field Site A'
});

Parameters:

Returns: Promise that resolves when form is opened

editObservation(formType, observationId)

Edit an existing observation.

await api.editObservation('survey', 'obs-123');

Parameters:

Returns: Promise that resolves when form is opened

deleteObservation(formType, observationId)

Delete an observation.

await api.deleteObservation('survey', 'obs-123');

Parameters:

Returns: Promise that resolves when deletion is complete

getObservations(formType, isDraft?, includeDeleted?)

List observations for a form type (no structured filter).

const observations = await api.getObservations('survey', false, false);

getObservationsByQuery(options)

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:

Returns: Promise resolving to an array of observations

sync(options?)

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.

getConnectivityStatus()

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.

getCurrentDataRevisionCount()

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.

persistObservation(input)

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 }

openFormplayer options

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.

Database Schema

Observations Table

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

Attachments Table

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

Synchronization Protocol

Two-Phase Sync

Phase 1: Observation Sync

  1. Pull: Request changes from server since last sync
  2. Apply: Apply server changes to local database
  3. Push: Send local changes to server
  4. Resolve Conflicts: Handle conflicts if any

Phase 2: Attachment Sync

  1. Download Manifest: Get list of attachments to download
  2. Download Files: Download missing attachments
  3. Upload Files: Upload pending attachments
  4. Update Status: Mark attachments as synced

Sync State Management

The app maintains sync state:

Configuration

Server Configuration

Configured through Settings screen:

Sync Configuration

Development

Building from Source

See Formulus Development Guide for complete development setup.

Key Development Commands

# 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

Project Structure

Platform-Specific Features

Android

iOS

Security

Authentication

Data Protection

Performance

Optimization Strategies

Memory Management

Troubleshooting

Common Issues

Sync Failures

App Crashes

Performance Issues

API Reference

For complete API documentation, see: