# One app for the field: building Ritwik One as a super app

Field staff need trips, travel claims, bills and approvals, with HR and sales on the way. How we built Ritwik One as a thin Android shell over one web app, with a typed bridge, native background GPS, and a release pipeline that ships the exact image we tested.

By Nikhil Kumar, AI/ML Engineer, Digital Transformation and Alliances Department · 14 November 2026 · Ritwik Finance Enterprises Private Limited
Source: https://blogs.ritwikfinance.com/ritwik-one-super-app/

*I designed and built the Ritwik One Android app, the Travel and Finance modules inside it, and the pipeline that releases them. This post explains how the pieces fit together.*

A field officer at Ritwik Finance starts the day on a two-wheeler, visits customers, ends the trip, and claims the kilometres. A branch raises a bill for the electrician. A manager approves both from the road. Accounts pays the vendor and records it. HR and sales tools are coming next.

We could have given each of those its own app. That would mean several installs, several logins, several updates to chase across every field officer's phone, and a new app to explain every time a module opened. Instead there is one: **Ritwik One**, a staff super app with a tab per module: Travel, Finance, and HR and Sales marked "Soon". This post is about the decisions that keep one app manageable for a small team.

## Part 1A thin native shell around one web app

The biggest decision was how much to build natively. We chose as little as possible. Ritwik One is an Expo (React Native) app whose main screen is a WebView showing the same web app staff can open in a browser. Sign-in, every screen and every rule come from the web app. The native side does only what a web page cannot:

- **record GPS with the screen off**, in an Android foreground service, into a SQLite spool;
- **check the phone is set up** for a trip: location allowed all the time, battery optimisation off;
- **raise an alarm** when GPS stops arriving;
- **open stored files** such as bill PDFs in the phone's own viewer.

The payoff is speed. A fix to a screen or a rule reaches every phone with the next web deploy, with no store update and no "please update the app". Staff without the app can still use a browser. And a module like Finance arrives in the app the day it ships on the web.

[Diagram: The Android app holds a WebView showing the web app, with module tabs for Travel, Finance, HR and Sales. Below it, native services handle background GPS, the SQLite spool, phone setup checks, alarms and files. Messages such as "trip started" go down from the page to the native side, and the tracking status comes back up, through a bridge validated on both sides. The web app talks to Google Cloud Run in Mumbai with the session cookie; the native recorder uploads GPS batches with a trip token.]

*Figure 1. Ritwik One. Everything a user sees is the web app; the native layer below it does only what a web page can't. Messages cross the bridge in both directions and are checked on both sides.*

A shell that shows web pages needs firm rules about where it will go. Pages from our own server stay in the app. Any other https site opens in an in-app browser tab, so it can never pretend to be part of Ritwik One. Phone, email and SMS links go to the phone's own apps. Everything else is blocked. Every request carries the app's version and platform, so the server knows which app it is talking to.

## Part 2A typed bridge between the page and the phone

The web page and the native code are written separately and released separately: the web whenever a change is ready, the app through the Play Store. They meet in one place, the bridge, and most of the design went into keeping that meeting safe when the two sides are not on the same version.

Before any of the page's own scripts run, the app injects a small frozen object with its version, its platform and a list of **capabilities** such as background location and phone setup. The page treats itself as "inside the app" only if that object exists and passes validation. From then on, the two sides talk in small named messages:

| Direction | Message | What it carries |
| --- | --- | --- |
| Page → app | Trip started | the trip, its upload token, the next sequence number |
| Page → app | Flush | upload everything now (before a trip ends) |
| Page → app | Trip ended, signed out | stop recording, forget the token |
| Page → app | Setup check, open settings | is the phone ready for a trip? open the settings |
| App → page | Tracking status | recording state, points waiting, last sync, next sequence number |
| App → page | Setup status | ready or not, item by item |

Every message is checked against a schema on *both* sides, and anything that doesn't match is ignored rather than half-handled.

A few more rules keep it predictable. The app accepts messages only from pages served by its own server. Trip messages are handled one at a time, in order, so a quick "start, flush, end" can never be processed out of sequence. And instead of a protocol version number, the page asks for capabilities: a newer page simply doesn't offer phone setup on an older app that doesn't list it, and setup items are plain strings, so a new check never breaks an older page.

## Part 3Native only where the web can't reach

**Background GPS.** The heart of the native side is the trip recorder: a foreground service that records a fix every 5 seconds with the screen off, spools points in SQLite, and uploads them in batches with a trip token kept in Android's encrypted secure storage. The full story, including how points are numbered so retries and hand-overs are safe, is in [our post on offline-first GPS](https://blogs.ritwikfinance.com/offline-first-gps-travel-claims/).

**Phone setup.** Some Android makers kill background apps aggressively. Before a trip, the page asks the app whether the phone is ready, and the app checks for itself, without trusting the page: location allowed all the time and battery optimisation off are required; precise location and notifications are recommended. A small Kotlin module reads the battery settings and opens the right screen, and the app shows tips for Xiaomi, Oppo, Vivo and Samsung.

**A dead-man's switch.** Every batch of fixes pushes a local notification a few minutes into the future. While GPS flows, the alarm never rings. If it stops, the employee gets "GPS stopped" and one tap back to the trip.

**Files.** A WebView can't open a bill PDF the way a browser can. When the page links to a stored file, the page fetches it with its own signed-in session and hands the bytes to the app, which saves them to its cache and opens the phone's viewer.

**The small things.** Verified App Links open our links straight in the app. The camera opens straight away for our own pages, and any other page has to ask. And because everything runs in Android System WebView, the app asks users to update it if it is older than Chrome 111.

## Part 4One switch for development and production

A single build setting decides which server the app talks to and what it is called. The development build is "Ritwik One Dev" with its own app id and points at our development server; the production build is "Ritwik One" and points at production. Both can sit on one phone side by side, which makes testing a release against real phones painless. Builds run on EAS, which also keeps the version numbers and raises them automatically. WebView debugging is switched on only in development builds, and crash reporting catches native crashes without sending personal data or screenshots.

## Part 5Shipping the web side: the image we test is the image we run

With a thin shell, the web app *is* the product, so its release pipeline matters as much as the app's. It runs on Google Cloud Run in Mumbai and follows three rules.

[Diagram: Five steps: build one image with a software bill of materials and a vulnerability gate; deploy it as a new revision with no traffic; smoke-test it before it takes traffic; switch all traffic to it; for production, copy the same image by digest, check the digest matches, and deploy it the same way.]

*Figure 2. Every deploy is a candidate first. Production never gets a fresh build: it gets the exact image development ran, checked by digest.*

- **One image for every environment.** Settings that differ between development and production, such as map keys and the storage address in the content security policy, are read when the server starts, not baked into the build.
- **Zero-downtime deploys.** A new revision starts with no traffic, is smoke-tested first, and only then takes all the traffic. If it fails, users stay on the current revision without noticing.
- **Production runs the tested image, byte for byte.** A numbered release copies the development image to production by its digest and checks the digest before deploying.

The last rule came from a real failure. Our first production release stopped at the deploy step: the image it asked for didn't exist. The development image had been built as an OCI image, and copying it with `docker pull` and `docker push` had quietly re-saved it in Docker's format, with a new digest. Production was asked for the tested digest and had only the re-saved one. The fix was to copy registry-to-registry with `skopeo copy --preserve-digests` and stop the release if the digests don't match. Since then, "it worked on dev" is a statement about the same bytes.

## Part 6Keeping two codebases honest

Each side guards its half of the contract. In the app, one command runs linting, formatting, type checks, dependency-boundary rules and the tests, and CI builds the app for both development and production on every change. The tests run in Node with no phone attached, using an in-memory store for the spool: batches of 200 with the token, points kept when offline, a 409 that stops and clears, a bad token that keeps the points. The web side tests its copy of the bridge schemas the same way.

## What's next

The bridge contract will move into one shared package used by both sides. And when the HR and Sales tabs open, they will open in the app the same day they open on the web, with no store update, which was the point of building it this way.

## What I'd tell anyone building a super app

> Build natively only what the web can't do. Everything else ships faster as the web.

- **Make the bridge a contract**: named messages, validated on both sides, processed in order.
- **Ask for capabilities, not versions.** Old apps and new pages keep working together.
- **Let the native side verify for itself**, like phone setup, instead of trusting the page.
- **Release the bytes you tested.** Copy images by digest and check it.

---

Published 14 November 2026.

About the author: Nikhil Kumar, AI/ML Engineer, Digital Transformation and Alliances Department, Ritwik Finance Enterprises Private Limited. Nikhil designed and built these parts of Ritwik One, the staff super app of Ritwik Finance: Travel: field trips, GPS tracking, customer visits and travel claims; Finance: vendors, bills, petty cash and vendor payments; AI document reading: bill OCR and odometer photo checks; The Ritwik One Android app and its background GPS; The release pipeline and production setup on Google Cloud.
