12-Oct-2026 — NGX Ramblers mobile: Capacitor, background recording and releases


NGX Ramblers shares its Angular application between the website, an installed progressive web app (PWA), and the iPhone and Android apps. Capacitor packages that web interface inside a native application and connects it to phone capabilities. The main reason for the native recording implementation is straightforward: a walk needs to keep recording when the phone is locked and the web interface is suspended.

This article describes the implementation as it stands in October 2026, including the separate release paths for the website and the apps.

Why NGX uses Capacitor

NGX already has an Angular interface for choosing walks, displaying maps, following routes and recording a route. Making that website installable as a PWA gives members a home-screen shortcut, but it still runs under the browser's rules. When the phone locks, the browser can suspend the page and its location callbacks. That's a problem for recording a route when the phone is in your pocket.

Capacitor is an open-source runtime that lets an existing web application run inside an iOS or Android application. It supplies the native application shell and a bridge between JavaScript and the phone's native APIs. A WebView is the embedded browser surface that displays the Angular interface; a plugin exposes a native operation that the TypeScript application can call. The interface remains HTML, CSS and Angular, while selected operations run as native phone code.

For NGX, this means we can reuse the same map screens, route controls, site chooser and API services across the website and both phone apps. We do not need to rewrite those screens separately in Swift and Java. The native projects add the platform-specific parts: application identity, permissions, signing and the recording plugin. Capacitor also provides the native HTTP bridge used to communicate with the selected NGX site.

The distinction matters for background recording: Capacitor provides the connection to native code; NGX's recording plugin does the recording. Packaging the website in Capacitor alone would not make browser geolocation continue indefinitely when the screen locks. Our Swift implementation uses iOS Core Location, and our Java implementation uses an Android foreground location service. Those native recorders write positions to local storage while the web interface is suspended, then give the accumulated points back to Angular when it resumes.

Capacitor therefore gives NGX an installable, signed app for TestFlight, the App Store and Google Play, while preserving the shared web interface and runtime area/group selection. It introduces a separate native build and release step, but does not require a different application for each NGX site. The following diagram shows where the web interface, Capacitor bridge and native recorder sit. Capacitor's official introduction describes the runtime and plugin approach.

One interface, two ways to run it

A PWA loads the application from a website. Its service worker caches the web application so it can open from a home-screen shortcut. It runs within the browser's capabilities and lifecycle.

The store app carries a compiled copy of the Angular interface inside its installation. Capacitor displays those files in a native WebView, with a plugin bridge for capabilities such as background recording. It does not download the chosen group's website and display that entire website inside a frame. The bundled interface requests configuration, walks, routes and other data from the chosen NGX site's API.

sequenceDiagram
    box rgb(234,242,251) On the phone - JavaScript
        participant UI as Angular in WebView
    end
    box rgb(232,245,238) On the phone - native code
        participant C as Capacitor bridge
        participant N as NGX recording plugin
        participant J as Local journal
    end
    box rgb(253,243,231) Across the internet
        participant S as Selected NGX server
    end
    UI->>S: Usual HTTPS API request - load walks or routes
    S-->>UI: Usual JSON response
    Note over UI,N: User taps Start recording - local plugin calls begin
    UI->>C: start({sessionId, reset})
    C->>N: Dispatch to Swift or Java start method
    N->>N: Ask operating system for GPS updates
    N-->>C: Success or permission error
    C-->>UI: Resolve or reject JavaScript Promise
    N->>J: Store a GPS point from the operating system
    N-->>C: positions event - new data is available
    C-->>UI: Invoke registered JavaScript listener
    UI->>C: positions({sessionId, after})
    C->>N: Read points newer than last timestamp
    N->>J: Read session journal
    J-->>N: Stored points
    N-->>C: {sessionId, positions: [...]}
    C-->>UI: Resolve Promise with position batch
    UI->>UI: Draw route and update distance
    Note over UI,J: Screen lock can suspend Angular - native recorder still writes journal
    Note over UI,J: On unlock Angular requests missed points through the same bridge
    UI->>S: When user saves - upload recorded GPX over HTTPS
    S-->>UI: Saved route response

Read the diagram from top to bottom. There are two kinds of communication: network requests to the selected site, which work as in the website, and local calls to native phone code, which pass through Capacitor. The recording messages do not go to the web server and are not WebSocket messages. The bridge passes structured values between the JavaScript runtime and native plugin methods inside the same installed application.

Angular calls start({sessionId, reset}) through the plugin's JavaScript API. Capacitor dispatches that call to the registered Swift or Java method and returns its result as a JavaScript Promise. A permission failure rejects the call; a successful call starts the native recorder. Angular still owns the screen and buttons. It has handed off the work of collecting GPS points, not navigation to another screen or website.

The return path has two stages. Native code first writes a point to the journal, then emits a positions event. That event is a notification that data is available, not the point itself. Angular's listener asks for positions({sessionId, after}), and the Promise returns a batch containing latitude, longitude, accuracy and timestamp, plus optional altitude and heading. Angular uses those points to update the map and distance. Native code can also emit a locationError event carrying a code and message.

When Angular is suspended, it cannot process those notifications. Native code keeps writing the journal while recording remains active. On resuming, Angular requests the points after its last received timestamp. This is why storing the points natively matters: the app does not depend on every live notification reaching JavaScript. Pause or stop calls the plugin's stop() method through the bridge; saving flushes pending points and then uses the normal site upload API.

In the PWA, there is no NGX native recording plugin or Capacitor hand-off. Angular calls browser geolocation instead, and the browser invokes its location callback while it is allowed to run. Both versions use the same route display logic and the selected site's API; their location collection mechanisms differ.

The code detects Capacitor's native runtime and the availability of the recording plugin. That selects the appropriate implementation; the member does not need to toggle a “native mode”. The normal production PWA registers the service worker. The native application uses its installed bundle and does not register that web-update mechanism. This follows Capacitor's native runtime and plugin model.

How the app chooses a site

The generic store build starts with a site chooser. A member can choose an area, optionally choose a group, or enter an HTTPS website address for an NGX site being tested before its final domain is ready. A directory entry alone does not guarantee that its website runs NGX: the chosen site must provide the compatible NGX experience.

The app is generic across participating NGX environments. The selected area or group address is a runtime setting remembered on the phone; it is not a reason to build a different app. Direct website entry also allows the same installed app to connect to a test environment.

There is a separate bootstrap dependency: the chooser needs a public directory service to retrieve its area and group choices before a site has been selected. The current build script supplies that directory service address through MOBILE_DIRECTORY_URL. This address does not choose the member's site or bind the app to the directory service's homepage.

Changing site signs the member out of the previous site and separates the cached lists and preferences by site origin. That separation matters because the native app has one local application origin, whereas separate websites naturally have separate browser storage. Saved recording data is retained rather than discarded during a site change. Recovery must respect which site the data belongs to.

Recording with the screen locked

The Angular interface starts and controls a recording session. Once a native recording has started, location collection and durable storage belong to the phone's native layer. Continuous JavaScript execution, an open map and an active server connection are not needed for each GPS point.

sequenceDiagram
    participant UI as Angular recording controls
    participant Bridge as Capacitor bridge
    participant Native as Native location recorder
    participant Store as Local recording journal
    UI->>Bridge: Start session
    Bridge->>Native: Request authorised location updates
    loop Each location update
        Native->>Store: Append and synchronise position
        Native-->>Bridge: Notify that positions are available
        Bridge-->>UI: Invoke listener when UI is running
    end
    Note over UI: Phone locks and WebView may be suspended
    loop While recording remains active
        Native->>Store: Continue collecting and storing positions
    end
    Note over UI: Phone unlocks and UI resumes
    UI->>Bridge: Read positions after last received timestamp
    Bridge->>Store: Drain this session's journal
    Store-->>UI: Positions collected while UI was suspended
    UI->>UI: Update route and distance
    UI->>Bridge: Pause or stop and flush remaining positions
    Bridge->>Native: Stop location updates

Each recording has its own append-only journal: one JSON record per line, synchronised to the device's storage. The bridge can read points collected after a particular timestamp, allowing the interface to catch up without duplicating points it already received. The journals remain local; starting a recording does not stream the walk to the server.

Pausing stops location updates. Resuming continues the session, with the route logic preserving a break rather than joining the route across the pause. Saving flushes pending points and uses the existing route upload API to save the recording to the selected site, subject to the member's permissions.

Background recording still depends on the operating system allowing the native recorder to run. Force-quitting the app, restarting the phone or revoking permissions interrupts that operation. A stored journal can preserve the points already received; it cannot reconstruct points never collected. The Android service currently does not automatically restart after the system terminates it.

What differs in a PWA?

The PWA uses browser geolocation. A home-screen shortcut makes it convenient to open, but does not grant the browser the native background recording service. Browsers can suspend location callbacks and JavaScript when the screen locks. The PWA's saved session state helps recovery; it does not guarantee continuous locked-screen recording.

GPS collection can work without an internet connection. Loading new site data, fetching map tiles and uploading a saved route require connectivity. Offline map viewing depends on the selected provider and successfully downloaded coverage. The route geometry and recording journal do not contain the underlying map tiles.

Building the native apps

The repository keeps the shared application and the two native projects together:

npm run mobile:prepare prepares one generic application. The same resulting installation can connect to different areas, groups and test sites through its first-launch chooser; selecting another site does not require rebuilding it.

In the current implementation, the build environment must provide MOBILE_DIRECTORY_URL for the public area/group directory lookup. This is a bootstrap service endpoint, not the website the member will use. Release builds should not provide MOBILE_SITE_URL: the script still supports that legacy shortcut for local development, but site selection belongs in the app's chooser and it is unnecessary for testing a different environment.

Preparation builds the web application in production mode, copies its output into dist/ngx-mobile, adds the mobile startup configuration, and runs Capacitor synchronisation for iOS and Android. Synchronisation copies the web assets and updates the native dependencies. It does not produce a signed store binary. This is the same separation described in Capacitor's development workflow.

After preparation, npm run mobile:ios opens Xcode and npm run mobile:android opens Android Studio. Simulator and development-device builds are useful during implementation. Distribution adds a separate signing and packaging step: an Xcode release archive for Apple, or a signed Android App Bundle for Google Play.

The website pipeline and the app releases

The release process should begin with a push to main, with no recurring workstation build or manual upload. The website/backend workflow runs its checks, builds a Docker image and deploys to the configured automatic target, normally staging. A companion native workflow, build-and-release-mobile.yml, has been prepared to start automatically when that workflow succeeds on main. It checks out the exact successful source revision, builds the generic mobile bundle once, and passes that bundle to the iOS and Android jobs. Failed web checks prevent the native release from starting.

The iOS job tests the native journal, builds a simulator artifact, then signs, archives and uploads the app using Xcode and App Store Connect API authentication on a hosted Mac. It waits for Apple to validate that exact build, then assigns it to the existing internal pilot group through the API. The Android job tests its journal, builds an Android bundle, then signs and uploads it to Google Play's internal testing track using service-account API authentication. Build numbers and source references are recorded automatically.

There is a one-time setup boundary, not a manual step for each release: the workflow must be committed and pushed, and protected signing credentials, store API access and the pilot group identifier must be configured. Google account verification and initial app setup must also finish. At the time of writing, that setup is incomplete, so automatic uploads are not yet live. Native builds and tests run with uploads disabled by default, and the workflow summary explicitly reports that no pilot upload took place. Enable each platform's upload once its setup is complete; enabled uploads fail on missing credentials or genuine upload errors.

flowchart TB
    SRC@{ icon: "logos:github-icon", label: "Known committed source revision", pos: "b", h: 48 }
    subgraph web["Website and backend release"]
        PUSH["Push to main"]
        CI@{ icon: "logos:github-actions", label: "Checks and Docker build", pos: "b", h: 48 }
        DEPLOY@{ icon: "logos:fly-icon", label: "Deploy selected environments", pos: "b", h: 48 }
        PWA["Browser and PWA receive web update"]
        PUSH --> CI --> DEPLOY --> PWA
    end
    subgraph apps["Automatic native workflow after main pipeline succeeds"]
        PREP["mobile:prepare<br/>Production web bundle and cap sync"]
        IOS["Xcode archive and Apple signing"]
        ANDROID["Gradle bundle and Android signing"]
        TF["TestFlight pilot<br/>Then App Store review"]
        PLAY["Google Play testing<br/>Then production review"]
        PREP --> IOS --> TF
        PREP --> ANDROID --> PLAY
    end
    SRC --> PUSH
    CI -->|"Successful main workflow<br/>same source revision"| PREP
    style web fill:#EAF2FB,stroke:#8FB5DE,stroke-width:2px,rx:12,ry:12,color:#404143
    style apps fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143

Local native builds remain useful for development and do not require an origin push: Xcode and Gradle compile the local files. Routine shared pilot releases should use the automated path from main, with the source revision and build number recorded by the workflow. A push does not change an app already installed on a phone; it produces a new signed build that testers receive through the store's testing channel once processing completes.

Website deployments can immediately change the API, configuration and content consumed by installed apps. Changes to bundled Angular code or native recording code need a new native build. Keep API changes compatible with older installed versions during rollout.

The website's Docker build number and the native app's version/build number are separate. Apple can receive successive builds under the same visible version; Android requires an increasing version code for successive uploads. Record the source revision for both release paths so a reported problem can be tied to the code that actually ran.

Pilots before public release

The first iPhone pilot uses TestFlight. An internal group is suitable for the existing App Store Connect account holder. Wider member testing normally uses an external TestFlight group, which can require Apple's beta review. TestFlight distribution and public App Store publication are separate steps. Apple's TestFlight overview explains the two tester types.

Google Play account verification must finish before its testing tracks can be configured. For a new personal developer account, Google also requires verification using a real Android device. Internal testing is the first small pilot; the production-access process additionally requires a closed test with at least 12 testers opted in continuously for 14 days under the current Google Play requirements.

The useful pilot checks are the ones a desktop browser cannot prove: recording a route with the screen locked, catching up after unlocking, pausing and resuming without false joins, retaining data after reopening, changing sites without showing the previous site's routes, and following directions without scrolling moving the map. Run these on real iPhone and Android devices before public release.

Routine pilot builds and uploads belong in CI. The remaining work is to activate the prepared workflow with the one-time signing and account configuration, then verify the first end-to-end run. Public App Store and Google Play publication still follows each store's review and release process; a successful pilot upload does not automatically publish a public version.