Mobile and field work: Progressive Web Apps
Field crews, drivers and warehouse staff work on phones, often with a bad connection or none. Any pgkiln application can become a Progressive Web App (APEX: Progressive Web App): installed on the home screen with its own icon, full screen, and working on when the network drops.
Turning it on
App → Settings → Progressive Web App:
| Setting | What it does |
|---|---|
| Installable | Adds a web app manifest and a service worker to the application. Phones offer "Add to home screen" / "Install"; the app then opens full screen, without the browser bar |
| Name under the icon | The short name on the home screen (the application's name when empty) |
| Keep visited pages on the device | Pages a user opened are shown again when there is no connection (with a notice that they may be out of date) |
| Keep forms sent without a connection | Forms submitted offline are kept on the device and sent when the connection is back |
| Push notifications | Users may turn on notifications per device; application code sends them (below) |
| Icon | A square PNG of at least 512 × 512 pixels. Without one, the app gets a tile with its initial in the theme's accent colour |
Installing needs HTTPS in production (any address except localhost / 127.0.0.1); pgkiln behind a TLS proxy (chapter 1) is enough.
Everything is per application, under its own address: /a/<alias>/manifest.webmanifest, /a/<alias>/sw.js (the service worker, which only handles that application's pages), /a/<alias>/icon-192.png, /a/<alias>/icon-512.png and /a/<alias>/offline.
Offline
- The app shell (pgkiln's CSS, script and icons) and an offline page are stored when the app is installed, so the app always opens.
- Pages are fetched from the network first. Without a connection, a page the user opened before comes from the device (when Keep visited pages is on); otherwise the offline page appears, listing the pages that are on the device.
- Kept pages contain the user's data. They stay on the device only while the user is signed in: signing in or out removes them, so the next person on a shared device doesn't see them.
- Live data (reports, charts) is as fresh as the last visit. Actions that need the server (searching, downloads, dynamic actions) wait for the connection.
Forms sent offline
With Keep forms sent without a connection on, a form submitted while the network is down (or drops halfway) is not lost:
- The service worker keeps the form, files and photos included, on the device and shows the page again with the notice Saved on this device. A panel at the bottom shows how many forms are waiting, with Send now and Discard.
- When the connection is back (or the app is opened again), the forms are sent in order, only for the user who filled them in, with a fresh CSRF token. If the session has ended, the panel asks the user to sign in first.
- The server processes each form at most once: every page form carries a submission id, and a form that arrives again (a lost response, a double tap) is acknowledged without running its processes again.
- Each form also carries the key of the record it was opened for, signed by the server, so a form sent later updates that record even if the user opened other records in the meantime.
- A form the server refuses (a validation error) stays in the panel as rejected: open the page and correct it.
Validations, processes and row level security run when the form arrives, as for any submission.
Field items
| Item | For | Setting |
|---|---|---|
Location (location) | The device's position: a Use my location button fills in latitude,longitude (5 decimals, about 1 m). Read-only, it shows a map link. The server checks the format | item type location |
| Photo from the camera | File items open the camera on phones | {"capture": "environment"} (the back camera) or "user" |
| Smaller photos | Photos are scaled down (JPEG) on the phone before they are uploaded: much less mobile data and faster forms | {"max_px": 1600} (the longest side) |
| Barcode / QR code scan | A Scan button on a text item reads a code with the camera (parcels, pallets, assets). It appears only where the browser can read codes (BarcodeDetector, e.g. Chrome on Android) | {"scan": true} on a text item |
The page's Permissions-Policy allows the camera and the position for the application itself and nothing else; the browser asks the user for permission the first time.
Push notifications
APEX: Push Notifications of a Progressive Web App, APEX_PWA.SEND_PUSH_NOTIFICATION, the Send Push Notification process. A notification appears on the user's phone or computer like a message from an installed app, also when the app is closed, and opens a page of the app when tapped.
Turning it on. Settings → Progressive Web App → Push notifications (the app must be installable, and the server needs PGKILN_SECRET_KEY: the key that signs notifications is stored encrypted). Each application gets its own key pair (VAPID, RFC 8292) the first time; it is never part of an export.
Users choose per device. My account → Notifications has a Turn on notifications button for the device in use: the browser asks for permission, and the device is registered for that user. A dynamic action push_subscribe on a button does the same anywhere in the app (browsers only ask after a click). On iPhone and iPad (iOS 16.4 and later), notifications work only after the app was added to the home screen.
Sending. From application SQL (a process, an automation, a workflow step, a trigger):
select meta.send_push(
p_user => :P5_APPROVER, -- the user name
p_title => 'Leave request from ' || :APP_USER,
p_body => '3 days from 12 October',
p_page => 5, p_items => jsonb_build_object('P5_ID', :P5_ID), -- the page it opens
p_tag => 'leave-' || :P5_ID); -- a newer one with the same tag replaces itor declaratively with a send_push process. The notification goes to every device on which that user turned notifications on (meta.has_push_subscription(user) tells whether there is one). The link is signed for the recipient (session state protection), not for the sender, and is always a page of the application: a notification can't point elsewhere.
When it is sent. meta.send_push only queues the message (meta.push_message): the pgkiln server sends it right after the transaction commits, so a submit that fails sends nothing. Each message is encrypted for the device (RFC 8291) and posted to the browser's push service (Google, Mozilla, Apple or Microsoft), which delivers it when the device is online, for up to p_ttl_s seconds (default one day). A push service that is busy is tried again (3 attempts); a device the service no longer knows is removed. Settings → Progressive Web App shows the number of devices, the last week's results, and a Send test button.
Privacy and security.
- The push service sees only an encrypted message; the title and text are readable on the device only.
- A device belongs to one user: signing out turns notifications off on that device, and a device whose notifications another user turned on is turned off when the next user opens the app.
- A new password, a deactivated account or removed access to the app removes the user's devices.
- The server posts only to the browsers' push services (
PGKILN_PUSH_HOSTS), over HTTPS, to public addresses; a subscription naming another host is refused. - New keys (Settings → Progressive Web App) replaces the key pair, for example if the server's secret key may have leaked; every device then has to turn notifications on again.
Example
The HR example application is a PWA: install it from the browser menu on a phone, open a few pages, switch on airplane mode, and file a leave request; it is sent when the connection is back. Its employee form records a work location and takes the photo with the camera (examples/hr/hr_12_pwa.sql).
Not included
Native app store packaging is not needed: the installed PWA is the app. Notifications have a title, a text and a link; images and action buttons in a notification are not supported.