Offline mode
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.
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 type | Resource server endpoint | What the host must return |
|---|---|---|
form-resource | resource/rest/v2/resource/form/${form_name}?files=all | Full form definition object |
api-resource | resource/rest/v2/resource/global_apis/ZZ__environment_global_apis__ZZ | Global API definitions for the form environment |
variable-resource | resource/rest/v2/resource/global_variables/ZZ__environment_global_variables__ZZ | Global variable definitions for the form environment |
system-api-resource | resource/rest/v2/resource/system_api?lastOnly=true&files=all | System api definitions for project |
file-resource | resource/rest/v2/resource/form/${form_name}?files=all | base64 image format as part of the resource of the retrieved content |
data | Global API calls and form environment API calls | Data resolved by API calls |
ignore | — | No response required; the renderer discards this request type |
| Request type | EHR server endpoint | What the host must return |
|---|---|---|
form-resource | {EHR_SERVER}/form/{FORM_NAME}?resources=ALL | Full form definition object |
api-resource | /form/ZZ__environment_global_apis__ZZ?resources=global-apis | Global API definitions for the form environment |
variable-resource | /form/ZZ__environment_global_variables__ZZ?resources=ALL | Global variable definitions for the form environment |
data | Global API calls and form environment API calls | Data resolved by API calls |
save-composition | POST /composition?ehrId={EHR_ID}&lifecycleState=complete&templateId={TEMPLATE_ID}&format=STRUCTURED | Confirmation that the composition has been accepted (payload contains the full composition object) |
update-composition | PUT /composition?ehrId={EHR_ID}&lifecycleState=complete&templateId={TEMPLATE_ID}&format=STRUCTURED | Confirmation that the composition update has been accepted |
attachment-added | Upload API call configured on the multimedia / canvas field | Synthesise 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-fetch | Download API call configured on the multimedia field | The 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=ALL | base64 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
idfield in your response must match theidfrom the incoming request. - Requests are asynchronous; your handler must be an
asyncfunction or return aPromise.
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— theuploadIdminted 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:
- Persist
payload[0].filelocally (IndexedDB, OPFS, in-memory cache, …) keyed byresourceId. - 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/alternatetextback 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— theuploadIdyou stored under onattachment-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:
- On reconnect, iterate cached attachments.
- Replay each upload against the real upload API.
- Replace the offline
|urion the queued composition with the URI returned by the server. - 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-…"
}
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.
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.