Concurrences got native iOS and Android on its own backend, still running two-plus years later

Concurrences is a Paris legal publisher covering antitrust law and competition economics. Its website worked; its conferences were hard to reach from a phone. RaftLabs built a Flutter app for iOS and Android on top of the client's existing PHP backend, without adding an endpoint or owning a line of it. Two-plus years later it still runs on maintenance visits roughly every six months. Adoption was slower and thinner than we expected, and this page says so.

users on the product, below what we expected
200-300
live on both stores without a rebuild
2+ years
app-side issues after the first release
Near-zero

Short answer

RaftLabs built the Concurrences Events app, a Flutter iOS and Android companion to a Paris legal publisher's existing website. RaftLabs owned only the mobile layer: the client's own developer ran the PHP backend throughout, and no backend endpoints were added for the app. Real-time attendee chat runs on Firebase, decoupled from that backend. The app has been live for more than two years on maintenance visits roughly every six months, with around 200 to 300 users, and almost every post-launch issue traced to a backend change rather than the app. When Twitter's API repricing made the in-app live-tweets feature uneconomic, RaftLabs removed the API integration and replaced it with an in-app webview.

Engagement

The Concurrences engagement

Client
Concurrences, a Paris legal publisher covering antitrust law and competition economics, roughly 15 to 25 people
Sector
Legal publishing and professional conferences across Europe
What we owned
The mobile layer only. The client's own developer ran the PHP backend before, during, and after the build
Delivery
Initial build over a few months, then maintenance and feature runs roughly every six months for two-plus years
Team
One mobile engineer, one project manager, one product owner, with design support
Scope
Flutter apps for iOS and Android, Firebase chat and push, webview sections against the existing site, platform-specific payment handling
Status
Live on both stores. Maintenance only. A full redesign was produced, approved, and never commissioned

The situation

Concurrences publishes on antitrust law and competition economics, and runs conferences on the same subjects across Europe. Its audience is lawyers, economists, and regulators. Its website worked. On a phone it was a website, which for a body of published articles and a conference schedule is a worse experience than it sounds.

The ask was narrow and clear: give that audience a native way in, without rebuilding anything underneath. The backend was PHP, it worked, and Charles, the client's own developer, ran it. He kept running it for the whole engagement. RaftLabs owned the Flutter layer and nothing else.

That constraint is the whole story of this project. Every interesting decision on it comes from the same place: we could not add an endpoint, change a data model, or fix a bug below the API line. So anything the existing backend could not do, we either built beside it or did not build.

The honest part is the outcome. The app has been live for over two years, it almost never breaks, and the client almost never complains. It also reached around 200 to 300 users, with slow uptake and less engagement than we expected. Both of those things are true and this page reports both.

Concurrences Events Flutter mobile app for legal conference discovery and registration

Before and after

What changed, and what did not

Before
  • A working website and no native app. Phone users got a desktop experience scaled down
  • Published articles and conference listings reachable only through a browser session, with nothing cached and nothing pushed
  • No channel for delegates to reach each other around an event
  • Payment flows built for a desktop browser, with no allowance for what mobile platforms do to them
  • No analytics on mobile behaviour at all, because there was no mobile product to measure
After
  • Native iOS and Android apps from one Flutter codebase, built as a pure client of the existing PHP API
  • Real-time attendee chat and push notifications, running entirely on Firebase and independent of the client's backend
  • Previously loaded content available without a live connection
  • One sign-in carried from native screens into the website-backed sections, so users are not asked to log in twice
  • Payment flows reworked to survive platform rules rather than assume a desktop browser
  • Firebase and app store analytics giving the client a first view of mobile behaviour

The problems worth reporting

  • Real-time chat, with no ability to change the backend

    Delegates needed a way to reach each other around an event. Chat needs a persistent, low-latency data layer, and the only backend in the picture was a PHP system we did not own and could not deploy to. Routing chat through it would have meant a change request to the client's developer for every future change to the chat model.

    We built chat on Firebase instead, as a second data layer sitting beside the PHP API rather than on top of it. Push notifications ride the same infrastructure. The chat model has changed since without anyone touching the client's backend, which was the point of the choice.

    The cost is that the app now has two sources of truth: identity and content from the client's API, messaging from Firebase. That is a reasonable trade when you own neither side, and a bad one when you own both.

  • A shipped feature that became uneconomic because of someone else's pricing change

    The client asked for live event tweets in the app. We built it against the Twitter API and it worked exactly as specified. Then Twitter changed hands, the API pricing was restructured, and a feature that had been a normal integration became absurdly expensive to keep running.

    Nobody could have priced this in at kickoff, and we are not going to pretend otherwise. What we could control was the response. We flagged the cost, removed the API-backed integration, and replaced it with an in-app webview that opens the relevant pages directly. The feature's intent survived. The recurring cost and the dependency did not.

    This is the clearest thing on the engagement that we did not see coming, and it is the reason the lesson at the bottom of this page is about third-party risk rather than about Flutter.

  • Payments that had to be rebuilt per platform, not per browser

    The client's payment setup was designed for a desktop browser. Mobile platforms, iOS in particular, do not treat a payment flow inside an app the way a browser does, and Apple's rules around what an app may charge for and how are not negotiable.

    We could not change the client's payment infrastructure, so the handling had to be built specifically for the platform rather than inherited from the web flow. That work is unglamorous and it is where a meaningful part of the build time went. If you are putting an existing web checkout inside an app, budget for it as a workstream, not as a detail.

  • A state management choice that paid for itself and then charged interest

    The project used BLoC for state management, chosen on this project and no other, to raise code reuse across screens. It did raise code reuse. It also made the codebase harder to move around in: contributors have to understand the pattern before they can be useful, and debugging means tracing events through the BLoC layer rather than reading a simpler flow.

    Asked what he would do differently, the engineer who has lived with this codebase longest said he would not use BLoC the way it was used here. On a product of this size the reuse it bought did not clear the friction it introduced over two years of low-frequency maintenance.

    Two related things belong in the same paragraph, because they are the same problem: this codebase is thin on documentation and its branching history is untidy. That is a real cost on a product that gets touched twice a year by whoever is free.

the build

How the mobile layer was built around a backend we did not own

  1. Everything the existing API could serve, we consumed as-is

    No endpoints were added for the app, and no data model was changed to suit it. Where the API gave us something awkward, the app absorbed the awkwardness. That is slower than negotiating a better contract with the backend, and it is the correct default when the backend belongs to a one-person team with their own roadmap. It also means the client was never left holding backend changes that only existed to serve our app.

    Concurrences Events Flutter app consuming the client's existing PHP API
  2. Anything it could not serve, we built beside it on Firebase

    Real-time chat and push notifications are the two capabilities the PHP backend was never going to provide, so they run on Firebase as a parallel layer we control end to end. Latency, data model, and scaling decisions for messaging stayed inside our scope. Nothing about chat requires a backend deployment on the client's side, then or now.

    Firebase real-time attendee chat in the Concurrences Events Flutter app
  3. One session across native screens and website-backed sections

    Parts of the product, including account management and some registration flows, open Concurrences web pages inside the app rather than being rebuilt natively. Rebuilding them would have meant duplicating logic that lives on the client's side and drifts when they change it. A user signed in to the app arrives at those pages already authenticated, so the seam is invisible in use even though it is explicit in the code.

    Session carried from native screens into a webview section in the Concurrences app
  4. Content cached so the app is not useless without a signal

    Delegates read agendas and speaker details in conference venues and in transit, which are the two worst places for connectivity. Content already loaded stays available, with control over what is held and when it expires, so a schedule that changes upstream does not sit stale in a pocket.

    Concurrences Events app showing cached event content without a connection

Proof

What the app actually produced

RaftLabs never held the client's backend or their analytics. Every figure below is approximate, comes from Firebase and app store analytics on our side, and was given by the delivery engineer rather than pulled from a client system.

ResultWhat changedPeriod or contextEvidence and limitation
UsersRoughly 200 to 300 on the productCumulative across iOS and Android over the life of the appFirebase and app store analytics, reported by the delivery engineer. Below what the team expected for the audience size, and stated as such
Time to visible uptakeAround six monthsFrom first releaseEngineer's recollection, not a measured cohort. Adoption was slow rather than absent
EngagementLower than expectedOver the two-plus years liveThe team's own assessment. This is the weakest result on the engagement and the reason the recommendations below are about distribution, not features
StabilityAlmost no app-side issues after the first releaseTwo-plus years in production on both storesPost-launch issues traced to backend or API changes on the client's side, resolved by a patch. None reported in a long while
Maintenance loadA run roughly every six monthsOngoing since launchNo rebuild has been needed. The app has stayed on both stores without a major version reset
Access to published content and eventsImproved, not quantifiedArticle access and event registration through a native pathThe conversion data sits in the client's backend, which we do not have access to. We are not putting a number on this

Timeline

How the engagement ran

Before the build

A working site, a missing native path

Concurrences already had the content and the conferences. The gap was reach: a professional audience that wanted articles and event registration on a phone, without the publisher rebuilding anything to give it to them.

  • Initial build, a few months

    Flutter app for both stores, on the existing API

    One mobile engineer, one project manager, one product owner, with design support. Chat and push on Firebase, webview sections for the flows that belonged on the website, and payment handling built per platform.

  • After launch

    Slow uptake, then quiet

    Adoption took around six months to become visible and settled well below expectation. Complaints, on the other hand, effectively stopped after the first release.

  • After the Twitter repricing

    A feature removed on purpose

    The API-backed live tweets were withdrawn once the economics broke, and replaced with an in-app webview. A deliberate decision to stop paying for something rather than absorb it quietly.

  • Ongoing, two-plus years

    Maintenance runs roughly every six months

    Small fixes and feature runs. Most work has been reactive patching after a change on the client's backend. No rebuild has been required.

The lesson

If you do not own the backend, own the seam. And treat every third-party API as a price that can change

Building on someone else's backend is a reasonable choice more often than agencies admit. Concurrences had a working system and a developer who knew it. Replacing that to make an app possible would have been the expensive answer to the wrong question. The app consumed the existing API, added a Firebase layer for what the API could never do, and left the client's system of record exactly where it was. Two years on, the app has needed patches when the backend moved and almost nothing else. The seam held because it was designed as a seam.

The second half of the lesson cost more to learn. A feature built correctly against a third-party API is still a feature you do not control the price of. Twitter's repricing turned a working integration into an unjustifiable line item, through no fault of the build. The right response was not to absorb it and not to wait for the client to find it on an invoice: it was to name the cost, remove the dependency, and keep the intent with a webview. Before you ship anything that depends on an external API, ask what happens if its price rises tenfold, and know in advance what the downgrade path is.

The third thing is the one most case studies omit. A stable app that nobody uses much is not a success. This one shipped, held up for two years, and reached a few hundred users with thin engagement. A niche professional audience does not install an app because it exists, and a redesign that stays unshipped does not help. If you are in the same position, plan the distribution and the reason to open it weekly with the same seriousness you plan the build.

Where to go next

What we would recommend next

These are opportunities beyond the delivered scope, not work that was included or commissioned.

  1. Next 01

    Treat adoption as the open problem, not stability

    The app is stable and under-used. Two or three hundred users against an international readership of antitrust practitioners is a distribution result, not an engineering one. The next useful spend is on getting the app in front of conference registrants at the moment they register, not on new features.

  2. Next 02

    Ship the redesign or formally retire it

    A design was produced, approved, and left unbuilt for two years. Either it goes into a maintenance run or it gets written off. Leaving it in limbo means every conversation about the app starts by relitigating it.

  3. Next 03

    Document the codebase and tidy the branching

    This product is touched roughly twice a year, often by whoever is free. Thin documentation and an untidy branch history are what make those visits cost more than they should. A single documentation pass would pay for itself within two maintenance runs.

  4. Next 04

    Put a contract test on the client's API

    Almost every post-launch incident came from a backend change upstream. A small automated check against the endpoints the app depends on would catch those before a user does, without needing any access to the backend itself.

How it runs

Flutter
Two stores, one small team, and an app that had to be a client of an API rather than an owner of a system. Flutter gave a single codebase for iOS and Android with native-feeling performance and first-class webview support, which mattered because several flows were always going to open Concurrences web pages rather than be rebuilt natively. One disclosure: the project used BLoC for state management, chosen only on this project to raise code reuse. It did raise reuse, and it also made the codebase harder to work in over two years of infrequent maintenance. We would not make that call the same way again at this size.
Firebase
The two capabilities the client's PHP backend was never going to provide were real-time chat and push notifications, and we could not add them to a backend we did not own. Firebase gave both as a parallel layer under our control: Firestore for messaging, FCM for push, and the analytics that produced the only usage numbers on this page. Changes to the chat model have never required a deployment on the client's side.

Common questions

Yes, and that is exactly what happened here. RaftLabs built the entire iOS and Android app as a consumer of the client's existing PHP backend and website. No endpoints were added for the app and no data model was changed. The client's developer ran that backend before, during, and after the engagement. Where the existing backend could not provide something, in this case real-time chat and push, we added a Firebase layer alongside it rather than asking for backend changes.

On this project it happened. The live-tweets feature was built against the Twitter API to specification, then became uneconomic after the API was repriced. We flagged the cost, removed the native integration, and replaced it with an in-app webview that preserved the feature's intent without the recurring bill. The general rule we now apply: before shipping anything on an external API, agree what the downgrade path is if the price moves.

The initial build ran a few months. The team was one mobile engineer, one project manager, and one product owner, with design support. After launch the engagement continued for more than two years as maintenance and feature runs roughly every six months. We describe commercials as a shape rather than a figure across the portfolio, and on this engagement the budget shape is not first-hand, so this page does not state one.

One codebase for both stores, with a small team and a fixed appetite for spend. Flutter produces genuinely native-feeling apps and handles the webview sections this product needed, since several flows deliberately open Concurrences web pages instead of being rebuilt. The trade is that platform-specific behaviour, particularly around payments, needed targeted work that a native build would have got for free.

Around 200 to 300 users, with uptake taking roughly six months to become visible and engagement below what the team expected. Against that, the app has been live for over two years with almost no app-side issues, and nearly every post-launch problem traced to a change on the client's backend rather than the app. We treat the adoption number as the weak result on this engagement and we are not dressing it up.

Three things. Not use BLoC the way it was used here, because the code reuse it bought did not clear the friction it added over two years of infrequent maintenance. Document the codebase and keep the branching disciplined, for the same reason. And take the adoption question as seriously as the build question, rather than treating a stable launch as the finish line.

Yes. The app still runs on maintenance visits roughly every six months, and no rebuild has been needed. A full redesign was produced and approved during the engagement and has never been commissioned, which remains the largest open item on the product.

Work with us

Recognise this problem in your business?

Tell us what's broken. We'll diagnose it and show you exactly what to fix first, before you commit to anything.

  • Scope and cost agreed before work starts. No surprises. No obligation.
  • Working prototype within 3 weeks of kickoff.
  • Pay by milestone. You see progress before each invoice.
  • 60-day post-launch warranty. Bug fixes, UI tweaks, and deployment support. No retainer.
  • All conversations are NDA-protected.