The rep is standing in a godown outside Dewas with one bar of signal, trying to book an order the shopkeeper agreed to five minutes ago. The app was built for a distributor in Indore as twelve screens over one API, and every one of those screens waits for the network before it shows anything. Offline booking was not in the first brief. It is now, and so are reorder suggestions and GST invoice approval, and each request is far cheaper to plan for in month one than to bolt on in month nine.
What breaks when screens talk to the API
The fastest way to build a twelve-screen app is to let each screen call the backend directly. It ships quickly and demos well in an office with good Wi-Fi. Then the first change request lands and you find that the network call, the loading spinner, the retry logic and the business rule about credit limits are all sitting inside the order screen.
Offline booking now means rewriting that screen. So does the stock view, and the outstanding balance card, and anything else that fetched its own data. The same thing happens with an API change: a renamed field on the server becomes a hunt through every screen that read it. And the phones in a field team do not update on your schedule, so for months the backend has to keep answering the old version too.
None of this is a bug anyone can point at. It is the cost of a structure that assumed the network would always be there and nothing would ever be added.
Five layers we keep apart
When an app is expected to change after release, we separate five things and stay strict about it.
- Screensdisplay and input only
- Feature modulesorders, stock, invoices
- Local store and sync queuewhat the handset knows
- API clientOTP login, retries, versions
- Backend servicespricing, credit, Tally sync
Screens never call the backend directly; they read from the local store through a feature module.
Screens display data and collect input, and that is all. A screen does not know what a GST rate is. Feature modules own one product area each, so order booking, stock view and invoice approval are three modules rather than one tangle. The local store holds whatever the device knows, and the sync queue holds what it has changed. The API client handles login, retries and the differences between app versions. The backend does the work that should never run on a phone, which for the distributor means pricing rules, credit limits and the Tally sync.
Yes, it is more code in the first release. The return comes with the second request. When offline booking arrives, only the local store and the queue change, and the order screen has no idea anything happened.
The phone is the source of truth
An offline-first app reads from a database on the phone and sends changes to the server when the network comes back. The screens never wait on a connection, because they never ask it for anything. On patchy 4G between two Tier-2 towns, that is the difference between an app the reps use and one they work around with a notebook.
- 01Editrep changes a record
- 02Save on devicescreen updates at once
- 03Queue changekept until sent
- 04Syncwhen 4G is back
- 05Resolve conflictsrule per record type
The screen updates at the second step; everything after it happens in the background.
The last step is where it gets messy. Two reps visit the same kirana store on the same day, both update its outstanding balance from their phones, and both edits reach the server that evening. If the rule is "last write wins", one rep's work disappears without a trace, and nobody notices until the month-end reconciliation with Tally does not match.
So we ask for a written rule per record type before anyone writes sync code. Some records merge field by field, such as an order line or a delivery note. Some need a person to choose, and for those the app should show both versions and ask. If you want the longer version of this argument, our post on what offline support, AI features and super apps each add to a build goes through how to decide which screens need it at all.
AI features and new modules get one door each
Reorder suggestions from six months of sales are the kind of AI feature now being asked for. The structural question is where the model runs and what the screen does when the answer is slow or wrong. If the order screen calls a model provider directly, then changing provider, adding a spending cap or moving the work onto the phone means opening every screen that uses it.
We prefer a narrow interface inside the feature module. The screen asks for "a reorder suggestion for this store" and gets back either a suggestion or a clear failure it can show. The module decides whether the request goes to a server or runs on the device, and it is the one place where you decide what data leaves the phone. A shopkeeper's sales history probably should not go to a third party with his name attached, and that rule belongs in one module, not twelve screens.
A second product inside the same app follows the same logic. Once the distributor adds invoice approval for finance, you have a small super app: one shell, a shared login, and modules owned by different people. Write down three things before the second module starts. Which module owns which data, which parts of the shell are shared, and how a module gets switched off remotely when it misbehaves. That remote switch matters more than it looks, because a store review can take days you do not have.
Where the structure usually slips
Sync gets designed last. The screens were built for a live connection, somebody asks for offline six months in, and every screen has to be reworked. Close behind it is screens calling endpoints directly, so one API change spreads across the whole app.
Then there are the handsets nobody updates. A field team's phones can run an app version from last Diwali, and if the backend drops support for it, those reps cannot book. When all modules share one release schedule, the slowest team sets the pace for everyone. And the one that catches teams coming from web work is backend capacity: it was sized for a website, and a mobile client adds retries and background syncs the website never generated.
What sits behind the app, from LIWeddings
LIWeddings is a responsive web product rather than a native app: a Long Island wedding vendor marketplace with forums, modernised as a Laravel monolith. Its documented stack is still a useful reference for what sits behind any client, a phone included. Search runs through Laravel Scout with MeiliSearch or OpenSearch. Redis queues managed by Horizon handle background work such as notifications, lead routing and image processing. Response and fragment caching and a CloudFront CDN keep pages fast, on top of MySQL, with role policies for access.

A mobile client leans on the same mechanisms. Search served from an index rather than the main database, slow work pushed to a queue, cached responses from the CDN rather than built per request. Four hundred reps' phones all syncing at 6 pm is exactly the load a queue absorbs and a direct database call does not.
Test it on a real phone before the second feature
Before anything new goes into the app, run this on the cheapest phone your reps carry, not the one on your desk.
- Put the phone in airplane mode and open every screen. Each one should load from the local store.
- Edit the same record on two phones while both are offline, reconnect them, and check the result against your written conflict rule.
- Make the AI call time out and confirm the screen shows a failure the rep can understand.
- Switch a module off from the server and check the rest of the app still works.
- Install the oldest app version still in use and confirm the backend answers it.
If there is time for only one, do the first. Once every screen runs from the local store, conflict rules, fallbacks and module switches all have an obvious place to live. When you are ready to scope the build itself, our mobile app development page sets out what we take on.
From our work
LIWeddings
Planning your dream wedding on Long Island starts here!
Read the project story
