Auditing a mobile codebase before changing it
You have just taken over a mobile app. Maybe the original development team left, the agency contract ended, or your company acquired the product. Whatever the reason, you are now responsible for a codebase you did not write, with decisions you did not make, targeting deadlines you did not set. The first thing you need is a structured audit.
A codebase audit is not about judging the previous team. It is about building an accurate picture of what you are working with so you can make informed decisions about what to fix first, what risks are lurking, and how much effort future changes will require. Here is the checklist we use when inheriting a mobile app codebase.
Step 1: Dependency Inventory
Start with the dependency manifest. On Android, this is your build.gradle or build.gradle.kts files. On iOS, check Podfile, Package.swift, or Cartfile depending on which package manager is in use. Some older projects still use manually dragged-in frameworks with no package manager at all, which is itself a finding worth noting.
For each dependency, record three things: the current version in the project, the latest available version, and whether any known security vulnerabilities exist. On Android, run ./gradlew dependencyUpdates using the Gradle Versions Plugin. On iOS, pod outdated gives you a quick view if the project uses CocoaPods. For Swift Package Manager, Xcode shows available updates in the package dependency inspector.
Use the package ecosystem's audit tooling and an advisory database to identify known vulnerabilities, then establish whether the affected code and vulnerable path are present in the shipped application. A CVE identifier is a lead, not a severity decision. Prioritize from exploitability, exposure, data sensitivity, available mitigations, and the vendor's fix.
Pay special attention to dependencies that are more than two major versions behind. A library at version 3.x when the current release is 6.x often means the migration path is non-trivial and may involve breaking API changes across multiple upgrade steps.
Step 2: Architecture Assessment
Map runtime responsibilities before naming an architecture. Trace one critical journey from UI event through state, persistence, network, and error handling. Labels such as MVVM or Clean Architecture are useful only when they help you find ownership and predict the effect of a change.
Check for separation of concerns. Are network calls happening directly in view controllers or activities? Is business logic mixed into the UI layer? Are there distinct data, domain, and presentation layers, or is everything in one place?
On Android, check whether the project uses Jetpack components (ViewModel, LiveData, Room, Navigation) or older patterns (AsyncTask, Loader, raw SQLite). On iOS, look for whether the project uses Combine or async/await versus older callback-based patterns, and whether it has adopted SwiftUI or remains entirely UIKit.
Document the navigation structure. Is there a navigation graph (Android) or coordinator pattern (iOS), or does each screen push and pop directly? Spaghetti navigation is one of the biggest sources of bugs in inherited apps.
Step 3: Test Coverage Analysis
Run the existing test suite and measure what you actually have. On Android, ./gradlew testDebugUnitTest and ./gradlew connectedDebugAndroidTest will execute unit and instrumentation tests respectively. On iOS, xcodebuild test with the appropriate scheme runs the test target. Enable code coverage reporting to get a percentage.
The raw coverage number matters less than where the coverage exists. An app with 40% overall coverage but solid tests around the authentication flow, payment processing, and data persistence layer is in much better shape than an app with 70% coverage that only tests utility functions and data models.
Check whether the tests actually run. In many inherited codebases, the test target has been broken for months. Tests that do not compile or that have been skipped with annotations like @Ignore or disabled are effectively nonexistent. Count only tests that execute and pass.
Step 4: CI/CD Pipeline Review
Look for CI/CD configuration files: .github/workflows, Jenkinsfile, .gitlab-ci.yml, bitrise.yml, fastlane/Fastfile, or circle.yml. If none exist, that is a significant finding. It means builds and releases have been done manually from a developer's local machine, which introduces reproducibility and security risks.
If a pipeline exists, check what it actually does. Does it run tests? Does it run lint checks? Does it build both debug and release variants? Does it handle code signing and provisioning profile management? Does it deploy to TestFlight or Google Play internal tracks, or does someone still upload builds manually?
Verify that the pipeline actually works by triggering a build. Stale CI configurations that reference old Xcode versions, expired signing certificates, or removed environment variables are common in inherited projects.
Step 5: Crash Analytics and Production Health
Find the production-health data before changing instrumentation. The app may use Crashlytics, Sentry, Bugsnag, Datadog, a vendor-specific system, or nothing. If telemetry is absent, document the privacy and release work required to add it rather than quietly shipping a new data collector.
Record crash-free users and sessions separately, with their time window, app version, and sample size. Compare releases and critical journeys against the product's own baseline. A universal percentage does not account for traffic, session length, or the consequence of the failure.
Check whether crashes correlate with specific OS versions, device models, or app versions. A spike in crashes on Android 14 that the previous team never investigated tells you exactly where to focus early maintenance effort.
Step 6: Code Style and Lint Configuration
Check for linter configuration. On Android, look for lint.xml, .editorconfig, and ktlint or detekt configuration. On iOS, look for .swiftlint.yml. If linting is configured, run it and count the warnings. A project with 400+ lint warnings has been ignoring code quality enforcement for some time.
Check whether the project uses any code formatting tool (ktfmt, swift-format, or Prettier for React Native). Inconsistent formatting across files often indicates multiple developers with different editor configurations working without a shared standard.
Step 7: Build Configuration Audit
Review the build configurations and signing setup. On Android, check the build.gradle for signing configs, build types, product flavors, and ProGuard/R8 rules. On iOS, examine the Xcode project for build configurations, provisioning profiles, entitlements, and Info.plist values.
Verify that debug and release builds are properly separated. Check that API keys, backend URLs, and feature flags differ between environments. Hardcoded production API keys in debug builds or debug logging enabled in release builds are common findings that represent both security and performance risks.
Step 8: Documentation Gaps
Check for a README with build instructions. Can a new developer clone the repo and build the app on the first try? Missing environment variables, undocumented dependencies on local tools, or missing configuration files that are gitignored are the most common blockers.
Look for architecture decision records, API documentation, or any notes from the previous team about known issues or workarounds. The absence of documentation is itself a finding, and documenting what you learn during the audit is one of the most valuable outputs of this process.
Step 9: Technical Debt Scoring
Score each finding using consequence, likelihood, reach, detectability, and cost of delay. “No CI” may be urgent when releases cannot be reproduced and less urgent when the immediate risk is lost user data. Preserve the reasoning alongside the score.
Turn the findings into a backlog with evidence, an owner, a proposed verification method, and dependencies. Stabilize the delivery path before attempting broad refactors so each later change can be built, tested, released, and observed.
This structured approach transforms an unknown codebase into a known quantity with a clear path forward. You may not like everything you find, but at least you will know exactly what you are dealing with.
DEVSFLOW Maintenance audits inherited applications and defines an evidence-based stabilization and maintenance plan. Review the standard maintenance scope.