FForm Platform
enzh-CN

Offline surveys

Package supported survey controls, encrypted attachments and assisted completion for resilient offline collection.

Offline survey module

`FormPlatform.Offline` is an optional trusted module for authenticated respondents and ACL-authorized system-user assisted entry. It downloads version-bound survey packages, encrypts drafts and queued responses in IndexedDB, and synchronizes through the canonical Survey validation and persistence path when connectivity returns. Removing the module removes its routes and browser UI; the Host contains only stable gateway and transaction-level idempotency primitives.

Architecture

The implementation has three boundaries:

1. `ISurveyOfflineGateway` and the separate, binary-compatible `ISurveyOfflineFileGateway`/`ISurveyOfflineOptionGateway` capabilities remain the respondent contracts. SDK 1.7 adds the separate `ISurveyOfflineAssistanceGateway`; it never weakens or overloads respondent authentication. The Host implementation obtains an authorized deployment view, applies trusted SystemValue handling, sanitizes rich text, captures model-backed options through the canonical query service, validates exact form/option fingerprints, and delegates to the normal Survey response/file writers.

2. Core migrations `FormPlatform.Core/015_survey_submission_idempotency` and `016_survey_offline_file_idempotency` store response and file idempotency receipts. A response receipt is committed in the same transaction as the generated response-table mutation. A file receipt, its Survey file row, and its package quota increment are committed in one transaction. An identical retry returns the stored result; reuse of either client UUID with different content returns 409.

3. `FormPlatform.Offline` owns package grants, expiry policy, APIs, PWA assets, offline-passcode encryption, local drafts, and the outbox. Its migrations are `FormPlatform.Offline/001_offline_survey_package_grants`, `002_offline_option_snapshot_manifest`, and `003_offline_assisted_entry`. Migration 003 adds an explicit authorization mode and operator binding; the authoritative target remains the existing `respondent_id`. The display name is returned only for the encrypted browser package and is not duplicated in the grant table.

The module never references `FormPlatform.Host.dll`. It depends only on `FormPlatform.Sdk` and `FormPlatform.Extension.Abstractions`. Installing it is the explicit site-wide opt-in: it adds the respondent Offline tool and an **Offline assisted entry** system-menu link. Every package remains limited to deployments the current identity may use and the fail-closed supported surface below. No `survey_deployment` column or per-form property is added.

Security model

  • Respondent package issuance and synchronization require the isolated `Respondent` authentication policy. Assisted routes use the normal system-user cookie and a distinct server grant mode; the two modes cannot open, revoke, upload, or synchronize each other's grants.
  • Assisted issuance requires Survey Responses workspace Read, target Survey Read, and target Survey assisted-entry authorization (`submissions` or the `SurveyAssistant` default), plus current department scope. Synchronization and each file upload require the same operator to authenticate again and repeat those checks.
  • Issuance runs the normal deployment checks: current time window, active respondent, active list, membership, exact deployment/list/respondent department, and anonymous/external-identity rules.
  • Synchronization checks the package owner, authorization mode, identity kind, revocation, package window, clock skew, and upload grace period. Survey assignment, applicable active status, department equality/scope, form ACL/type, validation, and cross-record rules are checked again by the Host. Assisted grants bind both operator ID and acting respondent ID; neither value comes from the browser sync body. The package also binds `survey_deployment.updated_at`, so changing its list, form, repeat policy, time window, name, or department invalidates an upload that has not already committed.
  • `CompletedAt` must fall within the issued package window and cannot be unreasonably ahead of server time. It selects the deployment-time window only; it never supplies authorization or a department.
  • The server ignores client form, deployment, department, fingerprint, response ID, response-version, quota, and completion-status authority. Those values come from the authenticated package-grant row or module policy. Answer synchronization contains only the package ID, idempotency ID, answer data, and client completion time; the raw file endpoint receives an opaque client file ID plus bounded file metadata, while the module constructs all trusted gateway context from the grant.
  • The form plus complete metadata is SHA-256 fingerprinted. A changed schema, mapping, trigger configuration, or behavior metadata produces 409 for a not-yet-committed upload and requires a new download. An exact retry of an already committed idempotency ID still receives its stored result after current assignment authorization succeeds.
  • Each Data Model-backed AsyncSelect/Tree snapshot contains only records returned by the canonical option service after configured static filters, reference filters, and the applicable department convention are applied. Respondent mode uses the respondent endpoint convention; assisted mode uses the operator's current department IDs, matching online assisted option endpoints. A SHA-256 version binds the control ID/type, answer property, multiplicity, form fingerprint, and complete ordered item set. The grant manifest is stored server-side; synchronization compares the browser version map with that grant, re-queries current authorized options under current scope, and rejects changed snapshots or submitted keys outside the authorized set. Client-side filtering is usability only.
  • Updates carry the response `updated_at` observed when the package was issued. A different current value produces 409 rather than overwriting another device's work. A new non-repeat package also conflicts if another device created the response first.
  • Browser data is encrypted with AES-256-GCM. Package bodies, drafts, response data, owner information, expiry, answer outbox metadata, file metadata, and every file Blob are encrypted; only opaque IndexedDB record IDs, the device UUID, salt, IVs, and ciphertext remain outside the envelope. File metadata and bytes use distinct IVs and authenticated contexts. Each ciphertext authenticates its store and record ID as additional data, so a valid package/outbox ciphertext cannot be moved to another local record. Its key is derived from an 8–64 character offline passcode using PBKDF2-SHA-256 with 600,000 iterations. The passcode and derived key are not persisted; use a long passphrase rather than a short numeric PIN for sensitive surveys. Losing the passcode makes local data unrecoverable; the locked page still offers an explicit device-data reset.
  • Attachment, Camera, and Signature files are SHA-256 hashed before encryption. The browser enforces the package's count, per-file byte, aggregate-byte, global MIME, and control `accept` limits before storing. At upload, the module repeats size/global MIME checks; the Host re-hashes the received bytes, validates the declared MIME against a file signature or Open XML container, checks the exact form control and storage mode, and atomically applies the server package quota. Client-side checks are usability only; server checks are authoritative.
  • Offline file receipts preserve unattached Survey files through the package synchronization grace window. Ordinary stale-upload cleanup cannot remove them early. After the retention window, receipts, unreferenced files, media assets, and unused quota rows become eligible for normal cleanup.

Browser encryption protects stored data from casual filesystem inspection; it is not a defense against script execution in the same origin. Production still requires HTTPS, a restrictive CSP, reviewed trusted modules, and prompt browser security updates. Compiled application files, module files, generated form CSS, and same-origin form design assets are ordinary Cache Storage entries rather than encrypted answer data; do not put respondent-specific secrets in a static design asset. `CompletedAt` is necessarily a client-clock assertion: the server bounds it to the server-issued package window, current time, and configured skew, but offline software alone cannot prove the physical time at which a person entered an answer. Do not treat it as a high-assurance legal timestamp.

Package and sync lifecycle

1. The respondent opens **Offline** from the respondent dashboard and creates or enters the device passcode.

2. While online, the respondent downloads an eligible deployment. The server creates a package grant and captures complete authorized AsyncSelect/Tree option sets within configured limits; the browser stores the package, option snapshots, and initial response snapshot encrypted.

3. The client preloads every supported FormReader control used by the schema and asks the module service worker to cache the current SPA shell, module assets, generated form CSS, and loaded control chunks.

4. Draft changes and selected files are encrypted and autosaved locally. AsyncSelect and Tree search/page exclusively over the encrypted snapshots and never fall back to a network API. File values are deliberately invalid `offline:*` placeholders, so an implementation bug cannot accidentally submit them as server IDs. Pagination's normal `surveyProgress` next-page event remains a local draft save; only a completed response and its exact option-version map are frozen into one outbox item for that package.

5. The page synchronizes explicitly or on a foreground `online` event. It decrypts and uploads referenced files sequentially and idempotently, verifies every server ID/SHA-256 receipt, replaces all local placeholders in a cloned frozen answer, and only then submits that answer. Survey API responses are never cached. The server validates option expiry at completion time, the package-bound version map, the current authorized option version, and every selected key. It removes an item only after the canonical result is received. A definitive field/file/option-value validation rejection removes the uncommitted queue item and preserves the draft for correction; uncertain transport and conflict failures remain frozen.

6. If the HTTP response is lost after the database commit, the unchanged outbox item retries with the same client UUID and receives the previously committed result.

7. After the browser receives success, that downloaded package is marked synchronized and becomes read-only. Remove it and download a fresh package before another completion; this also obtains a current response version and form fingerprint.

Queued answer data is deliberately immutable. Remove and redownload the package to discard an uncertain request; never edit a request under the same idempotency UUID.

Supported form surface

Offline packages accept local controls and containers including Input (except file), Textarea, Radio, Dropdown, Checkbox, RichText, Table, Spreadsheet, Pagination, Tabs, GridLayout, Header/static content, QR code, labels/messages/statistics, static or form-data charts, geolocation, SystemValue, Attachment (`FileUpload`), Camera, Signature, and Data Model-backed AsyncSelect/Tree controls with static filters. FileUpload/Camera/Signature must use `auto` or `survey` storage; custom, form-media, and manual storage modes fail package creation. Image controls are accepted only when empty or backed by an inline `data:image/*` source.

Package creation fails closed for:

  • DataGrid and ItemRenderer;
  • AsyncSelect with `apiUrl`, or AsyncSelect/Tree without a Data Model;
  • Tree without `parentIdField`;
  • AsyncSelect/Tree filters containing runtime `{property}` substitutions, because a single immutable download cannot safely reproduce answer-dependent queries;
  • an option control or package whose complete authorized result exceeds the configured record/byte limits; partial option snapshots are never issued;
  • file-type Input (use FileUpload/Attachment instead);
  • Image controls that reference a network resource;
  • CustomBlock controls configured with a form-name or JSON source rather than ordinary packaged children;
  • menu, breadcrumb, system-slot, unknown extension controls;
  • non-empty form Action Code;
  • configured component actions other than local `validateForm()` and a Button click's `submitForm()`, Pagination action-chain completion, and a submit-on-next mode other than `surveyProgress`.
  • charts configured with API data mode.

These features need a separately designed immutable option/data snapshot or executable-code versioning contract. The module does not silently present a partially working form. Custom validation and visibility/read-only expressions on otherwise supported controls must be pure and depend only on packaged form data/context; expressions that call a network service are not offline-compatible. SystemValue is displayed from the package snapshot and is always recalculated by the server during synchronization; server time therefore means synchronization time unless the form has a separate respondent-entered completion-time field.

Supported controls must still be authored for offline use: options and chart data must be embedded or form-derived, and geolocation depends on browser/device capability. Remote images and fonts are not cached. Only same-origin compiled assets, module assets, generated form assets, and resources already admitted by the service-worker allow-list become part of the offline shell. Chart.js is bundled with the lazily loaded chart-control chunk so the module preloader can cache it before disconnecting.

Configuration

`appsettings.offline.json` is module-owned:

{
  "OfflineSurveys": {
    "PackageLifetimeHours": 168,
    "SyncGracePeriodHours": 720,
    "AllowedClockSkewMinutes": 5,
    "MaximumPackagesPerRespondent": 50,
    "MaximumPackagesPerOperator": 500,
    "OptionSnapshotLifetimeHours": 72,
    "MaximumOptionSnapshotRecordsPerControl": 1000,
    "MaximumOptionSnapshotRecordsPerPackage": 5000,
    "MaximumOptionSnapshotBytesPerPackage": 2097152,
    "MaximumAttachmentsPerPackage": 25,
    "MaximumAttachmentBytes": 10485760,
    "MaximumTotalAttachmentBytes": 52428800,
    "AllowedAttachmentMimeTypes": [
      "image/jpeg",
      "image/png",
      "image/webp",
      "image/gif",
      "image/heic",
      "image/heif",
      "application/pdf",
      "text/plain",
      "text/csv",
      "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
      "application/vnd.openxmlformats-officedocument.presentationml.presentation"
    ]
  }
}

`PackageLifetimeHours` is capped at 90 days and is shortened to the deployment end time. `MaximumPackagesPerRespondent` is clamped to 1–500; `MaximumPackagesPerOperator` is clamped to 1–5,000 because one field device may intentionally carry many explicitly selected assisted packages. `SyncGracePeriodHours` permits upload after the package/deployment completion window, but only for a response whose recorded completion time was inside that window. Revocation and current assignment/authorization checks still win.

Option snapshots expire at the earlier of the package expiry and `OptionSnapshotLifetimeHours` (clamped to 1–2,160 hours). `MaximumOptionSnapshotRecordsPerControl` is clamped to 1–5,000, the package total to 1–25,000, and serialized snapshot bytes to 64 KiB–16 MiB. Defaults are 1,000 records per control, 5,000 per package, 2 MiB, and 72 hours. Expiry is evaluated against `CompletedAt`, so a response completed before expiry may synchronize during the normal grace period; the option set is nevertheless re-queried at synchronization and a changed version returns 409. Increase limits only after reviewing offline data exposure, download size, IndexedDB capacity, and query cost.

Attachment limits are embedded in the server-issued package response and repeated on upload; the browser cannot raise them. Allowed MIME values are restricted to the types for which the Host implements content validation. Keep `MaximumAttachmentBytes` at or below the Host `SurveyFiles:MaxFileSizeBytes`. Files uploaded during a later rejected answer remain server-side unattached and count against that package until the retention window expires; this intentionally prevents repeated replacement attempts from bypassing the aggregate server quota.

HTTP API

Respondent routes require the isolated Respondent policy. Assisted routes require normal system-user authentication and then enforce the same workspace/form ACL used by online assisted entry.

MethodRoutePurpose
`GET``/api/offline/packages`List the current respondent's active server-side package grants
`POST``/api/offline/packages`Issue a versioned package for one deployment/form/device
`DELETE``/api/offline/packages`Revoke every package grant for the current respondent identity
`DELETE``/api/offline/packages/{packageId}`Revoke one package owned by the current respondent
`POST``/api/offline/packages/{packageId}/files/{clientFileId}`Upload one raw, SHA-256-bound encrypted-outbox file idempotently
`POST``/api/offline/sync`Synchronize one versioned, idempotent response
`POST``/api/offline/assisted/packages`Issue a package for one explicitly selected assigned respondent and device
`DELETE``/api/offline/assisted/packages`Revoke every assisted grant owned by the current system user
`DELETE``/api/offline/assisted/packages/{packageId}`Revoke one assisted grant owned by the current system user
`POST``/api/offline/assisted/packages/{packageId}/files/{clientFileId}`Upload one assisted-package file after current operator/ACL/scope checks
`POST``/api/offline/assisted/sync`Synchronize one assisted response as the grant-bound target respondent

`/offline`, `/offline/assisted`, `/offline-sw.js`, `/offline.webmanifest`, and `/offline-assisted.webmanifest` are public application-shell resources so either encrypted workspace can bootstrap while disconnected. Public shell access does not expose package data or make an API anonymous. Local records include an encrypted authorization-mode/owner binding and each page shows and synchronizes only its own mode. Clearing an unlocked page revokes and deletes that mode's grants/data; a locked emergency reset clears all local encrypted data because it cannot safely classify ciphertext without the passcode. Unreachable server grants expire normally. The service worker uses the current PathBase as its scope, refreshes allow-listed static assets network-first, falls back only for the two offline application routes, and never caches Survey API payloads; encrypted business data remains in IndexedDB.

Server-side revocation prevents future synchronization but cannot remotely erase an already downloaded, decryptable browser package. Before uninstalling the module, respondents should use **Clear this device** where operational policy requires local erasure. Removing module files removes the live route/API after the next online application refresh, but browsers may retain encrypted IndexedDB records and previously cached static assets until site data is cleared; unreachable grants remain harmless for writes and expire under server policy.

Build and deployment

Build the upgraded Host/SDK and the module, then deploy only the module assembly/package files—not a private copy of `FormPlatform.Sdk.dll`—to:

Modules/FormPlatform.Offline/
  FormPlatform.Offline.dll
  FormPlatform.Offline.deps.json
  module.json
  appsettings.offline.json
  client/*

Restart FormPlatform. With migrations enabled, Core `015`, Core `016`, and module `001`/`002`/`003` are applied before the module endpoints are used. The Host automatically discovers `client/index.js` and projects the module-owned assisted-entry link into `SystemLeftPane`; no `Program.cs`, SPA route table, or `ClientModules` setting is required.

Verify respondent mode on HTTPS (or a browser-recognized secure localhost): log in as an active respondent, download a supported deployment, load `/offline` once, disconnect networking, refresh `/offline`, search/page/select AsyncSelect and Tree values without network traffic, capture a Camera image, draw a Signature, select allowed and rejected Attachment files, complete and queue, reconnect, and synchronize. Verify limits, option expiry/change, forged keys, files-before-response ordering, idempotent retry, changed versions, expiry/revocation, inactive respondent/list, and cross-root rejection.

Verify assisted mode separately: grant a non-administrator Survey Responses Read plus target Survey Read and `submissions` (or use `SurveyAssistant`), open `/offline/assisted`, select an in-scope deployment/respondent, and download. Disconnect, refresh the assisted route, unlock and queue without respondent credentials. Reconnect first as a different system user and confirm synchronization is denied; then use the original operator and confirm success plus `survey_response_audit.actor_type = system_user`, the original operator in `actor_id`, and the target in `respondent_id`. Repeat after revoking form ACL, removing deployment membership, shrinking operator department scope, changing the option set/form/deployment/response, and choosing a respondent under another company root; every case must fail closed. Also verify assisted Attachment/Camera/Signature uploads and that respondent endpoints cannot use an assisted package ID (and vice versa).

Assisted-entry offline mode

Assisted entry is enabled as an independent authorization mode at `/offline/assisted`. While online, the system user selects an active in-scope deployment and explicitly assigned respondents; the server—not the browser—resolves and stores the operator ID, acting respondent ID, department, deployment/form/response versions, option grants, device, expiry, and file policy for every grant. The target display name is included only in the encrypted browser package. The module menu is visible to users who can read Survey Responses, while the APIs additionally require target Survey Read and assisted-entry authorization. A page returns at most 500 respondents per search; use the search field to narrow larger lists, then download one respondent or select the page for sequential, independently authorized batch issuance. A failure stops the batch without rolling back packages already encrypted locally.

Once packages are encrypted locally, field collection needs neither respondent credentials nor an online operator session. The device passcode is a local vault secret, not a FormPlatform login and not a transferable server credential. On reconnection, the same grant-bound operator must authenticate through the normal system login. Every file and response request rechecks workspace/form ACL, current department scope, deployment membership, target binding, package window, versions, option snapshots, and all canonical Survey validation. Audit rows retain the system operator in `actor_id` and the person represented by the answer in `respondent_id`.

Respondent and assisted grants use distinct server routes, ownership predicates, and authorization-mode values. The sync payload never selects an acting respondent, and a respondent package is never accepted as a bearer package. Passwords and respondent credentials are never cached. If another operator needs to continue collection, the original operator must revoke/remove the old packages and that other operator must issue new packages under their own authorization and audit identity.