Mobile apps for iOS and Android.
From first MVP to store release. We build with Flutter and React Native, and go native when the project calls for it.
Native or cross-platform, decided on facts.
Almost every mobile project starts with this question, and most answers are delivered as an ideology. It is a technical choice with a small number of real inputs, and it is usually settled in an afternoon once those inputs are on the table.
Cross-platform is the right default when
- The app is screens, forms, lists, media and API calls — which describes most products people pay to have built.
- iOS and Android have to ship together, from one team and one backlog.
- Your own design language matters more than matching each platform's stock controls exactly.
- The budget has to cover two platforms, and a second native team is not on the table.
Native earns its cost when
- The app lives on hardware: sustained Bluetooth Low Energy, camera pipelines, sensors, augmented reality, precise background behaviour.
- It depends on a vendor SDK shipped only for Swift and Kotlin, with no maintained bridge.
- It leans on OS surfaces that are native by definition — home-screen widgets, live activities, watch apps, share extensions, deep system integration.
- One platform is the product and the other is an afterthought.
- A team already maintains a native codebase that you are extending rather than replacing.
It is also not all-or-nothing. A cross-platform app can call native code where it needs to, and a native app can host a cross-platform screen. What decides the shape is where the difficult ten percent sits, not the easy ninety.
What is in a mobile app besides the screens.
The design shows the screens. The build is the screens plus everything the two platforms require around them, and that second list is where mobile projects overrun.
- Onboarding and permissions: asking for notifications, camera, photos or location at a moment where the user understands why — and behaving correctly when the answer is no.
- Account handling: sign-in, password reset, session expiry on a device someone keeps for years, and in-app account deletion, which Apple requires of any app that lets people create an account.
- Sign in with Apple, which Apple requires when you offer other third-party sign-in options.
- Payments: physical goods and real-world services take an ordinary payment provider. Digital content and subscriptions consumed inside the app go through App Store and Play billing, with the platform commission and the receipt validation that come with it.
- Crash reporting and product analytics wired up before launch, because the first week's crashes are the ones that set your rating.
- A privacy policy, Apple's app privacy disclosure and Google's data safety form — both of which have to match what the app actually sends.
- Store assets: icon, screenshots at each required device size, descriptions and keywords in every language you list.
- Offline behaviour, empty states, error states, and what the app does on a slow connection in a lift.
None of that is optional and none of it appears in a design file. We scope it explicitly, so it is priced once, at the start, instead of arriving as a series of small surprises.
Store submission is a process, not a button.
Both stores review apps and both reject them. Rejections are usually procedural rather than fatal, but each round costs days. The point is to submit an app that has already answered the reviewer's questions.
Accounts and signing
The Apple Developer Program account and the Google Play Console account belong to you, in your company's name. An organisation account on Apple's side needs a D-U-N-S number, which takes time to obtain and is therefore started early rather than the week before launch. We work inside your accounts. Signing keys and certificates are yours and stay yours, because losing them is one of the few genuinely painful mistakes in mobile.
Why apps get rejected
- A crash on the reviewer's device — the most common reason, and the easiest to avoid by testing the exact build being submitted.
- No working demo account, so the reviewer cannot get past your sign-in screen.
- Permission strings that do not explain what the app does with the camera, the location or the contacts.
- Privacy answers that do not match what the app actually sends, or a privacy policy that is missing or unreachable.
- Digital purchases routed around the store's own billing.
- Too little functionality to justify an app: a wrapper around a website that does nothing a browser did not already do.
- Screenshots or descriptions showing features the app does not have.
- A sign-in wall in front of content that has nothing to do with having an account.
Before production
Builds go to TestFlight and to Play's internal or closed testing track first, on real devices, with the people who will support the app. Google also requires new personal developer accounts to run a closed test before they can publish publicly, which is a schedule item rather than a formality. On Android, releases roll out in stages, so a bad build reaches a fraction of users instead of all of them.
Push, offline and deep links.
Push notifications
Notifications go through APNs on iOS and FCM on Android. The work is less in sending them than in the lifecycle around them: asking for permission at a moment that earns a yes, storing and refreshing device tokens, discarding dead ones, handling a tap that has to open one specific screen from a cold start, and keeping a record of what was sent to whom. iOS users have to opt in, so the first prompt is a product decision before it is a technical one.
Offline
A phone loses its connection constantly. Deciding what the app does then is design work: which data is cached and for how long, which actions can be queued and replayed, and what happens when two devices changed the same record. We do the simple version deliberately — read-only caching with an honest stale indicator — unless the product genuinely needs offline writes, in which case conflict resolution is scoped as its own piece of work rather than discovered late.
Deep links
A link in an email or an advert should open the right screen in the app when it is installed, and a web page when it is not. That needs Universal Links on iOS and App Links on Android: an association file served from your domain and verified by the OS, plus routing inside the app that survives a cold start. It is fiddly, and it is worth doing properly, because it is the difference between a campaign that converts and one that drops people on a home screen.
The half of a mobile app that is not the app.
An app is a client. Nearly everything it shows comes from somewhere, and that somewhere is part of the project even when the brief only mentions screens.
- An API designed for a mobile client: few round trips, small payloads, and versioned so an old app on somebody's phone keeps working after you deploy.
- Authentication that survives a device kept for years: refresh tokens, revocation, and a sensible answer to a password changed on another device.
- Push infrastructure: who may send what, to whom, with a record of it — rather than a console somebody opens by hand on a Friday.
- File and media handling: upload from a phone on a poor connection, resize, store, and serve at the size the screen actually needs.
- An admin surface for your team: find a user, see their state, fix it, without a developer.
- Analytics and crash reporting joined to real accounts, so a report is something you can act on rather than a number on a chart.
We build the backend too — usually Next.js and PostgreSQL — or integrate with what you already run. Either way the app is only as good as what stands behind it, and a mobile estimate that ignores the server is not an estimate.
Version skew
Old versions of an app live on phones for a long time, and you cannot make people update. So the API has to tolerate them, and the app needs a way to say that a version is too old to continue: a check on launch, a screen that explains why, and a link to the store. Deciding this late leaves you choosing between breaking users and freezing the API.
An app is a subscription, not a purchase.
Mobile platforms move every year. An app left untouched for a year and a half often cannot even be built any more, let alone shipped.
- Apple requires new submissions to be built against a recent SDK, so a release needs a current Xcode and whatever OS changes come with it.
- Google Play requires apps to target a recent API level, refreshed annually, or updates stop being accepted.
- Each OS release changes something visible: permissions, background execution, notification behaviour, privacy prompts, layout at the edges of new hardware.
- Certificates, provisioning profiles and push keys expire on their own schedule, usually at an inconvenient moment.
- Dependencies move underneath you, and a single abandoned native module can hold back an entire upgrade.
So plan a maintenance release a few times a year even when the product itself does not change, and treat each OS major version as a known event rather than a surprise. It is cheaper than the alternative, which is a revival project the moment something finally breaks.
What done means for an app.
- The app is live on both stores, under your developer accounts, with your signing keys in your possession.
- Crash-free rate and the events that matter are visible in a dashboard your team can open without asking us.
- The release process is written down and repeatable — anyone on the project can cut a build and ship it.
- The repository is yours, with its history, its CI configuration and a README that gets a new developer running.
- The backend, the environments and the third-party accounts are yours, documented, with credentials handed over.
- There is a support path: how an issue reaches us, how it is triaged, and what happens next.
Launch is the start of the useful part. The first weeks after release produce the only feedback that counts — real devices, real networks, real users — and the plan leaves room to act on it.
The questions that come before a contract.
- Native or cross-platform — how do you decide?
- By what the app actually does. If it is mostly screens, forms, data and notifications, one Flutter or React Native codebase serves both stores. If it leans hard on platform-specific hardware, background behaviour or the newest OS APIs, native Swift and Kotlin is the honest answer. We make that call with you during scoping, in writing.
- Does one codebase mean half the cost?
- It removes most of the duplicated interface work, not all of the cost. You still pay once for design, backend, testing on both platforms, and two store submissions. Expect a real saving against two native builds, not a fifty percent one.
- Who owns the App Store and Google Play accounts?
- You do, always. Apps are published under your developer accounts, with your company as the seller. We handle signing, build configuration, store listings and review responses from inside your accounts, so nothing has to be transferred later.
- Can you build the backend too, or only the app?
- Either. We can build the API, authentication, push notifications and admin side alongside the app, or integrate with the systems you already run. If a backend exists, we start by reading its contract and telling you what the app will need added to it.
- What happens if the scope changes mid-build?
- Small changes are absorbed into the current sprint by re-prioritising, not by quietly extending the deadline. Anything larger gets a written estimate of cost and schedule impact, and you decide before it is built. Every accepted change is logged against the original scope.
- What does maintenance look like after release?
- Mobile apps do not stand still: iOS and Android ship a major version every year, SDKs get deprecated and store policies change. Plan on maintenance releases even in a quiet year. We can carry that, or document it and hand it to your team.