What a Companion App Is, and What It Is Not
A companion app is a separate app, with its own bundle identifier and its own store listing, that serves a different audience of the same company on the same backend. The rider and the driver. The customer and the courier. The merchant at the counter and the owner in the back office. The fighter logging a weigh-in and the coach who needs to see it. One business, two kinds of user, one data model underneath, and two front doors because the two people walking through them want different things.
That definition matters because the phrase gets used for two other things that are not companion apps at all. The first is a second copy of the same app: the same screens, a different logo, sometimes a different city or team name. Apple's App Review Guidelines address that directly in 4.3(a), telling developers not to create multiple Bundle IDs of the same app, and Google Play's Spam policy says it does not allow apps that merely provide the same experience as other apps already on the store. The second is a thin satellite: a settings screen, a read-only list, a notification inbox, something that could have been a tab and got a bundle ID instead. That one runs into Apple's 4.2 Minimum Functionality clause, which says an app should include features, content and UI that elevate it beyond a repackaged website.
We wrote this guide because we have just shipped a pair. Fighter Cut is a local-first weight-cut and nutrition app for combat athletes; The Corner is the coach and gym dashboard that sits beside it on the same Supabase project, the same authentication table and the same Apple developer team. Most of what follows is the platform documentation, read carefully, and the decisions and mistakes from that build. Where a claim comes from a vendor or from our own work, it says so.
One App With Roles, or Two Apps: The Decision Table
The default should be one app. Two apps cost two store listings, two review queues, two release trains, two sets of screenshots and two support surfaces, and every one of those costs is permanent. The question is whether the audiences differ enough that forcing them into one binary costs more, in branching screens, in permission complexity and in a user who has to be told which mode they are in. The rows below are the criteria we actually use in discovery, in roughly the order they decide the answer.
| Criterion | Points toward two apps | Points toward one app with roles |
|---|---|---|
| Session shape | A driver has navigation on for hours; a customer orders in three minutes | Both audiences open it briefly and for the same reason |
| Whose data is on screen | One audience reads other people's data (a roster, a queue, a fleet) | Everyone mostly sees their own |
| Permission model | Roles differ in kind, not degree: a coach can never see a food photo | Roles differ in degree: an admin sees a bit more of the same |
| Acquisition | The second audience is recruited separately, with its own landing page and store listing | The second audience is invited from inside the first |
| Overlap | A person is rarely both; being both is a special case | A person is often both (buyers who also sell) |
| Size of the second audience | Large enough to justify its own release cadence and support | Small enough that a hidden tab and a role flag serve it |
| Offline and storage philosophy | One side needs local-first storage, the other must be server-owned | Both sides can share one storage strategy |
| Store risk | Each app has a job the other cannot do, so 4.3 and Play's Spam policy do not apply | The second app would be a thin slice of the first, which is exactly what those policies describe |
Two of these rows deserve emphasis. Session shape is the one Uber's engineers have written about most: their 2018 account of rewriting the driver app describes a product that serves over three million driver-partners who use it to find fares, get directions and track their earnings, an app people rely on to make a living, and one whose outages are therefore a different kind of emergency from a rider's. An app that runs for a whole shift with navigation on is not the same engineering problem as an app opened for three minutes, and pretending it is means one audience always gets the compromise.
The other is whose data is on screen. A fighter looks at their own weigh-ins. A coach looks at twelve other people's. That is not a bigger version of the same permission; it is a different kind of permission, and it changes what a stale cache can do, what an error state must say, and what a screenshot in the store listing can show. When one side of the product is fundamentally about other people's data, that side wants its own app, its own storage philosophy and its own consent model, and the rest of this article is largely about how to give it those without duplicating the backend.
Who Already Does This: Uber, DoorDash, Shopify
The pattern is old enough that the largest consumer platforms in the world treat it as the obvious shape. Three examples, each documented by the company itself rather than by a commentator.
Uber: rider and driver, rewritten separately, shipped as two binaries in one package
Uber's engineering blog published two posts about the driver app rewrite it called Carbon. The first, in October 2018, explains why the driver app had to be rebuilt at all: technical debt from rapid growth had produced regressions serious enough that development was paused in late 2016, the product could not stretch to new business lines, and the team wanted the same architecture it had already built for the rider app. The second, in January 2019, describes how they shipped it. In their words: "With the Android version of our new driver app, we couldn't take the chance that its launch would negatively impact our users, so we took the unlikely path of shipping two apps, or binaries, in one package." That let them roll the new app out to a percentage of drivers in specific cities while others stayed on the old one. The lesson for a smaller team is not the dual-binary trick, which is unusual. It is that the driver app was always a separate product with a separate release strategy, because the people using it depend on it in a way riders do not.
DoorDash: a consumer app and a Dasher app, two listings under one seller
The App Store lists DoorDash - Dasheras its own app from the seller DoorDash, Inc., with a description that opens "Deliver and earn with DoorDash", separately from the consumer app titled DoorDash: Food, Grocery, More. Same company, same orders, same restaurants, and two apps, because the person waiting for a burrito and the person carrying it need entirely different screens. Google Play carries the same two listings under the same developer account. Nothing about this trips either store's duplicate-app rules, and it has been that way for years, which is the plainest evidence available that a genuine companion is not what those rules are for.
Shopify: the POS app for the counter, the admin for the office, and a staff type that only gets one
Shopify's help centre documents the split at the level of permissions. The Shopify POS app runs on a mobile device for the person selling in person; the Shopify admin, available on mobile and desktop, handles locations, inventory, products, orders and staff. And there is a staff category the documentation describes as "POS app only staff that have access to the Shopify POS app, but don't have access to your Shopify admin or Point of Sale channel", available to merchants on POS Pro. That is the companion pattern in its purest form: the permission is a property of the account, not of which app is open, and the app the counter staff install simply has no door into the office.
Notice what all three share. None of them tried to make one app do both jobs with a mode switch. All three keep one backend and one notion of an order, a ride or a sale. And in each case the audience with less to see has an app that cannot show it more, which is the shape we will come back to when we talk about consent.
The Shared Backend: One Identity Store, One Data Model
The only thing two companion apps must truly share is the backend, and the only part of the backend that must be shared without exception is identity. One users table. One identity provider configuration. One notion of a session. The moment a company has two user tables for two apps, it has a synchronisation problem that will outlive every engineer who created it, and every question about "is this coach also a fighter" becomes a join across systems instead of a row.
In the Fighter Cut pair, both apps authenticate against one Supabase project and therefore one auth.users table. A coach and a fighter are rows in the same table; what distinguishes them is not which app they signed up in but which other rows point at them: a membership row in a clubs-and-staff table makes someone a coach of a gym, and a fighter profile attached to that club makes someone a fighter on its roster. A person can hold both. The apps are different windows onto the same account, and the account does not know or care which window is open.
Sharing the schema does not mean sharing the storage philosophy. The athlete app is local-first: it keeps its data in IndexedDB on the device, works with no account and no network, and syncs when it can. The coach app is the opposite by design. A roster is inherently server-owned, a live read of other people's data, so it has no local database at all and its session store holds UI state only. A cached roster that outlived a fighter's decision to stop sharing would be a privacy failure dressed up as performance, so the rule in that codebase is never to add a persistence layer to make a screen feel faster. Two apps let each side have the right philosophy. One app would have forced a compromise on one of them.
What about the front-end code? Both apps are React, Vite, TypeScript and Tailwind, wrapped for iOS with Capacitor 8, which Ionic announced on 8 December 2025 with Swift Package Manager as the default dependency manager for new iOS projects. They share a design language, a paper-and-ink palette with one accent colour, and a handful of primitives that were ported from one repository to the other. They do not share a package. For a team of this size the explicit list of twin files, with the instruction to check the other when fixing one, has been easier to keep honest than a shared package would be. A larger team with the discipline to maintain package boundaries should reach for a monorepo instead; the cross-platform strategy guide covers that trade-off in depth.
Consent as a Data Shape: Row-Level Security Across Apps
When two apps read one database, the database is the only place a permission can be enforced for both at once. A check in the coach app's code protects nothing against a stale build, a curious developer with the anon key, or the next app you add. This is the strongest technical argument for a shared backend over two backends with a sync job: the rule lives once, at the row.
PostgreSQL's documentation describes row security policies as restricting, on a per-user basis, which rows can be returned by normal queries or inserted, updated or deleted by data modification commands, and states that if no policy exists for a table with row security enabled, a default-deny policy is used, meaning no rows are visible or can be modified. Supabase builds its authorisation model on exactly this, describing it as granular authorisation rules that run inside the database, with a helper that returns the ID of the user making the request and returns null when there is no authenticated user. The Supabase row-level security checklist covers the mechanics; here the point is what those mechanics make possible across two apps.
| What a coach might see | Row-level condition | Consent semantics |
|---|---|---|
| progress_events (weigh-in, session, assignment done) | Fighter is attached to the coach's club and reader is staff there | Sent whenever progress sharing is on |
| nutrition_days (calories, macros, meals, recipes, training minutes) | Fighter opted into nutrition sharing AND is on this roster AND reader is staff | A second, separate switch; off deletes what it shared |
| Meal photos and the fighter's own notes | No coach-side policy exists on these columns or tables | Never readable by a coach, whatever the client does |
| Cut plan, macro targets, onboarding answers | No coach-side policy exists | Athlete-app only, by schema |
| Fighter identity | Roster queries select a generated public_name column ('Chris M.') | A test fails if the full-name column is requested anywhere in the coach app |
Two design choices in that table are worth defending. First, the coach app deliberately checks no consent flag of its own before fetching nutrition. The policy on the nutrition table already requires all three conditions; a second check in the client would be another place to get it wrong without making anything safer, because a stale client can skip a client-side check and cannot skip the database. The roster row does carry a boolean saying whether the fighter shares nutrition, but it decides only which sentence to render: "has not turned on nutrition sharing" versus "opted in and logged nothing" versus the data itself. Telling a coach "no food logged" about a fighter who was never asked to share would be wrong twice, about the fighter and about the app.
Second, the things a coach must never see are not hidden by the interface. There is no policy that grants a coach role access to a meal photo or a fighter's own note, so no query from the coach app can return one, and no future feature can surface one without a schema change that would be reviewed as such. The privacy promise on the screen, that meal photos and the fighter's own notes stay on their phone and this app never receives them, is a description of the schema, not a promise the UI is keeping on its own.
iOS Mechanics: App Groups, Keychain Sharing, Team-Scoped Identity
Apple provides three same-device mechanisms for apps from one company to cooperate, and all three carry the same restriction: they work only for apps from a single development team. That restriction is the single best reason to publish a companion under the same Apple team as the main app, with a second bundle identifier beneath the same root, rather than under a subsidiary's account.
- App Groups: Apple's documentation says an app group allows multiple apps developed by the same team to access one or more shared containers, and enables interprocess communication between those apps using Mach IPC, POSIX semaphores and shared memory among other mechanisms. The container ID for iOS must begin with the literal prefix group. followed by your own string. In practice: a shared UserDefaults suite for flags, a shared file container for a cache, and nothing that leaves the device.
- Keychain access groups: The keychain documentation puts it plainly: if you develop a family of apps that rely on the same password or cryptographic key, you can use access groups to share it among those apps, so that logging into one grants the user access to all of them, and this sharing requires no interaction or permission from the user but is limited to apps delivered by a single development team. Since iOS 8 an app group can double as a keychain access group.
- Team-scoped Sign in with Apple identifiers: The user identifier Sign in with Apple returns is scoped to the developer team, so the same person signing into two apps from one team is one identifier, and one account on your backend. Apple's Technote 3159 exists because that identifier does not survive an app transfer to another team: the transferring team must generate transfer identifiers for each user, and the recipient exchanges them within a 60-day window for identifiers scoped to its own team. Keep both apps on one team and this problem never arises.
The right way to think about these is as conveniences layered on top of a shared backend, not substitutes for one. A shared keychain item lets the coach app skip a login screen if the same person already signed into the athlete app on that phone. It does not tell the coach app what that person may see; the database does. And App Groups synchronise nothing across devices or between users, so anything both audiences need to agree on, a weigh-in, a roster, an assignment, still has exactly one authoritative copy, on the server.
Fighter Cut and The Corner use the simplest version of all this: two bundle identifiers under one Apple team and one root, com.fightercut, with each app holding its own session token for the same backend account. The apps are wrapped with Capacitor 8 on Swift Package Manager, which meant the second app inherited the first one's pinned package versions and archive settings rather than solving them again. The store name of the coach app, Fight Corner, differs from its in-app name, The Corner, which is a listing decision rather than a product one, and is written down in both repositories so nobody "fixes" it.
Android Mechanics: The Deprecated Shared UID and What Replaced It
Android's historical answer to two apps from one company was the shared user ID: set the same value in both manifests, sign both with the same certificate, and the apps shared a Linux user, could read each other's data and could even run in one process. The manifest reference is now blunt about it. The attribute is deprecated as of API level 29; shared user IDs cause non-deterministic behaviour within the package manager; their use is strongly discouraged and might be removed in a future version of Android; and developers should instead use proper communication mechanisms such as services and content providers.
The same reference carries a warning that matters for anyone with an existing pair: existing apps cannot remove the value, because migrating off a shared user ID is not supported, and should instead add the maximum-SDK attribute so new installs stop using it. If you are inheriting two Android apps that already share a UID, that is the path, and it is a one-way door.
The other mechanism companies reach for on Android, and on iOS, is the deep link between apps: a button in the athlete app that opens the coach app, or a link in a coach's email that should land in the right one. That is worth its own section, because both platforms have rules about one domain serving two apps, and they are not the same rules.
Links Between the Apps: Universal Links and App Links on One Domain
On iOS, associated domains establish, in Apple's words, a secure association between domains and your app so you can share credentials or provide features in your app from your website. The proof is a file hosted at https://<fully qualified domain>/.well-known/apple-app-site-association, served over HTTPS with a valid certificate and with no redirects. The good news for a companion pair is that one file can list several apps: the applinks details entry takes an appIDs array, and the documentation explicitly describes the appIDs and apps keys as specifying the application identifiers for the apps available for use on this website. One domain, two apps, path-by-path routing.
Android is stricter. The Digital Asset Links file lives at https://hostname/.well-known/assetlinks.json and can name more than one app, but Google's verification documentation states that on a given device only one app at a time can be associated with a particular domain, that if another app is already verified for the domain the user must disassociate it first, and that installing two app variants simultaneously requires special handling. Two companion apps both claiming fightercut.com would fight over it on every Android phone that had both installed.
| Mechanism | What it lets a pair share | Constraint |
|---|---|---|
| Backend account | Same users table, same identity provider, same session semantics | The only place identity should truly be shared |
| Sign in with Apple identifier | Team-scoped: the same for all apps in one developer team | Changes on app transfer to another team; see Technote 3159 |
| iOS App Groups | Shared container and IPC between apps from the same team; container ID must begin with group. | Same device only; nothing syncs anywhere |
| iOS keychain access groups | Share a password or key among a family of apps with no user interaction | Limited to apps delivered by a single development team |
| Android sharedUserId | Apps with the same UID and identical signing certificates could read each other's data | Deprecated at API level 29; strongly discouraged; existing apps cannot remove it |
| Android content providers and bound services | Structured, permission-gated data exchange between apps | The path Google recommends instead of a shared UID |
| Associated domains (iOS) | One apple-app-site-association file can list multiple appIDs | Hosted at /.well-known/ over HTTPS, valid certificate, no redirects |
| Digital Asset Links (Android) | One assetlinks.json can name multiple apps | Only one app can be associated with a domain per device |
The practical answer, and the one we used, is one subdomain per app: app.fightercut.com for the athlete app and corner.fightercut.com for the coach app, each with its own association file and each claimed by exactly one app on both platforms. It costs one DNS record and one certificate, and it makes the email problem in the failures section tractable, because every link a coach receives can be built from a coach origin and every link a fighter receives from a fighter origin, with no ambiguity about which app should open.
What the Stores Police: Guideline 4.3, 4.2 and Play's Spam Policy
Both stores have rules that a badly conceived companion app will break, and both are quoted below verbatim from the current published policy, because paraphrases of these clauses circulate widely and most of them overstate what the rules say.
Don't create multiple Bundle IDs of the same app (for example, submitting a separate map app for every city in the world instead of a single worldwide map that allows users to search any city). This practice results in unnecessary apps, which makes it hard for users to find the apps they want. If your app has different versions for specific locations, sports teams, universities, etc., consider submitting a single app and providing the variations using in-app purchase.
— Apple App Review Guidelines, 4.3(a)
We don't allow apps that merely provide the same experience as other apps already on Google Play. Apps should provide value to users through the creation of unique content or services.
— Google Play Developer Policy Center, Spam
Read both carefully and the target is clear: the same app, many times. A city-per-app map. A theme-per-app wallpaper collection. Google's own list of violations names creating multiple apps with highly similar functionality, content and user experience, and adds that if those apps are each small in content volume, the developer should consider a single app that aggregates the content. A driver app and a rider app do not have similar functionality, and neither do a coach dashboard and an athlete tracker. Uber, DoorDash and Shopify all publish pairs under one developer account on both stores, and have done for years.
The clause that does catch companion apps is a different one. Apple's 4.2 Minimum Functionality says your app should include features, content and UI that elevate it beyond a repackaged website, and that if your app is not particularly useful, unique or app-like it does not belong on the App Store. A companion that is one read-only screen is exactly what that describes. The defence is the same as the design principle: the second app must have a job the first cannot do. The Corner mints invite codes, claims gyms fighters have already named, manages staff roles, sets assigned work and reads a roster; none of that exists in the athlete app, and the review notes say so, with test credentials for a coach account and a fighter account so a reviewer can watch a weigh-in cross from one to the other.
A Real Pair: Fighter Cut and The Corner
Fighter Cut is a first-party Frenchy Digital product, so the numbers here are counts from the code, the schema and the commit history rather than marketing figures, and the product is in beta with public release gated to October 2026. The athlete app plans a weight cut with a deterministic engine that caps the daily deficit at 750 kilocalories and refuses unsafe plans, compares 168 branded supplements across 65 brands with a rule engine rather than an AI coach, and stores everything locally so that no account is required. It shipped first.
The coach app came from a specific problem: a fight coach with a dozen athletes in camp spends Sunday evening texting. How is the weight? Did you run? Did you do the work? The fighters were already logging all of it in their own app. The data existed and had no window for the person in the corner. The obvious answer, a coach login to the athlete app, was rejected, because what a fighter eats is private in a way a weigh-in is not, and the athlete app's entire pitch is that this is your data and nobody else's. So The Corner is a separate product in a separate repository that can see only what a fighter chooses to send it, one switch at a time.
- 1.17 August 2026: Clubs and fighter profiles land in the athlete app's schema. The coach app does not exist yet, but its data model does.
- 2.23 August 2026: The org dashboard migration: memberships, progress events, assignments and roster invites. The coach repository starts the next day, with no SQL of its own.
- 3.25 August 2026: Emailed links fixed after both of them silently landed coaches in the athlete app. The App Store listing is written down as a file rather than only into Apple's forms.
- 4.30 August to 1 September 2026: Founding codes, club claiming, staff management and sub-admins. corner.fightercut.com is live and build 3 is on TestFlight with a bot check in front of every auth call. Eight days from first commit.
- 5.2 to 4 September 2026: Nutrition sharing: daily totals first, then the meals and recipes behind an expanding day row, gated by a second consent switch on the fighter's side. Fighters are shown to coaches by a generated public name.
- 6.7 September 2026: The landing page redesign: three photographs, a shorter hero with the copy centred, and the descenders the slide-up animation had been clipping off the headline.
Two things made eight days possible, and neither was heroics. The backend, identity store, design system, hosting pattern and Capacitor configuration already existed, so the coach app was seven migrations and a new front end rather than a new system. And the boundary was written before the first screen: the coach repository's working notes open by saying the app is for gym owners and coaches to track their fighters and nothing else, and list the athlete features that must never appear in it. A feature request that sounds like cut planning or macro maths is aimed at the wrong app, and saying so in advance saved the arguments.
The consent model evolved, and the evolution is instructive. The first version shared no food at all: three event types, weigh-in, session and assignment done, and nothing else. On 2 September a second switch was added on the fighter's side that shares a fortnight of nutrition, daily calories and macros, then meals and the recipes behind them. The line moved, and the screen's promise moved with it: it no longer says "daily totals only", which would now be a false statement about a fighter's privacy made to their coach, and says instead that meal photos and the fighter's own notes never arrive and that everything shown disappears when the fighter switches it off. Both sentences are pinned by tests, because a privacy statement that drifts from the schema is worse than no statement.
What Breaks First: Six Failures From a Live Two-App Build
Every one of these happened. They are listed with the signal that revealed them and the fix that is now a rule, because a shared backend makes some failures more likely, not less, and a team going in should expect them rather than discover them.
| Failure | What actually happened | The rule now |
|---|---|---|
| Coaches waking up in the wrong app | Supabase substituted the project site URL, the athlete app, for an unlisted redirect target; confirmation links landed coaches in fighter onboarding with a valid session | Build every emailed link from one origin helper; add every new link type to the redirect allow-list first |
| Infinite recursion in row-level security | An inline membership check against the membership table recursed inside the policy (Postgres 42P17) | SECURITY DEFINER helper functions for membership; a source-grep test blocks the inline form from returning |
| Duplicate gyms that orphaned fighters | A coach created a new gym under a new slug when a fighter had already named it; the waiting fighters stayed attached to an unclaimed row nobody owned | Setup searches unclaimed gyms first and shows the waiting count, so claiming is the path of least resistance |
| A club nobody was a member of | Creating a club without inserting the creator as staff looked like success and then every roster read came back empty | The membership insert is part of creation and its failure is surfaced, not swallowed |
| Demo data impersonating a live roster | A helper that returned sample rows on a failed request would have shown a coach a roster that was not real | Sample data only when there are no backend keys at all; a failed fetch returns an empty list |
| A silent bot check | An invisible CAPTCHA declined on VPNs and shared gym networks with nothing on screen, so sign-in failed like a wrong password | Gate every token-spending action on the widget being ready, say why in a sentence, offer a retry |
The first row is the one specific to sharing an auth project between two apps, and it is worth dwelling on. Supabase, like many hosted identity providers, does not reject a redirect target that is not on its allow-list; it silently substitutes the project's configured site URL. With one project behind two apps, that site URL can only be one of them, and it was the athlete app. Both of the coach app's emailed links, confirmation and password reset, did exactly that: a coach clicked confirm and landed in a fighter's onboarding questionnaire holding a valid session, with no error anywhere. The fix was mechanical, one origin helper that every emailed link is built from and a rule that a new link type means checking the allow-list first, but the class of bug is structural to the pattern. Anywhere the shared backend has a single default, one app owns that default and the other must never rely on it.
The second row is the one the Supabase documentation warns about in the abstract and that becomes concrete with two apps. Membership checks written inline as a subquery against the membership table, inside a policy on that same table, recursed, and Postgres reported error 42P17. The fix, SECURITY DEFINER helper functions that answer "is this user a member" and "is this user an owner" without re-entering the policy, is now a rule in both repositories, and a test greps the source for the inline form so a well-meaning refactor cannot bring it back. That test strips comments first, because these files explain a fix by naming the construct it replaced, and a naive match would pass on a reverted file.
Keeping the Boundary: What Each App May Do Alone
The long-term risk of a companion pair is not that one app fails. It is that the two drift toward each other until the coach app grows a food log and the athlete app grows a roster, and the reason for having two disappears while the cost of having two remains. The boundary has to be enforced in three places at once: in a written statement of what each app is for, in the schema, and in tests that fail when the line is crossed. The table below is the working boundary for the Fighter Cut pair, and the shape of it transfers to any coach-and-client, driver-and-rider or staff-and-owner product.
| Action | Which app, which role | Where it is enforced |
|---|---|---|
| Read a roster of shared weigh-ins and sessions | Coach app, alone | Row policy: attached fighter, staff reader |
| Show a fighter's daily calories and meals | Coach app, only after the fighter's second switch | Row policy requires the nutrition opt-in as well |
| Show a meal photo or a fighter's note | Neither app may show it to a coach, ever | No coach-side policy exists; not a UI decision |
| Plan a weight cut or compute macros | Athlete app, alone | The coach repo has no such code; a request for it is aimed at the wrong app |
| Mint a team invite code | Coach app | Owner or coach role; one code per gym |
| Remove a coach or spend the founding spot | Coach app, owner only | Owner and coach are distinct roles in the membership table |
| Change the schema | Athlete repository only | The coach repo has no SQL and should never get any |
The third column is the one that matters. Where the answer is "row policy", the boundary survives any client bug. Where it is "the coach repo has no such code", the boundary survives until someone adds the code, which is why the written notes exist. Where it is a test, the boundary survives a refactor that did not read the notes. A team should be able to point at all three for every row of its own table, and a row that has only the first, a UI decision, is a row that will drift.
Cost, Timeline, Red Flags and Limitations
A companion app on an existing backend costs less than a second product and more than a tab, and the difference is almost entirely in how much of the backend, identity and design system already exists. These are Frenchy Digital's engagement bands; senior-led work runs $150 to $225 an hour, retainers $2,500 to $9,500 a month, every engagement carries a 30-day post-launch warranty, and a written fixed-price phased proposal follows a discovery call within five business days.
| Engagement | Range | Timeline | What it covers |
|---|---|---|---|
| Discovery + workflow audit (one app or two?) | $9k–$22k | 2–4 weeks | Audience mapping, permission model, storage philosophy per side, store-policy risk review, written recommendation |
| Single-audience companion on an existing backend | $28k–$70k | 4–9 weeks | New app on the current identity store and schema, migrations added to the primary repo, shared design system, web plus one native wrapper |
| Two apps built together with system integration | $70k–$180k | 9–16 weeks | Both apps, one backend, row-level policies, shared auth with allow-listed redirects, universal links and App Links, both store submissions |
| Multi-site or regulated pair | $180k–$420k+ | 14–24 weeks | One side handles patient, payment or minor data; audit logging, consent records, SOC 2 or HIPAA posture, staged rollout by site |
Red flags when a vendor proposes a companion app.Two backends, or one backend and a "sync service" between two user tables: that is two products with a bridge, and the bridge will be where every bug lives. A permission model described in terms of screens, "coaches won't see that tab", rather than rows. A proposal to ship the second app as a mode inside the first "for now", which becomes forever. A second Apple developer account for the second app, which forfeits every same-team sharing mechanism this article describes. A plan to use Android's shared user ID. And a store strategy that does not mention Guideline 4.2 or review notes with credentials for both sides, which means nobody has thought about how a reviewer will see an empty companion.
Limitations of this guide. The case study is our own product, in beta, with no published user or revenue figures; every number attached to it is a count from code, schema or commit history, and we have said so rather than invent adoption metrics. The Uber, DoorDash and Shopify examples are drawn from those companies' own published material and store listings, not from interviews, and they establish that the pattern is standard, not what it cost them. We did not find, and do not cite, any independent study quantifying the cost of one app with roles against two apps, and we would distrust one that claimed a single answer. Platform documentation was checked in September 2026; Apple and Google revise these pages, and the store guidelines in particular, without notice. A discovery engagement exists precisely because the decision in the second section depends on facts about your audiences that no article can know.
Deciding Between One App and Two? Get the Answer in One Call
Book a free 60-minute discovery call with Frenchy Digital, a senior-led Black-owned Los Angeles agency. We map your audiences, your permission model and your store risk, and send a written, fixed-price phased proposal within 5 business days.
1517 S Bentley Ave Unit 204, Los Angeles CA 90025
Frequently Asked Questions
Sources & References
- 1Apple Developer Documentation — Configuring App Groups↗
- 2Apple Developer Documentation — Sharing Access to Keychain Items Among a Collection of Apps↗
- 3Apple Developer Documentation — Supporting Associated Domains↗
- 4Apple Developer Documentation — TN3159: Migrating Sign in with Apple Users for an App Transfer↗
- 5Apple Developer — App Review Guidelines (4.2 Minimum Functionality, 4.3 Spam)↗
- 6Apple Developer Account Help — Register an App ID↗
- 7Android Developers — <manifest> element reference (android:sharedUserId, deprecated in API level 29)↗
- 8Android Developers — Verify Android App Links (assetlinks.json, one app per domain per device)↗
- 9Android Developers — Handling Android App Links↗
- 10Google Play Console Help — Spam policy (Repetitive Content)↗
- 11Uber Engineering — Why We Decided to Rewrite Uber's Driver App (October 9, 2018)↗
- 12Uber Engineering — How to Ship an App Rewrite Without Risking Your Entire Business (January 3, 2019)↗
- 13Apple App Store — DoorDash - Dasher (separate listing from DoorDash: Food, Grocery, More)↗
- 14Shopify Help Center — Point of Sale staff management overview (POS app only staff)↗
- 15Supabase Docs — Row Level Security↗
- 16PostgreSQL Documentation — Row Security Policies↗
- 17Ionic Blog — Announcing Capacitor 8 (December 8, 2025)↗
Related Articles You May Find Helpful
- Supabase Row-Level Security Checklist for Lovable, Bolt, and Bubble Apps (2026)
- Cross-Platform App Development 2026: The Complete Strategic Guide
- AI Agents for Personal Training & Fitness Coaching 2026
- Enterprise App Solutions: The Complete 2026 Guide to Business Mobile Applications
- MVP Development Guide: Launch Your Startup App in 90 Days

