Skip to main content

Offline mode

info

This content is intended for advanced Studio users.

Offline mode allows Form renderer to operate without network connectivity or reliance on backend services. Instead of making direct HTTP calls, Form renderer delegates all resource requests to the host application via a typed request handler function that you register. The host application resolves each request from a local cache or from the network and returns a structured response to the renderer.

This page covers what offline mode is, how to install the offline-capable build, the request handler contract, attachment handling, host application responsibilities, and common pitfalls.

warning

Offline mode is available from Form renderer version 3.15. onwards.

When to use offline mode​

Offline mode is suited to clinical deployments where:

  • Users need to enter data in environments with intermittent or no network access, such as point-of-care devices, field clinics, or ward rounds without reliable Wi-Fi.
  • The host application manages its own data cache and synchronisation layer.
  • The integration team can implement and maintain the request handler contract described on this page.

Offline mode is not intended as a drop-in replacement for standard online integration. It requires deliberate architectural decisions in the host application about caching strategy, conflict resolution, and composition queuing.

How offline mode fits into a clinical application​

In offline mode, the host application sits between Form renderer and all external services:

Host Application
├── Authentication
├── Backend Serivces APIs
├── Offline Cache (e.g. IndexedDB)
├── Synchronisation Logic
└── Renderer Host Adapter
↑ ↓
Form Renderer (Web Component)

Form renderer is responsible for:

  • Rendering forms from their definitions
  • Managing in-memory form state
  • Emitting typed resource requests to the host

Form renderer does not:

  • Persist data of any kind
  • Own authentication or authorisation
  • Manage offline storage
  • Decide where data comes from or how it is synchronised

All data ownership belongs to the host application.

Installation​

Offline mode is part of the standard form renderer package from version 3.15 onwards.:

npm install @better/form-renderer

The renderer is distributed as a Web Component and is compatible with any JavaScript framework. For framework-specific setup steps (registering schemas, adding scripts, configuring compilers), follow the standard setup instructions in Framework guides and then add the offline context configuration described below.

Enabling offline mode​

Set mode: 'offline' in the EhrFormContext object and provide an offline configuration block containing your requestHandler function and an optional timeoutMs value.

When mode is set to 'offline', Form renderer stops making direct HTTP calls to the EHR server and begins routing all resource requests through the handler you provide.

Request handler contract​

Request shape​

Every resource request from Form renderer arrives as a RendererResourceRequest object:

export type ResourceRequestType =
| 'api-resource'
| 'form-resource'
| 'variable-resource'
| 'data'
| 'save-composition'
| 'update-composition'
| 'save-contribution'
| 'update-contribution'
| 'attachment-added'
| 'attachment-fetch'
| 'file-resource'
| 'ignore';

export interface ResourceRequest<T> {
type: ResourceRequestType;
resourceId: string;
resourceName: string;
resourceVersion?: string;
payload?: T;
}

export interface ResourceRequestMetaData {
id: string;
url: string;
}

export type RendererResourceRequest<T> = ResourceRequest<T> & ResourceRequestMetaData;

Response shape​

Your handler must return a RendererResult object for every request:

export interface RendererResult<T = unknown> {
id: string; // must match the id from the incoming request
ok: boolean;
result?: T;
error?: string;
}

Request types and expected responses​

Request typeResource server endpointWhat the host must return
form-resourceresource/rest/v2/resource/form/${form_name}?files=allFull form definition object
api-resourceresource/rest/v2/resource/global_apis/ZZ__environment_global_apis__ZZGlobal API definitions for the form environment
variable-resourceresource/rest/v2/resource/global_variables/ZZ__environment_global_variables__ZZGlobal variable definitions for the form environment
system-api-resourceresource/rest/v2/resource/system_api?lastOnly=true&files=allSystem api definitions for project
file-resourceresource/rest/v2/resource/form/${form_name}?files=allbase64 image format as part of the resource of the retrieved content
dataGlobal API calls and form environment API callsData resolved by API calls
ignore—No response required; the renderer discards this request type
Request typeEHR server endpointWhat the host must return
form-resource{EHR_SERVER}/form/{FORM_NAME}?resources=ALLFull form definition object
api-resource/form/ZZ__environment_global_apis__ZZ?resources=global-apisGlobal API definitions for the form environment
variable-resource/form/ZZ__environment_global_variables__ZZ?resources=ALLGlobal variable definitions for the form environment
dataGlobal API calls and form environment API callsData resolved by API calls
save-compositionPOST /composition?ehrId={EHR_ID}&lifecycleState=complete&templateId={TEMPLATE_ID}&format=STRUCTUREDConfirmation that the composition has been accepted (payload contains the full composition object)
update-compositionPUT /composition?ehrId={EHR_ID}&lifecycleState=complete&templateId={TEMPLATE_ID}&format=STRUCTUREDConfirmation that the composition update has been accepted
attachment-addedUpload API call configured on the multimedia / canvas fieldSynthesise the response shape the form's upload bindings expect (e.g. { href, mimeType, fileName }). Persist the binary locally so it can be served back on attachment-fetch.
attachment-fetchDownload API call configured on the multimedia fieldThe cached binary as a Blob (or ArrayBuffer). Look the file up by resourceId (set to the uploadId previously assigned on attachment-added).
file-resource{EHR_SERVER}/form/{FORM_NAME}?resources=ALLbase64 image format as part of the resource of the retrieved content
ignore—No response required; the renderer discards this request type

If you are using Resource server, you can get all the required resources at once by calling the resource/rest/v2/resource endpoint with the appropriate request body.

Example of the request body:

{
"resources": [
{
"type": "form",
"name": "Offline testing form",
"version": "1.0.3",
"files": "ALL"
},
{
"type": "form",
"name": "Offline testing form 2",
"version": "1.0.1",
"files": "ALL"
},
{
"type": "global-apis",
"name": "ZZ__environment_global_apis__ZZ",
"files": "ALL"
},
{
"type": "global-variables",
"name": "ZZ__environment_global_variables__ZZ",
"files": "ALL"
}
]
}

Handler rules​

  • Every request must be resolved or rejected — do not leave requests pending.
  • Requests must resolve within timeoutMs (defaults to 5,000 ms if not set). Requests that exceed this limit are treated as failed by the renderer.
  • The id field in your response must match the id from the incoming request.
  • Requests are asynchronous; your handler must be an async function or return a Promise.

setContext() {
return {
language: 'en',
mode: 'offline', // 'offline' | 'online',
offline: {
requestHandler: async (req: RendererResourceRequest<unknown>) => {
// fetch from API or IndexedDB
switch (req.type) {
case 'form-resource':
return this.createResponse(req, formMock);
case 'api-resource':
return this.createResponse(req, apisMock);
case 'variable-resource':
return this.createResponse(req, variablesMock);
case 'data':
return this.createResponse(req,dataMock);
case 'attachment-added':
// 1. Persist req.payload[0].file under req.resourceId.
// 2. Return a response shaped like the field's upload bindings expect
// so the renderer can extract the URI/mime/altText back into the
// model. The URI you return is what the user will later fetch
// back via 'attachment-fetch'.
return this.createResponse(req, await this.handleAttachment(req));
case 'attachment-fetch':
// req.resourceId carries the uploadId that was minted on
// 'attachment-added'. Look the cached blob up by that key and
// return it; the renderer wraps it as a Blob for download/preview
// and inline thumbnail rendering.
return this.createResponse(req, await this.loadAttachment(req.resourceId));
case 'save-composition':
return this.createResponse(req, '');
case 'file-resource':
return this.createResponse(
req,
await (
await fetch(
`data:image/png;base64,${formMock[0].files.find((file) => file.name === req.resourceName)?.content}`,
)
).blob(),
);
default:
throw new Error(`Unsupported request: ${req.type}`);
}
},
timeoutMs: 2000,
}
};
}

createResponse(req: RendererResourceRequest<unknown>, result: unknown) {
return {
id: req.id,
ok: true,
result,
};
}

Example request payloads​

The following examples show the shape of requests the renderer emits, so you can match on them in your handler.

form-resource:

{
"resourceId": "Offline testing form",
"resourceName": "Offline testing form",
"url": "${RESOURCE_SERVER_URL}/form/Offline%20testing%20form?resources=ALL",
"type": "form-resource",
"id": "19765734-f12a-4735-bc08-0c279f18fe5c"
}

api-resource:

{
"resourceId": "Offline testing form",
"resourceName": "Offline testing form",
"url": "${RESOURCE_SERVER_URL}/form/ZZ__environment_global_apis__ZZ?resources=global-apis",
"type": "api-resource",
"id": "8079cadf-4b20-4155-81cd-431026780c89"
}

variable-resource:

{
"resourceId": "Offline testing form",
"resourceName": "Offline testing form",
"url": "${RESOURCE_SERVER_URL}/form/ZZ__environment_global_variables__ZZ?resources=ALL",
"type": "variable-resource",
"id": "0566762d-aebe-46d3-9107-584a42f4d4e8"
}

save-composition (abbreviated — the payload object contains the full composition):

{
"resourceId": "Offline testing form",
"resourceName": "Offline testing form",
"url": "${RESOURCE_SERVER_URL}/composition?ehrId=${EHR_ID}&lifecycleState=complete&templateId=Problem%20Diagnosis&format=STRUCTURED",
"type": "save-composition",
"id": "6aad4e9b-c699-4659-b52a-d794cc00cef9",
"payload": {
"params": {
"ehrId": "${EHR_ID}",
"lifecycleState": "complete",
"templateId": "Problem Diagnosis"
},
"data": { "...": "full composition object" }
}
}

file-resource :

{
"resourceId": "asset-55161a51-d615-4e86-9cd3-d703a3fc9667",
"resourceName": "asset-55161a51-d615-4e86-9cd3-d703a3fc9667",
"url": "${RESOURCE_SERVER_URL}/resource/rest/v2/resource/form/${RESOURCE_NAME}/${RESOURCE_VERSION}/file/asset-55161a51-d615-4e86-9cd3-d703a3fc9667",
"type": "file-resource",
"id": "7b90de4b-a8cb-4484-a837-a3d8effd1d38"
}

The host application must convert file content from a base64 string to Blob and return it in the result field of the response:

Handling attachments in offline mode​

Attachments in offline mode work as a two-way lifecycle: an upload event (attachment-added) when the user adds a file, and a fetch event (attachment-fetch) every time the renderer needs to read that file back (preview, download, inline thumbnail). The same resourceId ties both ends together — your handler stores the binary under that key on upload and serves it back from the same key on fetch.

attachment-added — capturing the upload​

When a user picks a file in a multimedia field (or saves an image from a Generic Canvas field), Form renderer emits an attachment-added request. The request carries:

  • resourceId — the uploadId minted by the renderer for this attachment. Use it as your storage key.
  • resourceName — attachments-${resourceId}-${fileName}, useful as a human-readable label.
  • payload[0] — { file: Blob, mediatype: string, alternatetext: string, resourceId: string }.

What the host must do:

  1. Persist payload[0].file locally (IndexedDB, OPFS, in-memory cache, …) keyed by resourceId.
  2. Return a response shaped exactly like the field's upload API would return — the renderer feeds it through the field's response bindings to extract uri / mediatype / alternatetext back into the form model. The shape depends on what bindings the form designer configured.

If your upload API normally returns { href, mimeType, fileName }:

async handleAttachmentAdded(req: RendererResourceRequest<{ file: Blob; mediatype: string; alternatetext: string }[]>) {
const { file, mediatype, alternatetext } = req.payload![0];
await this.cache.putAttachment(req.resourceId, file); // your storage
return {
href: req.resourceId, // becomes the |uri stored on the field
mimeType: mediatype,
fileName: alternatetext,
};
}

The value you return for the URI binding (href in this example) is what later flows back as the resourceId on attachment-fetch, so it has to be a stable handle. The simplest choice is to return req.resourceId itself; that gives you a deterministic round-trip.

attachment-fetch — serving the file back​

When the renderer needs to read the file (user clicks preview / download, or an image thumbnail renders), Form renderer emits attachment-fetch:

  • resourceId — the uploadId you stored under on attachment-added.
  • resourceName — attachments-${resourceId}.
  • No payload.

Your handler returns the cached binary as a Blob:

async handleAttachmentFetch(req: RendererResourceRequest<unknown>) {
return await this.cache.getAttachment(req.resourceId); // Blob
}

End-to-end example​

Drawn from apps/renderer-app/src/app/app.component.ts:

offline: {
requestHandler: async (req: RendererResourceRequest<unknown>) => {
switch (req.type) {
case 'attachment-added': {
const payload = (req.payload as Array<{ file: Blob; mediatype: string; alternatetext: string }>)[0];
await indexedDbCache.put(req.resourceId, payload.file);

// The shape returned here matches what the field's upload bindings parse.
// Returning resourceId as the URI makes the round-trip deterministic.
return this.createResponse(req, {
document: {
id: req.resourceId,
uniqueId: req.resourceId,
repositoryUniqueId: req.resourceId,
},
});
}

case 'attachment-fetch': {
const blob = await indexedDbCache.get(req.resourceId);
return this.createResponse(req, blob);
}

// …other request types
}
},
timeoutMs: 2000,
}

Synchronising attachments back to the server​

When the host later synchronises a queued composition to the EHR server, all referenced attachments must be uploaded and their server-side IDs confirmed before the composition is submitted. Compositions submitted with unresolved attachment references will fail.

A common pattern:

  1. On reconnect, iterate cached attachments.
  2. Replay each upload against the real upload API.
  3. Replace the offline |uri on the queued composition with the URI returned by the server.
  4. Submit the composition.

Example payloads​

attachment-added:

{
"id": "5b8a-…",
"type": "attachment-added",
"resourceId": "8f1e-4d2a-…",
"resourceName": "attachments-8f1e-4d2a-…-photo.png",
"url": "https://store.example.com/upload",
"payload": [
{
"file": "<Blob>",
"mediatype": "image/png",
"alternatetext": "photo.png",
"resourceId": "8f1e-4d2a-…"
}
]
}

attachment-fetch:

{
"id": "1a2c-…",
"type": "attachment-fetch",
"resourceId": "8f1e-4d2a-…",
"resourceName": "attachments-8f1e-4d2a-…",
"url": "https://store.example.com/attachments/8f1e-4d2a-…"
}
good to know

Form renderer never persists binary data. The host application is fully responsible for attachment storage and for ensuring that attachment references are valid at the time of composition submission.

tip

For the renderer to emit attachment-fetch, the multimedia field must have a Download API configured (Property panel → Upload → Download tab). Without it, downloads/thumbnails go through the legacy direct-HTTP path and the offline handler is bypassed for fetches.

Host application responsibilities​

Offline mode requires the host application to own the following:

Local cache.: Store form definitions, API definitions, variable definitions, and composition data in a client-side store such as IndexedDB. Decide which resources to cache eagerly (at application start) versus lazily (on first request).

Cache invalidation.: Determine when cached resources need to be refreshed from the EHR/Resource server — for example, when a new form version is published or when variable values change.

Synchronisation.: Queue compositions saved offline and submit them to the EHR/Resource server when connectivity is restored. Implement conflict resolution for compositions that may have been updated server-side during the offline period.

Authentication.: Manage credentials and token refresh independently of Form renderer. The renderer assumes it receives trusted, pre-authenticated responses from the host.

Security.: Encrypt sensitive clinical data at rest if required by your deployment context, regulatory framework, or information governance policy.

Common pitfalls​

Registering the request handler too late. Register the handler before the renderer is attached to the DOM, or immediately after attachment. Requests that arrive before the handler is registered will fail.

Assuming the renderer persists data. It does not. Every composition, attachment, and form definition must be stored by the host application.

Not handling attachment uploads before composition submission. Compositions that reference attachment IDs not yet confirmed on the server will fail at submission time. Upload attachments first; then submit the composition.

Ignoring timeout behaviour. Requests that do not resolve within timeoutMs are treated as failed by the renderer. Set timeoutMs to a value that reflects your cache read latency, not network latency.

Not handling unknown request types. The ResourceRequestType union may expand in future releases. Implement a default branch in your handler's switch statement to avoid silent failures on unrecognised request types.

Frequently asked questions (FAQs)​

What is offline mode in Form renderer?

Offline mode is an operating mode in which Form renderer delegates all resource requests like form definitions, data, and composition saving to the host application via a request handler function, rather than making direct calls to the EHR server. It is designed for clinical deployments where network connectivity cannot be guaranteed.

Does Form renderer store any data in offline mode?

No. Form renderer manages in-memory form state only. All persistence, including form definitions, composition data, and attachments, is the responsibility of the host application.

Which npm package version enables offline mode?

Offline mode requires form renderer version 3.15 or newer.

What is a request handler?

A request handler is an async function you register on Form renderer. It receives typed resource requests from the renderer and must return a RendererResult object for each one. The handler is where your host application logic lives: Fetching from IndexedDB, queuing compositions for synchronisation, handling attachments, and so on.

What happens if my request handler does not respond in time?

Requests that do not resolve within timeoutMs (default 5,000 ms) are treated as failed by the renderer. Set timeoutMs to a value appropriate for your cache read latency.