Skip to content

· 5 min read · Nadeera Silva

Mobile

Taking Over an Existing Flutter App and PHP Backend: My Playbook

How I take over a live Flutter app with a legacy PHP backend: a first-week checklist, reproducing bugs, PHP security fixes and a safe move to token auth.

A ring of brass keys and a teal wrench on a folded cloth in front of a laptop showing a live dashboard with a warning sign, next to stacked servers and a phone, illustrating taking over a live app

Some of my freelance work starts with an app that's already in production. On Recipians, I took over a client's live Flutter app with a PHP and MySQL backend and an admin panel. The work covered debugging across all three parts, security hardening, token-based authentication and in-app purchase billing. If you're about to take over an existing Flutter app or a legacy PHP codebase, this is the playbook I'd follow, with the checks and code I'd start with.

Week one: change nothing

The first job is to understand the system, not improve it. Real users depend on it, and the original developer may not be around.

CheckWhy it matters
Source for the app, backend and admin panel matches what's deployedYou can't safely fix code that isn't what's running
Who holds App Store Connect, Play Console, domain, hosting and database accessA release you can't publish is a release you can't fix
How backend code reaches the serverStops a "quick fix" overwriting someone's unrecorded change
A database and file backup that you've actually restoredA backup you've never restored is a guess
A map of which endpoints the app calls and what the admin panel can changeAnswers "what will this break?"

On the Flutter side, a few commands tell you a lot in ten minutes:

flutter --version          # compare with the version the project was built on
flutter pub get
flutter analyze            # how much has drifted
flutter pub outdated       # how far behind the dependencies are
flutter build apk --release   # can it still build at all?

If the release build fails, that's your first task, before any bug. You need to be able to ship a fix.

Diagram of the takeover order: map and back up the system, reproduce and fix bugs, harden security, then add features such as billing
Understand first, then fix, then harden, then build.

Reproduce bugs before fixing them

Client reports describe symptoms. "Payments sometimes fail" could be an app bug, an API bug, a database problem or a third-party outage. For each issue:

  1. Identify which part fails: the app, the API or the admin panel.
  2. Check server and error logs around the time it happened.
  3. Reproduce it on a test build or a staging copy.
  4. Fix it and confirm the original steps no longer fail.

Fixing a bug you can't reproduce is guessing. Sometimes that's necessary, but it should be the exception.

Legacy PHP security: where to look first

Older PHP and MySQL backends tend to share the same risks. This is the order I review them in.

SQL injection. Any query that joins request data into a string should become a prepared statement. OWASP's cheat sheet lists prepared statements as the first defence:

// Before: injectable
$rows = $db->query("SELECT * FROM orders WHERE user_id = " . $_GET['user_id']);

// After: PDO prepared statement
$stmt = $pdo->prepare('SELECT * FROM orders WHERE user_id = ?');
$stmt->execute([$userId]);
$rows = $stmt->fetchAll();

Password storage. Passwords should go through password_hash, not plain text or a fast hash like MD5. You don't need a forced reset to migrate; upgrade each user's hash the next time they sign in:

if (password_verify($password, $user['password_hash'])) {
    if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
        $newHash = password_hash($password, PASSWORD_DEFAULT);
        // save $newHash for this user
    }
    // signed in
}

For legacy MD5 hashes, check the old hash once at sign-in, then replace it with a password_hash value straight away.

Authorization on every endpoint. Each endpoint should check that the caller may access the record they asked for. Hiding a button in the app isn't access control.

Secrets in code. API keys and database passwords belong in server configuration, not the repository or the app bundle. I explain why build-time tricks don't hide keys in how to hide API keys in Flutter.

Admin panel. Strong authentication, not easy to find, and protected against brute force.

Errors and input. Validate input on the server, and make sure errors never print stack traces or SQL to users.

Moving a live app to token-based authentication

Adding token-based authentication was part of the Recipians work. If an API relies on something weaker, such as the app sending a user ID with each request, it can't tell who's really calling.

With tokens, the user signs in once, the server issues a token, and the app sends it with every request. The server checks it first and derives the user from it, instead of trusting an ID from the request.

Rolling this out to a live app needs a transition plan:

  • Older app versions that users haven't updated will still call the API the old way.
  • Decide how long the old method keeps working, and when to require the update.
  • Make the app handle an expired token by asking the user to sign in again, not by failing silently.
  • Store tokens on the device with flutter_secure_storage, not plain shared preferences.

Adding billing to an existing app

Adding in-app purchases to an app with existing users touches live account data. Keep it additive: new tables or fields for purchases and subscription status, existing user records left alone. Verify purchases on the server as you would in a new app (my StoreKit subscriptions post covers the server side), and keep billing out of the admin panel's general edit screens so access can't be changed by accident.

Keep the client informed

After the first week, you'll often know more about the internals than the client does. Short, regular updates help:

  • What I found, in plain language.
  • What I fixed, and how I tested it.
  • What's still risky, and what I'd recommend next.

Clear updates build trust and turn a pile of fixes into a plan. If the Flutter side's state management is part of the mess, my Riverpod best practices post shows the structure I migrate towards.

You can read about the project on the Recipians case study page.

Frequently asked questions

What should I do first when taking over an existing app?

Change nothing. Get access to every account and repository, confirm the code matches production, take a backup and restore it once, and map how the parts talk to each other.

How do I secure a legacy PHP API?

Move string-built SQL to prepared statements, hash passwords with password_hash, check authorization on every endpoint, move secrets to server config and stop errors leaking details.

How do I migrate old password hashes in PHP?

Upgrade them at sign-in: after password_verify succeeds, use password_needs_rehash and save a new password_hash value. For MD5 hashes, check the old hash once and replace it straight away.

How do I add token authentication to a live app?

Issue a token at sign-in and derive the user from it on every request. Keep the old method working for older app versions for a planned period, then require the update.

What if the inherited Flutter app doesn't build?

Make the release build work first. Until you can ship, you can't deliver any fix.

Sources

  1. SQL Injection Prevention Cheat Sheet (OWASP)
  2. Prepared statements and stored procedures (PDO) (PHP)
  3. password_hash (PHP)
  4. password_needs_rehash (PHP)
  5. password_verify (PHP)
  6. flutter_secure_storage (pub.dev)
SharePost on XLinkedIn

Comments

Share a question or note about this article.

Loading comments…

Taking Over an Existing Flutter App and PHP Backend | Nadeera Silva