Back to Projects

    Building 25 iOS Apps Solo From One Expo Scaffold

    This iOS app portfolio is 25 standalone apps I built solo from one Expo and React Native scaffold in TypeScript. Every app repeats the same six-screen Expo Router layout, one RevenueCat entitlement named premium, bundled offline content and a committed privacy manifest, then builds and submits through EAS from three shell scripts.

    2026
    Mobile Developer
    11 Technologies
    Expo SDK 55React Native 0.83TypeScriptExpo RouterRevenueCat (react-native-purchases)EAS BuildEAS SubmitApp Store Connect API key authAsyncStorageexpo-notificationsReact Native Reanimated
    Building 25 iOS Apps Solo From One Expo Scaffold

    Introduction

    Twenty-five apps is not twenty-five times the work of one app, but only if you decide up front what will never differ between them. This page is about that decision set: the scaffold, the subscription contract, the privacy posture and the submission pipeline. The individual apps are ordinary consumer utilities, and they are not the interesting part.

    The Challenge

    Every iOS app carries a fixed cost that has nothing to do with the app. You need an onboarding flow, a paywall, a settings screen with privacy and terms links, a restore-purchases path, an App Store Connect record, a bundle identifier, a privacy manifest, a build pipeline and a submission step. Do that once and it is a week of chores. Do it 25 times by hand and the chores are the project. Working solo, I had no way to absorb that cost with headcount, so the only option was to make it a fixed cost paid once rather than a variable cost paid per app.

    The Solution

    I settled on one scaffold and refused to deviate from it. Each of the 25 apps is a standalone Expo project with the same six routes under app/: a root layout that wraps every screen in an app provider, and in the 18 apps that ship alternate themes a theme provider above it, an index route that redirects to onboarding or home based on persisted state, and then home, onboarding, paywall and settings. Each app has a src/context/AppContext.tsx that hydrates from AsyncStorage and subscribes to RevenueCat customer info, and a src/utils/purchases.ts that is the same file in all 25 projects apart from the API key. What varies per app is content, extra routes and colours. In total the portfolio holds 187 route files under app/ and 56 components under src/components, so the apps do diverge, but they diverge on top of a fixed base.

    Technical Deep Dive

    1

    Framework check first, because the repo description is wrong. The backup repo is described as 25 SwiftUI apps, and it is not. Every project has app.json, eas.json, expo-router as the entry point and a package.json pinned to Expo SDK 55, React Native 0.83.4 and React 19.2.0. There are exactly 25 Swift files in the whole tree, one AppDelegate.swift per app, all generated by Expo prebuild. The portfolio is React Native and TypeScript, with 357 TypeScript and TSX files. I mention this because verifying the stack against the source tree instead of the label is the same habit that keeps the rest of this page honest.

    2

    The entitlement contract is a single string. Every app exports ENTITLEMENT_ID set to premium and checks customerInfo.entitlements.active for that key in four places: after purchase, after restore, on a manual status check, and inside a customer info update listener. Nothing in the UI decides what premium means. AppContext holds an isPremium boolean, mirrors it into AsyncStorage, and every gate in every app reads that one flag. Adding an app means wiring one new RevenueCat project key and nothing else.

    3

    The paywall reads offerings rather than hardcoding prices. It calls getOfferings, maps RevenueCat package identifiers such as rc_weekly, rc_annual and rc_lifetime onto display labels, preselects the package flagged as popular, and falls back to an explicit error state when the offerings list comes back empty. That last case matters more than it sounds: offerings are legitimately unavailable before an app is approved, so the code treats an empty list as a normal state to render rather than a crash to debug.

    4

    There is no backend of my own. I grepped the application source of all 25 apps for fetch, axios, Supabase and outbound API hosts and found nothing. Content lives in bundled TypeScript modules under src/data/, user state lives in AsyncStorage under a per-app storage key, and nine apps bundle audio under assets/sounds. The only code that talks to a server is the RevenueCat SDK handling purchases. That choice removes the entire server cost line, removes account systems and password resets, and means each app's privacy manifest can declare no collected data types.

    5

    Privacy and legal surfaces are declared in the repo, not filled in per app. Each project has a committed PrivacyInfo.xcprivacy, byte-identical across all 25, with NSPrivacyTracking set to false, an empty NSPrivacyCollectedDataTypes array, and required-reason API declarations for file timestamps, user defaults, disk space and system boot time. The same privacy policy URL appears in 50 files, the paywall and the settings screen of all 25 apps, terms resolve to my own terms page on the paywall and to Apple's standard EULA in settings, and one support mailbox backs every contact action. When a policy changes I change one hosted page, not 25 store listings.

    6

    Release config is templated with only the identity fields varying. All 25 app.json files set version 1.0.0, portrait orientation, supportsTablet true, a dark user interface style and ITSAppUsesNonExemptEncryption false, and differ in bundle identifier, name, scheme, colours and EAS project id. All 25 eas.json files declare the same production profile, a Release build configuration with autoIncrement enabled, and a submit block pointing at one App Store Connect API key and issuer, with 25 distinct ascAppId values because each app has its own store record. Eight of them additionally pin credentials to the remote source. The .p8 key itself is excluded by gitignore and referenced by path.

    7

    The pipeline is three shell scripts, not a CI platform. automate.sh verifies the EAS login, then runs eas init and a non-interactive eas build with --no-wait for each app so all builds queue instead of blocking on each other. build_all.sh runs the same build command over the remaining 24 apps once the first two-factor prompt is cleared. submit_all.sh runs eas submit --platform ios --latest --non-interactive per app and continues past failures rather than aborting the batch, which is the right behaviour when some builds are still processing.

    Key Features

    One six-screen scaffold in all 25 apps

    Root layout, redirect index, home, onboarding, paywall and settings exist in every project, wrapped in an app context that hydrates from AsyncStorage before the first render. New apps start from a working shell rather than an empty Expo template.

    Single-entitlement subscription handling

    react-native-purchases configured once per app, one entitlement key named premium, and a customer info listener that pushes status changes into app state. Purchase, restore, cold-start check and live update all resolve through the same predicate.

    Premium-gated theming

    Eighteen of the apps carry a ThemeContext that defines named colour themes with an isPremium flag on each theme, persisted by name in AsyncStorage. That gives the paywall a real non-content benefit to sell without building any new feature surface.

    Offline-first data model

    Verse lists, sound catalogues, prompts and other content ship as TypeScript modules inside the bundle. Twelve apps share a reminders module that requests notification permission, clears existing notifications and reschedules on a daily or fixed-interval trigger through expo-notifications.

    Portfolio-wide build and submit scripts

    Three bash scripts iterate the app list and drive EAS. Builds queue asynchronously so one two-factor prompt covers the batch, and submissions tolerate individual failures so one incomplete build does not stop the other 24.

    Committed native projects and per-app history

    Each app keeps its prebuilt ios/ directory under version control, including the Xcode project, Podfile and Podfile.lock, with Hermes enabled. The backup repo also stores a git bundle per app so each project's own commit history survives being consolidated into one repository.

    Results & Impact

    • All 25 apps exist as complete Expo projects with committed native iOS projects, and all 25 have their own App Store Connect app record wired into eas.json for submission.
    • The scaffold is verifiably uniform: 25 of 25 apps share the same six core routes, the same app context pattern, the same purchases module and the same entitlement key, so a change to the subscription flow is one edit applied 25 times rather than 25 different edits.
    • The privacy position is provable from the source. No network calls in application code, empty collected data types in every privacy manifest, and tracking declared false in all 25.
    • Build and submission are scripted end to end: one script queues every iOS production build on EAS, a second submits each completed build to App Store Connect. Both scripts hardcode the local project directory and read an API key kept out of the repository, so they run from my machine rather than from CI.
    • The repository also carries a per-app improvement roadmap in app_plans/, 25 markdown files plus a shared README covering a proposed design system package, an ad strategy for free tiers and a phased rollout. Those are plans, not shipped code, and the current apps have no ad SDK.

    Lessons Learned

    "Uniformity is worth more than elegance at this count. The purchases module is duplicated in 25 places rather than extracted into a package, which is technically the wrong answer, and it is also why any single app can be opened, changed and shipped without touching a workspace or reinstalling a monorepo. Once a shared package exists, one breaking change becomes 25 broken builds."

    "Treat the store account as part of the architecture. One App Store Connect API key, one issuer id, one owner account and a consistent reverse-domain bundle scheme are what make an unattended submit script possible. The apps that are painful to ship are usually the ones whose credentials were set up ad hoc."

    "Design the empty case of the paywall first. RevenueCat offerings are genuinely unavailable until an app is approved, so the paywall has to render a sensible state with zero packages. Getting that right early removed a whole class of confusing failures during review submission."

    "Shipping without a backend is a strategy, not a shortcut. Bundled content and on-device state cost me remote updates and analytics, and in exchange removed hosting, auth, data retention and most of the privacy questionnaire from every one of the 25 apps. Related: this repository is labelled as SwiftUI and contains none, which is a reminder to verify a stack from the source tree rather than the description."

    Conclusion

    The transferable output here is not 25 consumer apps, it is a scaffold plus a pipeline where the marginal cost of app number 25 is close to the cost of its content. If you are planning a family of mobile products rather than one flagship, the decisions worth copying are the fixed entitlement contract, the shared legal and privacy surfaces, and a submission path that runs from a script.

    Frequently asked questions

    Are these SwiftUI apps?
    No. The backup repository is described as SwiftUI, but the source tree is Expo and React Native with TypeScript. Every app uses expo-router, app.json and eas.json, pinned to Expo SDK 55 and React Native 0.83.4. The only Swift in the repository is the Expo-generated AppDelegate, one file per app.
    How is the subscription entitlement handled across 25 apps?
    Each app configures react-native-purchases with its own RevenueCat key and checks one entitlement named premium. Purchase, restore, startup check and the live customer info listener all resolve to the same boolean, which is stored in app context and mirrored to AsyncStorage. UI code never inspects products or prices directly.
    Is this open source?
    No. The repository is private, so there is no public link on this page. What is described here is the scaffold and the pipeline rather than the app source. If you want the same structure for your own portfolio, it can be rebuilt from the pieces described above without any of my code.
    What does it cost to run 25 apps like these?
    There is no server bill, because the apps hold no backend of their own and make no network calls in application code. The recurring costs are the Apple Developer Program membership, a RevenueCat account, and EAS build minutes when you build in the cloud rather than locally. I have not published spend figures, so I will not quote any here.
    How does submitting all 25 apps work?
    Submission runs as one scripted batch. The first script queues every iOS production build on EAS with no-wait so builds run in parallel, then a second script submits the latest completed build of each app to App Store Connect non-interactively using one API key. Failures on individual apps are skipped rather than aborting the run.
    Can this approach work with my stack?
    It works if your apps are content-and-subscription shaped and can live without a backend. The reusable pieces are the fixed route scaffold, the single-entitlement purchase module, shared legal URLs, a committed privacy manifest and script-driven build and submit. Apps needing accounts, sync or server-side logic will need a real backend added to this base.
    Why duplicate shared code instead of using a monorepo package?
    Deliberate tradeoff. Duplicating the purchases module and the scaffold keeps every app independently buildable and shippable, which matters when one app is in review and another is being edited. A shared package would centralise the fixes and also make a single regression capable of breaking all 25 builds at once.

    Interested in a Similar Project?

    Let's discuss how I can help bring your ideas to life.

    Get in Touch

    Let's Create a Revolution