Hello everyone,
Following yesterday’s live stream and the request for a mobile application, I got Claude to work on the subject just to see. In less than 15 minutes, he produced a complete spec and a demo on my Android phone where I can find my dashboards, scenes… everything Gladys, in fact. I’m amazed… ![]()
Here’s his analysis:
The ‹ je › in the message is Claude, I would have been unable to produce all this in 15 minutes.
I started working on a native mobile application for Gladys, and I would like to share the approach before going further — especially the choice of architecture, which seems to me the most debatable point and therefore the one on which your feedback will be most useful.
The foundation is functional: the app runs on my phone, connected to my instance, with all my dashboards and cameras.
The Problem
Today, Gladys on mobile is the browser or the addition to the home screen. It works, but it hits a ceiling:
- no reliable notifications;
- no background presence tracking;
- no presence on the stores;
- and the current service worker (
front/old-sw.js) deliberately uninstalls itself on each activation — there is therefore no usable PWA base on which to build.
The Architecture Choice: Embedding the Existing Frontend
This is the structuring decision. Three options were on the table.
Option A — React Native, a Distinct Native App
A rewritten app, consuming Gladys’s REST API and WebSocket. Better performance, true native feel.
But you have to rewrite the entire interface: 23 routes, 22 types of dashboard widgets, the scene editor, the map, the chat, the integration pages. And above all, you then have to maintain two interfaces in parallel: every evolution of the web front-end would have to be manually ported to the mobile app, with the risk of divergence that this implies. For a community project, this seems unsustainable in the long run.
Option B — Improve the PWA
Fix the service worker, add offline cache and Web Push. Lightweight, no store to manage.
But this does not solve background geolocation, home screen widgets, or presence on the stores. And on iOS, Web Push remains limited. We would still have the same ceilings.
Option C — Capacitor + the Existing Frontend ← retained
The current Gladys frontend (Preact / Vite) is compiled exactly as today, then embedded in a native WebView by Capacitor. The capabilities that the web cannot offer are exposed via plugins, behind an abstraction layer.
front/ (Preact, unchanged)
└── build/ ──> Capacitor WebView
├── android/
└── ios/
+ native plugins: push, geolocation, biometrics,
mDNS, secure storage, widgets
Why this choice:
- A single codebase. Any evolution of the frontend benefits the web, Android, and iOS without porting. This is the decisive point for a community project.
- Almost total reuse. Screens, widgets, actions, translations, dark theme: everything is taken as is.
- Nothing lost on the native side. Push notifications, zone geolocation, biometrics, mDNS discovery, home screen widgets remain accessible via plugins.
- Reversible. If the approach shows its limits, the frontend remains the frontend — we have not created parallel debt.
The guiding principle: the native shell embeds no business logic. It exposes capabilities, the frontend consumes them. Any native functionality is optional, and its absence (permission denied, incompatible platform) never breaks the application.
What Has Been Done: The Connection Foundation
The first batch answers a question that the web frontend never asks: which instance to talk to?
On the web, the answer is trivial — the frontend is served by the instance itself. A mobile app, on the other hand, is distributed in a single build for everyone and must be configured by the user.
Concretely:
- Configuration resolved at runtime.
front/src/config.jsreadprocess.env, which Vite replaces with literals at build. The exported object is now mutable, with a whitelist: only the connection keys are overrideable, so that a malformed stored profile cannot activate demo mode. - First connection screen asking for the instance address, with address testing before saving.
- Multi-instance profiles (main residence, secondary, test instance), stored in the Keychain / Keystore.
- Automatic switch local network ↔ Gladys Plus, with a probe on
/api/v1/ping. - Settings page to manage these instances — hidden on the web, where it would make no sense.
Impact on the web frontend: none. The native code is entirely eliminated from the web bundle by tree-shaking, and the build values remain the same as before. Verified manually in the browser.
Three Things the Spec Did Not Anticipate
This is the most instructive part, and the one that can be useful to others.
1. The Mixed-Content Rule and the Compromise It Imposes
For Gladys Plus’s end-to-end encryption to work, crypto.subtle is required, which is only available in a secure context. Capacitor obtains this by serving the WebView in https://localhost.
Except that a page https:// cannot call either http:// or ws:// — Chromium blocks it. Yet a local Gladys instance is very often in plain HTTP. Android’s network security configuration does not change anything: it is a browser policy, not a platform policy.
The CapacitorHttp plugin handles HTTP requests (they go out through the native layer), but not WebSockets, which remain blocked. Without WebSocket, no real-time.
Hence the arbitration, which must be known:
androidScheme |
Secure Context | Gladys Plus | Local HTTP | WebSocket |
|---|---|---|---|---|
https |
yes | OK | via CapacitorHttp | blocked |
http |
no | unavailable | OK | OK |
For this first batch, I chose http: the goal was to connect to the local instance, which requires the WebSocket. This is explicitly temporary. The clean solution is to serve the instance in HTTPS behind a reverse proxy (Caddy, nginx, Traefik): everything then goes through https/wss, no more blocking, secure context preserved. This is also the only path that will work for iOS.
If you have an opinion on this point, I’m very interested: should we assume requiring an HTTPS instance for using Gladys Plus from the mobile app, or is it better to invest in a native WebSocket plugin to stay in https?
2. An HTTP Client That Froze Its URL
HttpClient captured config.localApiUrl in its constructor — executed when the module is loaded, therefore before the mobile profile is resolved. In the WebView, window.location.origin is https://localhost, i.e., the app itself: all requests would have gone into the void.
It has become a getter that reads the configuration on each request. On the web, the value never changes, so the behavior is identical.
3. Missing viewport-fit=cover
The top bar of the app went under the Android clock and notification icons. The cause was not the header, but the <meta viewport>: itsWith viewport-fit=cover, all env(safe-area-inset-*) variables are zero.
Interesting detail: the Gladys front end already uses safe-area-inset-bottom in five places (chat, dashboard, device list). They therefore did nothing on mobile. The fix reactivates all of them.
Device Validation
Tested on a Xiaomi running Android 16 (Chromium WebView 151), against a real instance:
| Point | Result |
|---|---|
| Web Crypto (RSA-OAEP 2048, ECDSA P-256, AES-GCM, PBKDF2) | 9/9 in https context |
| WebSocket to an HTTP instance | Handshake in 44 ms |
| MSE / hls.js (cameras) | Available |
| Full path: configuration, login, dashboards, cameras | Functional |
A counterintuitive measurement: PBKDF2 100,000 iterations in 15 ms on the phone, vs. 45 ms on my PC. The Android implementation is hardware-accelerated. The risk of slow unlocking on mobile, which I had anticipated, does not exist.
A lesson as well: my initial validation prototype gave three green lights, yet detected none of the two real blockages. It tested a WebSocket from a http://localhost page — therefore without mixed content — and made no API request. You need to validate crypto, WebSocket, and a real API request in the final schema configuration, not in isolation.
What Remains to Be Done
Lot 2 — Touch Ergonomics: Low tab bar, Android back gesture, dashboard drag-and-drop with fingers, keyboard behavior.
Lot 3 — Notifications and Presence. This is the main contribution of the app compared to the browser, and the only lot that requires server-side development: a route for registering push tokens, and a scene action user.send-push-notification.
A point to be collectively decided: A self-hosted instance cannot directly communicate with FCM or APNs, due to lack of service keys — which we obviously cannot distribute in a public image. The relay via Gladys Plus is the natural path (end-to-end encrypted content, the notification only carrying a trigger), with a fallback to local notifications without subscription. Is this an acceptable compromise for the community?
Lot 4 — System Integration: mDNS discovery (the server already publishes the service), home screen widgets, iOS App Intents and Android shortcuts, voice assistant.
Lot 5 — Publication: Build and signing CI, store listings, beta.
Two Open Points Where I Need Help
iOS could not be validated: I don’t have a Mac. Four unknowns remain — secure context on capacitor://, MSE in WKWebView, multicast entitlement for mDNS (granted case by case by Apple, with unpredictable delays), and ATS behavior facing an HTTP instance. If someone has a Mac and half an hour, running the validation prototype would clear up these four points at once.
The question of local HTTPS. It conditions the use of Gladys Plus from the app. Requiring an HTTPS instance is technically clean, but adds a configuration step to users who don’t need it today. I’m interested in your opinions.
The full specification is attached below (unfold the section): architecture, connection, native capabilities, security, hard points, delivery lots, build and acceptance. The code is on a dedicated branch.
Don’t hesitate to challenge the architectural choice — this is precisely the time when it’s still easy to change it.
Thank you for reading.
[details=« 📄 Full Technical Specification (unfold) »]
Technical Specification — v1
Port the existing Gladys front end (Preact / Vite) to a native Android and iOS app via Capacitor, with full functional parity and the capabilities that only a native app can offer: push notifications, background geolocation, biometric unlock, instance discovery on the local network.
| Codebase | Gladys 5.0.2 |
| Approach | Capacitor + existing front end |
| Targets | Android 8+ · iOS 15+ |
| Scope v1 | Full parity |
| Date | August 30, 2026 |
Table of Contents
- Context and Objectives
- Current State
- Target Architecture
- Connection and Authentication
- Native Capabilities
- Interface Adaptations
- Security
- Identified Hard Points
- Delivery Lots
- Build, CI, and Publication
- Testing and Acceptance
- Out of Scope for v1
1. Context and Objectives
Gladys Assistant is currently used on mobile via the browser or adding to the home screen. This approach has its limits: no reliable notifications, no background presence tracking, no presence on stores, and a service worker that, in its current state, deliberately uninstalls itself with each activation.
Objectives
- Functional parity with the web front end: dashboard, devices, scenes, cameras, chat, calendar, map, settings, and integrations.
- Native push notifications triggered by Gladys scenes, with quick actions from the notification.
- Automatic presence via background geolocation, feeding Gladys’s zone detection.
- Frictionless connection: automatic instance discovery on the local network, seamless local ↔ Gladys Plus switch.
- A single codebase for the web, Android, and iOS: any front-end evolution benefits all three targets without porting.
Guiding Principles
- Respect for privacy remains the rule. No telemetry, no third-party analytics SDK. Gladys Plus’s end-to-end encryption is preserved without exception.
- The front end remains the source of truth. The native shell does not embed any business logic: it exposes capabilities, the front end consumes them.
- Graceful degradation. Any native functionality is optional; its absence (permission denied, incompatible platform) never breaks the app.
2. Current State
The front end is a Preact 10 application built by Vite 6, with preact-router routing, unistore global state, and preact-i18n internationalization in three languages (fr, en, de). It is served either by the local Gladys server (server/static), or by Gladys Plus.
What is directly reusable
| Element | Location | Status |
|---|---|---|
| Screens and routing | front/src/routes/ |
As is |
| Dashboard widgets (22 types) | front/src/components/boxs/ |
As is |
Actions and unistore store |
front/src/actions/ |
As is |
| fr / en / de translations | front/src/config/i18n/ |
As is |
| Theme and dark mode | front/src/style/ |
As is |
What needs to be adapted
| Element | Location | Nature of Work |
|---|---|---|
| URL configuration | front/src/config.js |
URLs are frozen at vite build time via process.env. They need to be made dynamic at runtime. Hard point |
| Session storage | front/src/utils/Session.js, front/src/utils/keyValueStore.js |
Plain localStorage. To be replaced with encrypted storage backed by Keychain / Keystore. Hard point |
| HTTP client | front/src/utils/HttpClient.js |
localApiUrl was captured in the constructor, executed at module load via getDefaultState() — therefore before mobile profile resolution. Now a getter reading the config on each request. On the web the value never changes: identical behavior. |
| Safe areas | front/index.html, front/src/template.html |
<meta viewport> nIt didn’t have viewport-fit=cover, which set all the env(safe-area-inset-*) variables to zero — including the five existing uses of safe-area-inset-bottom. |
| WebSocket | front/src/utils/Session.js |
Fixed 1-second interval reconnection, unaware of the application lifecycle. Needs a backoff and reaction to sleep events. |
| Service worker | front/old-sw.js |
Uninstalls itself on activation. Not used in the native shell; to keep as is for the web to clear old caches. |
| Cameras (HLS) | front/src/components/boxs/camera/ |
Custom hls.js loader to inject the token. To validate in WebView, including native HLS on iOS. |
| Drag and drop | front/src/utils/dragAndDropBackend.js |
Already toggles between HTML5 and touch backends; to revalidate in the WebView. |
Server-side
No changes are needed for batches 1 and 2: the REST API (server/api/controllers/, 25 controllers) and the WebSocket already cover the needs. Two server additions are required later: the registration of push tokens (batch 3) and the corresponding scene action.
3. Target Architecture
The front is compiled exactly as today, then packaged into a native WebView by Capacitor. Capabilities that the web cannot provide are exposed to the front as Capacitor plugins, behind an abstraction layer that returns neutral values when the app runs in a browser.
┌─────────────────────────────────────┐
│ MOBILE APPLICATION │
│ │ ┌──────────────────────────────┐
│ ┌───────────────────────────────┐ │ │ Local Network │
│ │ WebView — front Preact │ │───────>│ REST /api/v1 + WebSocket │
│ │ routes/ · components/ │ │ │ Bearer token, min. latency │
│ │ ───────────────────────────── │ │ │ mDNS discovery │
│ │ utils/native/ — abstraction │ │ └──────────────┬───────────────┘
│ │ no-op on the web │ │ │
│ └───────────────┬───────────────┘ │ ┌──────────────▼───────────────┐
│ ▼ │ │ Gladys Plus — remote │
│ ┌───────────────────────────────┐ │───────>│ gladys-gateway-js │
│ │ Capacitor Native Shell │ │ │ RSA + ECDSA, end-to-end │
│ │ push · geolocation · biometrics│ │ │ The server decrypts nothing │
│ │ mDNS · storage · widgets │ │ └──────────────┬───────────────┘
│ └───────┬───────────────┬───────┘ │ │
└──────────┼───────────────┼──────────┘ │
▼ ▼ ┌──────────────▼───────────────┐
┌────────────┐ ┌────────────┐ │ Gladys Instance │
│ Android │ │ iOS │ │ Node 24 · SQLite │
│ Kotlin·FCM │ │ Swift·APNs │ │ 39 services │
└────────────┘ └────────────┘ └──────────────────────────────┘
Network path selection is automatic and re-evaluated with each network change; the user can force it from the settings. The compiled front is identical to the one served on the web: only the utils/native/ layer is added, and it returns neutral implementations outside of mobile.
Directory Structure
A new mobile/ directory at the root, next to front/ and server/:
mobile/
├── capacitor.config.ts configuration, points to front/build
├── package.json Capacitor dependencies only
├── android/ generated Android project, versioned
├── ios/ generated Xcode project, versioned
├── plugins/ custom plugins (mDNS, discovery)
└── resources/ source icons and splash screens
front/src/utils/native/ abstraction layer, in the front
├── index.js platform detection
├── push.js
├── geolocation.js
├── biometrics.js
├── secureStorage.js
└── discovery.js
Plugins retained
| Need | Plugin | Origin |
|---|---|---|
| Push notifications | @capacitor/push-notifications |
Official |
| Local notifications | @capacitor/local-notifications |
Official |
| Point geolocation | @capacitor/geolocation |
Official |
| Background geolocation | @capacitor-community/background-geolocation |
Community |
| Encrypted storage | capacitor-secure-storage-plugin |
Community |
| Biometrics | @aparajita/capacitor-biometric-auth |
Community |
| Network status | @capacitor/network |
Official |
| App lifecycle | @capacitor/app |
Official |
| Status bar and notches | @capacitor/status-bar |
Official |
| mDNS discovery | gladys-discovery |
To be written |
Design choice — Each community plugin is a maintenance risk. The
utils/native/layer exists precisely so that replacing an abandoned plugin only impacts one file, never the screens.
4. Connection and authentication
This is the most structuring part of the specification. The current front end only knows one mode at a time, decided at build time by the GATEWAY_MODE variable. The mobile app must handle both simultaneously and switch between them without intervention.
4.1 Runtime configuration
front/src/config.js reads process.env, which Vite replaces with literals at build time. Runtime resolution needs to be introduced:
- On the web, the current behavior is preserved exactly: no regression.
- On mobile, the URLs come from the active connection profile, stored in encrypted storage.
In practice, config.js exposes a mutable object populated at startup by utils/native/, before the first render. The modules that import the configuration today remain unchanged.
4.2 Connection profiles
The app manages multiple profiles — a main home, a secondary residence, a test instance. Each profile records:
| Field | Content |
|---|---|
id |
Local UUID |
name |
Display name, entered by the user |
mode |
local, gateway or auto |
localUrl |
URL of the instance on the local network |
localFingerprint |
Certificate fingerprint, if self-signed HTTPS |
ssids |
Wi-Fi networks where the local mode applies |
credentials |
Reference to encrypted storage, never the value |
4.2 bis The three Gladys Plus URLs
Not to be confused — they have distinct roles:
| URL | Role | Used by |
|---|---|---|
https://api.gladysgateway.com |
The API of the gateway | config.gladysGatewayApiUrl, called by gladys-gateway-js |
https://plus.gladysassistant.com |
The web front end of Gladys Plus (the same code, in gateway mode) | Outgoing links: subscription, billing |
https://gladysassistant.com/plus/ |
Marketing page | utils/gladysPlusUrl.js (subscription links) |
The mobile app talks to the API, never to the hosted front end: it is the front end.
The default value of config.js is therefore already correct and does not need to be
modified.
However, the flows that cannot run in the app — subscription management, Stripe billing — must open plus.gladysassistant.com
in the system browser, and not in the WebView: a payment tunnel in an app WebView is rejected by both stores. The plugin
@capacitor/browser (system tab) is the right vehicle.
4.3 Local instance discovery
The Gladys server already publishes an mDNS service (server/lib/mdns/). The gladys-discovery plugin to be written queries this service and presents the found instances:
- Android:
NsdManager, with acquisition of aMulticastLockduring the search. - iOS:
NWBrowser(Network framework). Requires the entitlementcom.apple.developer.networking.multicast, to be explicitly requested from Apple, and the declarationNSBonjourServicesinInfo.plist.
If discovery fails, manual entry of an address remains always possible: it is never a hidden fallback, but an option visible from the first screen.
4.4 Local ↔ Remote switching
In auto mode, the app chooses the network path on each startup, on each return to the foreground, and on each connectivity change reported by @capacitor/network:
- If a local profile is configured, a
GET /api/v1/pingrequest is attempted with a 1.5-second timeout. - If successful, the local mode is retained: minimal latency, no external dependency.
- If it fails and a Gladys Plus account is linked, the app switches to the gateway.
- If no path responds, an offline screen is displayed with the last known data and a resume button.
The switch rebuilds the HTTP client and the WebSocket connection. It is discreetly indicated in the interface (an indicator in the header), never by a blocking dialog.
Warning — The two modes do not use the same token store or the same client:
Session+HttpClientfor local,GatewaySession+GatewayHttpClientfor Gladys Plus. The switch must clear the in-flight request caches ofHttpClient(thependingRequestsMap) to avoid a response from the old path being attributed to the new one.
4.5 End-to-end encryption
GatewaySession relies on @gladysassistant/gladys-gateway-js, which receives window.crypto. In a WebView, the Web Crypto API is only available on a secure context: the capacitor:// (iOS) and https:// (Android) schemes are, unlike http://. This point needs to be validated as early as batch 1, as it conditions all remote access.
The serialized keys (gateway_serialized_keys) are currently in localStorage. On mobile, they go into the encrypted storage backed by the Keychain (iOS) or the Keystore with hardware encryption (Android).
4.6 Two-factor authentication
The existing 2FA flow (actions/login/loginGateway.js) is preserved without modification, including the generation of recovery codes and pasting from the clipboard. Automatic filling of the code from keyboard suggestions is added via the attribute autocomplete="one-time-code".
4.7 Biometric lock
Optional, activatable in settings. When active, unlocking by fingerprint or facial recognition is requested at launch and after a configurable inactivity delay (default 5 minutes in the background). The fallback is the device code; there is never a Gladys-specific code to remember in addition.
5. Native capabilities
5.1 Push notifications
This is the main contribution of the app compared to the web. It involves development on the Gladys server side, not just on the mobile side.
Registration. On the first launch after accepting the permission, the app obtains an FCM (Android) or APNs (iOS) token and sends it to the instance. New route to create: POST /api/v1/user/push_token, with the token, the platform, and the session identifier. The token is linked to the existing session: revoking a session revokes the associated push.
Sending. A self-hosted Gladys instance cannot speak directly to FCM or APNs without service keys — which cannot be distributed in a public image. Two paths:
- Via Gladys Plus (recommended) — The instance transmits the notification to the gateway, which holds the keys and relays to FCM / APNs. The useful content is end-to-end encrypted; the transported notification only contains a trigger, the app retrieves the actual content from the instance upon receipt.
- Without Gladys Plus — Fallback on local notifications: as long as the WebSocket is alive, the app schedules a notification itself. Works in recent background, not after prolonged sleep. This limit must be clearly announced in the settings, not discovered in use.
Triggering from a scene. A new scene action user.send-push-notification is added, with recipients, title, message, and optionally a camera image. It must be declared in the Joi schema of server/models/scene.js, otherwise the scene recording will fail with a 422 error without an explicit message.
Quick actions. Notifications include up to three action buttons, defined in the scene: run another scene, turn a device on or off, open a camera. These actions are processed without opening the app when the network allows.
5.2 Geolocation and Presence
Gladys already has server/lib/location/ and zone management. The app feeds POST /api/v1/location:
- Zone-based tracking rather than continuous tracking: the app subscribes to entries and exits of zones defined in Gladys. Battery cost is much lower than periodic polling.
- Reduced precision by default: the position is only transmitted when crossing a zone, not continuously.
- Offline queue: events captured without a network are stored locally and sent upon reconnection, timestamped with their actual date.
- Global switch visible in settings, and immediate stop of tracking when turned off.
Store constraint — Background geolocation is the most frequent rejection reason on the App Store. You need: a clear explanation before the permission request, a precise justification in
NSLocationAlwaysAndWhenInUseUsageDescription, and a fully functional app if the permission is denied. On Android 13+, theACCESS_BACKGROUND_LOCATIONpermission is requested in a second step, after the foreground permission.
5.3 Home Screen Widgets
Written natively: WidgetKit in SwiftUI on iOS, Glance on Android. They read a data snapshot written by the app in a shared space (App Group on iOS, SharedPreferences on Android), refreshed every time the app comes to the foreground and with each notification.
- Scenes: up to four favorite scenes, executable with a tap.
- Temperature: temperature of a chosen room.
- Devices: state and toggle of two to four devices.
Widgets do not open their own session: they delegate to the app, which executes the action. If the session has expired, the widget displays a state requiring reconnection rather than an error.
5.4 System Shortcuts
- iOS: exposure of scenes in App Intents, making them available in Shortcuts, Siri, and the action button.
- Android: dynamic shortcuts on the app icon for favorite scenes.
5.5 Voice Assistant
The front already has a complete voice chain: speechCommandRecorder.js, recordUntilSilence.js, speechTtsPlayback.js, backed by gateway.stt.js and gateway.processVoiceMessage.js on the server side.
In WebView, microphone access requires native permission (NSMicrophoneUsageDescription, RECORD_AUDIO) and authorization at the WebView level. On Android, the latter goes through onPermissionRequest, to be handled in the native shell. TTS playback must configure the audio session category on iOS to not be cut by silent mode.
5.6 Cameras
The HLS stream is read via hls.js with a custom loader injecting the authentication token. In WebView:
- On iOS, the native HLS of
<video>It does not allow adding headers;hls.js` via MSE remains necessary. MSE compatibility in WKWebView needs to be checked early. - Video fullscreen requires
allowsInlineMediaPlaybackand explicit rotation management. - Playback must pause when going to the background and resume upon return, to avoid unnecessarily consuming battery and data.
6. Interface Adaptations
The interface remains that of the web front end. Adaptations are targeted, and none should degrade the browser experience.
Safe Areas and Notches. Application of env(safe-area-inset-*) on the header, bottom navigation, and modals. The viewport switches to viewport-fit=cover in front/src/template.html, with no effect on the web.
Navigation. The current sidebar becomes a bottom tab bar on narrow screens: Dashboard, Devices, Scenes, Chat, Settings. The Android back gesture is linked to preact-router via @capacitor/app, with an exit confirmation only at the root.
Touch Targets and Gestures.
- Any interactive target is at least 44 × 44 points.
- The dashboard drag-and-drop uses the tactile backend of
react-dnd, already present, with a long press to arm the movement to avoid conflict with scrolling. - Pull-to-refresh on list screens, disabled during an ongoing edit.
Keyboard. View resizing on keyboard open rather than overlay, adapted input types (inputmode="numeric" for codes, type="email"), and automatic scrolling to the active field.
Dark Mode. Gladys’ dark theme relies on global CSS inversion, with the dark-mode-no-invert class for elements that should keep their true colors. This mechanism is preserved as is. Two new points: the native status bar color must follow the theme, and inversion should not apply to the native splash screen.
Tablet Mode. The existing tablet mode (routes/dashboard/SetTabletMode.jsx) makes perfect sense on a wall-mounted tablet: we add screen wake lock, orientation lock, and an immersive full-screen mode. The existing code lock is preserved.
7. Security
Secret Storage
No secrets remain in localStorage on mobile. Migrate to encrypted storage: access and refresh tokens, serialized Gladys Plus keys, public key fingerprints, two-factor authentication token. Non-sensitive preferences remain in regular storage: language, dark mode, selected home.
Plain HTTP on Local Network
A local Gladys instance is often served via http://. However, Android blocks plain traffic by default since version 9, and iOS via ATS.
| Platform | Mechanism | Scope |
|---|---|---|
| Android | network_security_config.xml |
Clear traffic authorization restricted to private ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, plus .local |
| iOS | NSAllowsLocalNetworking |
ATS exception limited to the local network, without global disabling |
There is never any question of NSAllowsArbitraryLoads or global cleartextTrafficPermitted: these two settings disable protection across all of the internet and are a reason for Apple review rejection.
Self-Signed Certificates
For a local HTTPS instance with a self-signed certificate, the app offers to pin the certificate fingerprint when adding the profile, with the fingerprint displayed for verification. No blind acceptance; a fingerprint change blocks the connection and requires explicit confirmation.
Content Protection
- Content masking in the app selector (
FLAG_SECUREoptional on Android, overlay view on iOS), activatable in settings. - No camera stream screenshots in system previews when the option is active.
- Complete clearing of encrypted storage upon logout.
What the App Does Not Do
- No analytics, advertising, or tracking SDK.
- No automatic third-party crash reporting; if needed, manual, explicit, and consented sending.
- No home data transits through a server other than the user’s instance or Gladys Plus, whose content is end-to-end encrypted.
8. Identified Hard Points
These seven points are those that can derail the schedule. They are deliberately placed as early as possible in the phasing, so that their real cost is known before investing in the rest.
| Point | Risk | Treatment |
|---|---|---|
| Web Crypto in WebView | If window.crypto.subtle is unavailable, all remote Gladys Plus access falls. |
Validation prototype in the first week, on both platforms. The capacitor:// and https:// schemes are secure contexts: the risk is low but the total impact. |
| Build-Time Frozen Configuration | config.js is resolved by Vite at build: the app cannot change instances. |
Refactor to runtime-resolved configuration, with strict preservation of current web behavior. |
| Push Without Gladys Plus | A self-hosted instance has no FCM / APNs keys. | Relay via the gateway for Gladys Plus accounts; local notifications as fallback, with their limits explicitly announced. |
| iOS Multicast | mDNS discovery requires an entitlement granted case by case by Apple, with unpredictable delays. | Request submitted as soon as the developer account is opened. Manual address entry is a first-class path, not a fallback. |
| Background WebSocket | Both OSes cut connections in standby; the displayed state may be stale upon wake-up. | Reconnection with backoff upon returning to the foreground, complete state reload, and data freshness indicator. |
| HLS in WKWebView | MSE playback via hls.js may behave differently from Safari. |
Test on real device in batch 1, before any commitment on batch 4. |
| Mixed Content (discovered in testing) | The WebView served via https://localhost cannot call either http:// or ws:// on the local instance: Chromium blocks. The Android network security configuration does not change anything — it is a browser policy, not a platform policy. CapacitorHttp handles the case of HTTP requests, but not that of WebSockets, which remain subject to the rule. |
See the arbitration below: batch 1 switches to androidScheme: "http". |
Arbitration: WebView Scheme
The choice of scheme opposes two capabilities that cannot be obtained together
as long as the instance is in HTTP:
androidScheme |
Secure Context | Crypto / Gladys Plus | Local HTTP | Local WebSocket |
|---|---|---|---|---|
https |
yes | OK | via CapacitorHttp | BLOCKED |
http |
no | unavailable | OK | OK |
Decision for batch 1: http. The batch targets connecting to the
local instance, which requires the WebSocket; Gladys Plus is not yet connected there. This
choice is explicitly temporary.
Must be revisited before the Gladys Plus batch. Two paths:
- Instance in HTTPS (TLS reverse proxy: Caddy, nginx, Traefik). Everything goes
tohttps/wss, no more blocking, secure context preserved. This is the
only solution that will also work for iOS, where thecapacitor://scheme imposes
the same constraints. Recommended path. - Native WebSocket Plugin, opening the connection outside the WebView like
CapacitorHttpdoes for requests. Allows staying inhttps, but requires modifyingSession.js, shared with the web.
Practical consequence: as long as lot 1 is in
androidScheme: "http",
crypto.subtleis unavailable in the app and any attempt to connect
Gladys Plus will fail. The validation prototype, on the other hand, did measure crypto
inhttps— the platform’s capability is proven, but its coexistence
with a plaintext backend is not.
| App Store Review | Background geolocation and « client of a service » app are two classic rejection reasons. | Demo mode accessible to reviewers without an instance, carefully written permission justifications, screenshot of the real use of the areas. |
9. Delivery Lots
The lots are truly sequential: each builds on the previous one and ends with a deliverable that can be installed on a real device. Lot 1 deliberately concentrates the technical risks.
Lot 1 — Foundation and Authentication
DELIVERED
Goal: prove that the approach works, and address the tough points 1, 2, and 6.
Commit feat(mobile): resolve the instance to connect to at runtime —
22 files, 1254 insertions, 14 deletions.
Configuration resolved at runtime (setRuntimeConfig, whitelist of keys)
utils/native/layer — eliminated from the web bundle by tree-shaking
Secure storage (Keychain / Keystore, fallback localStorageon the web)
Multi-instance profiles: list, add, edit, delete, toggle
First connection screen, with address test before registration
« Instances » settings page, hidden on the web (nativeOnly)
Lost connection indicator in the header
Safe areas: viewport-fit=cover+ top bar and navigation drawer
Web non-regression manually verified
Gladys Plus connection not functional — see the scheme arbitration, section 8
iOS project not generated: no macOS machine available
Validated on device (Xiaomi, Android 16, Chromium 151 WebView): complete configuration path, connection to the local instance, login, dashboards, cameras.
Checks: the three CI checks pass (Prettier, ESLint 0 errors, fr/en/de translation parity), plus 43 logic tests executed on the real code in the repository.
Lot 2 — Functional Parity and Tactile Ergonomics
Goal: an app usable on a daily basis, without yet the native features.
- Safe areas, low tab bar, Android back gesture
- Dashboard drag-and-drop validated with fingers
- Keyboard behavior, pull to refresh, touch targets
- Status bar and startup screen matched to dark mode
- Screen-by-screen review of the 23 existing routes on phone and tablet
Lot 3 — Notifications and Presence
Goal: the first concrete reason to install the app rather than open the browser.
- Server:
POST /api/v1/user/push_tokenroute, linked to sessions - Server:
user.send-push-notificationscene action, Joi schema included - Notification relay by Gladys Plus, end-to-end encrypted content
- Local notifications as a fallback without Gladys Plus
- Quick actions in notifications
- Geolocation by zones, offline queue, global switch
- Biometrics and lock after inactivity
Lot 4 — System Integration
Goal: make Gladys accessible without even opening the app.
- mDNS discovery:
gladys-discoveryplugin, Android and iOS - Home screen widgets: scenes, temperature, devices
- iOS App Intents and dynamic Android shortcuts
- Voice assistant: microphone permission in WebView, iOS audio session
- Cameras: full screen, rotation, stop in background
- Wall tablet mode: screen kept on, locked orientation, immersive full screen
Lot 5 — Publication
Goal: the app is installable from the stores and updates itself.
- CI chain: Android and iOS build and signing
- Play Store and App Store listings: descriptions, screenshots, privacy policy
- Demo mode for reviewers, without requiring an instance
- Internal testing then open beta: Play Console and TestFlight
- User documentation and contribution documentation
Scheduling — Lots 1 and 2 already produce an app distributable internally. If the tough points of lot 1 prove more costly than expected, the arbitration is made at this point, before engaging the native development of lot 3.
10. Build, CI, and Publication
Build Chain
New npm scripts at the root, in line with existing scripts:
build-mobile vite build targeted mobile, then npx cap sync
mobile:android opens the project in Android Studio
mobile:ios opens the project in Xcode
mobile:live hot reload on connected device
The mobile build reuses front/vite.config.mjs with a dedicated mode: the URL configuration is no longer injected at build, and the service worker is not copied.
Versioning
The app version follows that of Gladys (package.json root, today 5.0.2), with an incremental build number specific to mobile. Android’s versionCode and iOS’s CFBundleVersion are automatically derived in CI.
Instance Compatibility
An up-to-date app can connect to an older instance. utils/instanceVersion.js already handles this detection on the web: it is extended to properly disable features that require a minimum server version, particularly push, rather than letting a call fail without explanation.
Continuous Integration
A dedicated GitHub Actions workflow, separate from the existing CI:
- On each pull request affecting
mobile/orfront/: Android build in debug mode, without signing. - On each version tag: signed Android and iOS build, deposit on internal test channels.
- Signing secrets in repository secrets; no keys in the repository itself.
- Existing checks remain applicable to the front: Prettier then ESLint, and 100% patch coverage for any added server code.
Accounts and Costs
| Item | Nature | Cost |
|---|---|---|
| Apple Developer Account | Organization | $99 / year |
| Google Play Account | Organization | $25 one-time |
| Firebase Project | FCM only | Free |
| iOS Build Machine | macOS, CI or local | Variable |
11. Testing and Acceptance
What CI Covers
- The existing Cypress tests continue to run on the web front: they protect against regressions introduced by the configuration refactoring.
- Mocha unit tests on the added server code (push token route, scene action), with
TZ=UTCenforced. - The Android debug build serves as a smoke test on the Capacitor integration.
Manual Testing on Device
A test matrix is kept up to date for each lot. Scenarios that cannot be automated and must be manually verified:
| Scenario | What is Verified |
|---|---|
| Wi-Fi → Mobile Data Switch | Automatic switch from local mode to Gladys Plus, without visible disconnection |
| Return to foreground after a night | WebSocket reconnection, refreshed state, no stale data displayed as current |
| Notification, app closed | Reception, opening on the correct screen, quick action executed without launching the app |
| Zone entry and exit | Presence event reported, including after a network outage |
| Permission denied | The app remains fully usable, with a message explaining what is disabled |
| Live camera | Playback, full screen, rotation, clean stop when going to background |
| Dark mode | Consistency of the status bar, startup screen, and non-inverted areas |
| Offline instance | Offline screen, no aggressive reconnection loop, manual recovery possible |
Measured Results — August 30, 2026
On Xiaomi 2412DPC0AG, Android 16 (API 36), Chromium 151 WebView, against a
real instance in HTTP on the local network:
| Test | Result |
|---|---|
crypto.subtle in https://localhost context |
available |
| RSA-OAEP 2048 / SHA-256 — generation | 104 ms |
| ECDSA P-256 — generation, signature, verification | < 1 ms |
exportKey('jwk') (key storage) |
OK |
| PBKDF2 100,000 iterations | 15 ms |
| AES-GCM 256, tagLength 128 | < 1 ms |
| WebSocket to HTTP instance (handshake) | 44 ms |
MSE — Hls.isSupported() |
available |
| Full walkthrough: configuration → login → dashboards → cameras | functional |
PBKDF2 is three times faster on the phone than on a desktop PC
(15 ms vs. 45 ms): the Android implementation is hardware-accelerated. The
risk of slow unlocking, considered in section 5.7, does not exist.
Method lesson. The initial validation prototype gave three green lights and yet detected none of the two real blockages encountered
later (mixed-content on HTTP requests, then on the WebSocket). It tested a WebSocket from anhttp://localhostpage — therefore out of
mixed-content — and made no API requests to the instance. You must validate crypto, WebSocket and a real API request in the final schema configuration, never in isolation.
Minimal test fleet
- A recent Android and an old Android (API 26 to 28), for network security configuration.
- An iPhone with a notch and an iPad, for safe areas and tablet mode.
- A local Gladys instance in
http://and an instance linked to Gladys Plus.
12. Out of scope for v1
These elements are explicitly excluded. Mentioning them avoids them being introduced along the way.
- Full offline mode. The app displays the last known data, but does not replay commands issued offline. Only presence events are queued.
- Apple Watch and Wear OS apps. Widgets and system shortcuts cover most of the need at a lower cost.
- Configuration of complex integrations from mobile: Zigbee, Matter, Z-Wave pairing. Viewable, but configured from the web.
- Visual overhaul. The app uses the current interface. A mobile-first overhaul is a separate project.
- Support for Android 7 and earlier, and iOS 14 and earlier.
- Repair of the web service worker. The current behavior is preserved; improving the PWA remains a separate topic.
Questions to resolve before starting
- Does the notification relay go through the existing Gladys Plus infrastructure, or is a dedicated service needed? This conditions lot 3.
- Are developer accounts opened in the name of the Gladys project or personally? This has lasting consequences for the ownership of the records.
- Is the app reserved for Gladys Plus subscribers for notifications, or is the local fallback sufficient for the promise made to users?
- Is a macOS machine available for iOS CI, or should a hosted build service be planned?
Specification established from the Gladys 5.0.2 repository: Preact / Vite front-end (front/), Node 24 server (server/, 39 services, 25 API controllers). The file paths mentioned correspond to the actual repository structure at the time of writing.
