Skip to main contentSkip to footer

    Top Rated & Verified

    Top Clutch App Development Company Black Owned United StatesTop Clutch Java Developers France 2026Top Clutch Service Line Blind Company Black Owned 2026Top Clutch App Development Company Minority Owned 2026Top Clutch Web Developers Black Owned 2026Top Clutch App Development Company Black Owned 2026Top Clutch Flutter Developers France 2026Top Clutch Health & Wellness App Developers France 2026Top Clutch Swift Company France 2026Top Clutch Machine Learning Company France 2026Top Clutch Chatbot Company France 2026Top Clutch Artificial Intelligence Company France 2026Top Clutch App Development Company Minority Owned Los Angeles
    Back to Blog
    Engineering Guide
    September 7, 2026
    29 min read

    Companion Apps on One Backend:Two Apps, One Company, and How They Work Together

    Uber has a rider app and a driver app. DoorDash has Dasher. Shopify has POS. We have Fighter Cut and The Corner. This is the guide to deciding when one company needs two apps, the platform mechanics that let them share identity and data without sharing a binary, the store rules that police the split, and the six things that broke in a real two-app build.

    The Corner landing page, the coach companion to Fighter Cut: a coach in the ring corner behind the headline 'Every fighter. Every gym. One glance.'
    2 apps, 1 auth table
    Fighter Cut and The Corner sign coaches and fighters into the same Supabase project through different screens
    Frenchy Digital case studies, 2026
    8 days
    From The Corner's first commit to a live web app and a TestFlight build, on a backend that already existed
    Frenchy Digital, The Corner case study, 2026
    API level 29
    Where Android deprecated sharedUserId, the old way for two apps to read each other's data, calling it strongly discouraged
    Android Developers, manifest element reference
    4.3(a)
    The App Store guideline that forbids multiple Bundle IDs of the same app, and the one a thin companion has to answer to
    Apple App Review Guidelines, 2026

    Key Takeaways

    • A companion app is a separate app, with its own bundle ID and store listing, that serves a different audience of the same company on the same backend. It is not a second copy of the same app, which is what Apple's Guideline 4.3(a) and Google Play's Spam policy forbid.
    • Split into two apps when the audiences differ in what they do, how long they stay, what they may see and how they are acquired. Keep one app with roles when the same person is often both audiences, or the second audience is small enough for a hidden tab.
    • Share the backend, the identity store and the design language. Do not share a binary, and think hard before sharing a repository: the two Fighter Cut apps are separate repos with one Supabase project and one auth table, and all SQL lives in the athlete repo.
    • Put the permission model in the database. Postgres row-level security denies by default once enabled; a coach role with no policy on a table cannot read it regardless of what the client does. Consent should be a property of the row, not a checkbox in the UI.
    • On iOS, App Groups, keychain access groups and team-scoped Sign in with Apple identifiers all limit sharing to apps from the same development team, which is the strongest argument for publishing both apps under one Apple team.
    • On Android, sharedUserId has been deprecated since API level 29 and Google warns it may be removed; use content providers, bound services, or simply two tokens for one backend account.
    • One domain can list several apps in its apple-app-site-association and assetlinks.json files, but Android associates only one app with a domain per device. Give each companion its own subdomain.
    • Expect the failures that actually happened: a shared auth project that silently redirected coaches into the athlete app, a recursive row-level policy, and duplicate entities that orphaned waiting users. Each has a documented fix in the case study linked below.

    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.

    The test:put the two home screens side by side. If each audience would be confused by the other's, you have two products that happen to share a company and a database, and a companion app is the right shape. If they would see the same screen with a different word at the top, you have one product and a policy problem waiting for review.

    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.

    CriterionPoints toward two appsPoints toward one app with roles
    Session shapeA driver has navigation on for hours; a customer orders in three minutesBoth audiences open it briefly and for the same reason
    Whose data is on screenOne audience reads other people's data (a roster, a queue, a fleet)Everyone mostly sees their own
    Permission modelRoles differ in kind, not degree: a coach can never see a food photoRoles differ in degree: an admin sees a bit more of the same
    AcquisitionThe second audience is recruited separately, with its own landing page and store listingThe second audience is invited from inside the first
    OverlapA person is rarely both; being both is a special caseA person is often both (buyers who also sell)
    Size of the second audienceLarge enough to justify its own release cadence and supportSmall enough that a hidden tab and a role flag serve it
    Offline and storage philosophyOne side needs local-first storage, the other must be server-ownedBoth sides can share one storage strategy
    Store riskEach app has a job the other cannot do, so 4.3 and Play's Spam policy do not applyThe 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.

    What does not decide it: the size of the company. A two-person team can run a companion pair if the backend is genuinely shared and the boundary is written down. What sinks small teams is not two apps; it is two apps with two backends, two user tables and a sync job between them.

    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.

    One repository owns the schema.The Corner has no SQL of its own. Every table and function it calls, clubs, memberships, fighter profiles, progress events, nutrition days, assignments, invites and the founding-code pieces, is a migration in the athlete app's repository. A schema change for a coach feature is written over there and consumed here. Several of those functions have exactly one caller, the coach app, and a grep of the athlete repository for them comes back empty, which is correct and is written down so nobody deletes them as dead code.

    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 seeRow-level conditionConsent semantics
    progress_events (weigh-in, session, assignment done)Fighter is attached to the coach's club and reader is staff thereSent 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 staffA second, separate switch; off deletes what it shared
    Meal photos and the fighter's own notesNo coach-side policy exists on these columns or tablesNever readable by a coach, whatever the client does
    Cut plan, macro targets, onboarding answersNo coach-side policy existsAthlete-app only, by schema
    Fighter identityRoster 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.

    Performance note from the Supabase docs: wrap function calls in a select inside policies so Postgres caches the result per statement rather than per row, and use SECURITY DEFINER helper functions to read membership tables without recursive policy evaluation. The second one is not optional in a two-app setup; the failures section explains what happened without it.

    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.

    What to do instead: nothing on the device, in most cases. Two apps holding their own tokens for one backend account is a complete solution for identity, and a content provider or bound service covers the rare case where two apps on one phone must exchange something before the network does. The coach and athlete apps do not exchange anything locally; a fighter types a team code into their own app and the roster updates on the server, which is where the coach app reads it.

    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.

    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.

    Two review-note habits that help: name the other app in the first sentence, so the reviewer knows this is a pair and not a clone, and provide credentials for both sides. A reviewer who can only see the coach app, with an empty roster and no way to populate it, sees a thin app. A reviewer who can log a weigh-in on the fighter side and watch it appear on the coach side sees a product.

    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. 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. 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. 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. 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. 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. 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.

    FailureWhat actually happenedThe rule now
    Coaches waking up in the wrong appSupabase substituted the project site URL, the athlete app, for an unlisted redirect target; confirmation links landed coaches in fighter onboarding with a valid sessionBuild every emailed link from one origin helper; add every new link type to the redirect allow-list first
    Infinite recursion in row-level securityAn 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 fightersA 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 ownedSetup searches unclaimed gyms first and shows the waiting count, so claiming is the path of least resistance
    A club nobody was a member ofCreating a club without inserting the creator as staff looked like success and then every roster read came back emptyThe membership insert is part of creation and its failure is surfaced, not swallowed
    Demo data impersonating a live rosterA helper that returned sample rows on a failed request would have shown a coach a roster that was not realSample data only when there are no backend keys at all; a failed fetch returns an empty list
    A silent bot checkAn invisible CAPTCHA declined on VPNs and shared gym networks with nothing on screen, so sign-in failed like a wrong passwordGate 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.

    ActionWhich app, which roleWhere it is enforced
    Read a roster of shared weigh-ins and sessionsCoach app, aloneRow policy: attached fighter, staff reader
    Show a fighter's daily calories and mealsCoach app, only after the fighter's second switchRow policy requires the nutrition opt-in as well
    Show a meal photo or a fighter's noteNeither app may show it to a coach, everNo coach-side policy exists; not a UI decision
    Plan a weight cut or compute macrosAthlete app, aloneThe coach repo has no such code; a request for it is aimed at the wrong app
    Mint a team invite codeCoach appOwner or coach role; one code per gym
    Remove a coach or spend the founding spotCoach app, owner onlyOwner and coach are distinct roles in the membership table
    Change the schemaAthlete repository onlyThe 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.

    The identity boundary is a good example of all three at once.Coaches see fighters by a generated public-name column, Chris M. rather than a legal name. That is enforced by the schema, which provides the column; by every roster and detail query selecting it; and by a deliberately negative test that asserts the full-name column's identifier does not appear anywhere in the coach app's source, because what breaks this is not a wrong function but someone adding a query that asks for the column they remember. The same test caught the demo roster teaching the opposite rule, abbreviating first names and printing surnames in full, and the sample data was corrected, since sample data is what a coach believes the app does.

    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.

    EngagementRangeTimelineWhat it covers
    Discovery + workflow audit (one app or two?)$9k–$22k2–4 weeksAudience mapping, permission model, storage philosophy per side, store-policy risk review, written recommendation
    Single-audience companion on an existing backend$28k–$70k4–9 weeksNew 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–$180k9–16 weeksBoth 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 weeksOne 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

    Chris Machetto - CEO & Founder of Frenchy Digital

    Chris Machetto

    CEO & Founder of Frenchy Digital, a senior-led Black-owned Los Angeles agency that builds custom mobile apps, and the shared backends behind them.