Two apps, one platform: why we did not unify
Building an operations app for staff and a loyalty app for customers on a React Native monorepo with a 10-person team. Why we tried unifying first, threw it away after three sprints, and what the split actually paid for.
A unified app that tries to serve both employees and customers ends up serving neither. The jobs-to-be-done diverge inside a month, and after that the surface is fighting itself.
I want to start with a confession. We tried the unified-app approach first.
The argument was made by Dima, our lead designer, in week three of the mobile track. He drew a tab bar with a role switch in the corner. Customer mode showed loyalty and orders; employee mode showed tasks and the knowledge base. It was a clean idea on a whiteboard, the team liked it, the CEO was supportive. We built three sprints of it in React Native on a single codebase with role-based navigation.
By the end of sprint three we had something that demoed well in 30 seconds and was unusable in 30 minutes. Customer flows were buried under operational chrome. Employee flows had retail polish that slowed down task completion. The role switch that looked clean on the whiteboard became the place every QA bug landed. We threw it away.
Three sprints, not nothing. The next morning we started over with two apps on a shared backbone, and a year later it was carrying the whole operation. Here is why the second design was right and what it actually cost.
Why two apps, not one
A customer opens an app to do something for themselves: place an order, check a prescription, redeem a bonus, look at a warranty. They open it once a week at most, often less. The first three seconds matter. They do not care about your menu structure.
An employee opens an app to do something for the operation: check a task list, look up a standard, mark a step done, file a customer note, glance at a dashboard. They open it twenty times a day. The 30th menu structure matters. The first three seconds do not.
Those two are not two roles in one product. They are two products. Our three failed sprints showed it concretely: the customer surface needed to be calm, the employee surface needed to be fast, and the union of those two visual languages produced a third one that felt slow and busy at the same time.
The other failure mode of the unified approach is roadmap. A unified app forces every release to negotiate priorities between two audiences with different cadences. Customer features compete with operations features for the same surface. The operation always loses, because the customer is more visible to leadership. Operations gets crumbs, employees route around the app within a quarter, and the project quietly dies.
What was shared, what was not
We shipped two apps with one 10-person team. Not two apps in parallel, one platform with two surfaces. The split was specific.
The codebase is a pnpm-workspaces monorepo with shared packages and two app targets:
packages/
identity/ auth, sessions, permissions
domain/ customer, order, location, assignment, catalog
content/ prescriptions, KB, lens specs, warranty terms
push/ notification plumbing
apps/
customer/ React Native, retail tone
employee/ React Native, ops tone
Shared, in code: identity, domain models, content, push plumbing.
Separate, in code and process: UI components, navigation, telemetry, release cadence. The customer side ships when it is ready, gated by retention KPIs. The employee side ships every two weeks regardless, because operations needs predictable change.
This is the boring part of the story and the part that determined whether splitting was a luxury or a leverage. With the backbone shared and the surfaces split, we got the focus benefits of two apps and the cost structure closer to one. We measured this directly: shared packages are roughly 60% of the codebase by lines and roughly 30% of the bug surface by triage time. The "two apps" cost on a 10-person team turned out to be about 1.5x of a single app, not 2x.
What drove each app
The customer app was driven by retention. Every feature had to carry its weight against "would someone open the app next week because of this." Loyalty tiers, bonus points, prescription history, lens reorder, warranty lookup. Each feature is a small reason to come back. The aesthetic and tone were set by what stops a returning customer from feeling like they are filling a form.
The employee app was driven by adoption. The hardest thing to win in operational software is the moment a frontline employee chooses your app over a sticky note or a WhatsApp group. We measured "what fraction of the daily operation flowed through the app" and treated it as the only KPI worth optimising for in the first quarter. It moved from 18% in the launch week to 73% by the end of the quarter. The numbers we cared about were not stars in the store, they were stickies that disappeared.
What I got right by accident
I want to admit one more thing. The decision to keep the same identity model across both apps looks principled in this write-up. In practice we did it because we had a 10-person team and could not afford two auth stacks. The constraint forced the choice that turned out to be load-bearing later. When marketing wanted to attribute a returning customer to the staffer who served them, the join already existed. When customer support wanted an employee to log in as a customer for a service ticket, the session model handled it.
The lesson I take from this: a small team is not always a constraint. Sometimes it is the thing that prevents you from making the wrong architectural choice for the wrong reason.
What happened the Monday after launch
The Monday after the employee app hit 73% adoption, Dima, who had argued for the unified approach in week three, came up at the standup and said: "I would have made the wrong call. Glad we threw the first build away." That was the end of the unified-app argument inside the company. The thing that ends an architectural disagreement is not the design doc, it is the demo where the right answer is obviously the right answer to the person who wanted the wrong one.