← All posts

6 min read

A Git Workflow for Small Mobile Teams (1 to 5 Developers)

A practical Git workflow for small mobile teams: trunk-based vs GitFlow, short branches, small PRs, tags matching store versions, hotfixes and keeping secrets out of Git.

  • Git
  • Workflow
  • Teams
Cover illustration for A Git Workflow for Small Mobile Teams (1 to 5 Developers)

Most Git workflow advice is written for teams of fifty. If you’re two developers shipping a Flutter app, copying that process gets you a develop branch nobody understands, three long-lived feature branches and a merge day that ruins a Friday.

Mobile adds its own twist, too. You can’t redeploy an app in five minutes. A build sits in App Store review for a while, users stay on old versions for months, and sometimes you need to fix version 1.4 while 1.5 is already half-built on main.

Here’s the workflow I’ve settled on for teams of one to five people. It’s small on purpose.

1. Pick trunk-based, unless you have a reason not to

There are two classic models:

  • GitFlow has main, develop, feature branches, release branches and hotfix branches. It was designed for scheduled releases with several versions in flight.
  • Trunk-based development has one long-lived branch, main. Everyone branches off it, merges back quickly and releases from it.

For a small team, trunk-based wins almost every time. There’s one source of truth, fewer merges and less ceremony. The main thing GitFlow buys you (a separate place to stabilize a release) is something mobile teams can get with a lightweight release tag or branch when they actually need it, without keeping develop alive forever.

The one situation where I’d lean toward a more GitFlow-like setup is when the team really does maintain several released versions in parallel, for example a white-label app shipped to multiple clients on different schedules. That’s rare.

2. Keep branches short-lived

A branch is a bet that main won’t move much while you’re away. The longer it lives, the worse the odds.

My rule of thumb: a feature branch should merge within a few days. If a feature is bigger than that, split it. Merge the data layer first, then the screen behind a feature flag, then the polish. Unfinished work can live on main as long as users can’t reach it. Feature flags make this easy, and even a hard-coded const toggle is better than a three-week branch.

Naming doesn’t need to be fancy. I use a prefix and a short description:

git switch main
git pull --rebase
git switch -c feature/invoice-pdf-export

feature/, fix/, chore/ and release/ cover nearly everything.

3. Make PRs small enough to actually review

Even on a two-person team, I want every change going into main through a pull request. Not for bureaucracy, but because a second set of eyes catches things, and because the PR becomes a record of why the code changed.

The catch is size. A 2,000-line PR doesn’t get reviewed. It gets approved. A few habits keep PRs readable:

  • One concern per PR. A bug fix and a refactor are two PRs.
  • Separate generated code. If your PR touches *.g.dart or *.freezed.dart files, mention it so the reviewer can skip them.
  • Describe how to test it. Which screen, which account, which flavor.
  • Add a screenshot or short recording for anything visual. It saves the reviewer from building the branch just to look at a padding change.

What reviewers should actually look for is its own topic. I keep a code review checklist for Flutter teams for that.

For a solo developer, I still open PRs against my own repo. CI runs on them, and I review my own diff the next morning. You’d be surprised what a night’s sleep finds.

4. Conventional commits: nice, not mandatory

Conventional Commits is a convention for commit messages:

feat(invoices): export invoice as PDF
fix(auth): refresh token before it expires on resume
chore(deps): upgrade go_router

It’s useful if you generate changelogs automatically or want a quick visual scan of history. It’s optional. What I do insist on is commit messages that explain the change in plain language. “fix stuff” and “wip” are fine on a local branch, but squash them before merging. I usually squash-merge PRs, so the PR title becomes the commit on main, and that’s where the effort goes.

5. Tag every release with the store version

This is the habit that saves the most pain on mobile. Every build that goes to the App Store or Google Play gets a Git tag matching its version:

# pubspec.yaml has version: 1.4.0+42
git tag -a v1.4.0 -m "Release 1.4.0 (build 42)"
git push origin v1.4.0

Now when a crash report comes in for 1.4.0, you can check out exactly the code users are running. Without tags, you’re guessing which commit was “probably the one we uploaded on Tuesday.”

I like to include the build number in the tag message, since stores care about it and humans forget. If your CI builds release artifacts, have it create the tag, so the tag always points at what was actually built.

6. Plan the hotfix flow before you need it

Here’s the scenario every mobile team hits eventually. Version 1.4.0 is live. main already has half of 1.5.0 merged. A bug shows up in 1.4.0 that can’t wait.

You don’t want to ship half-finished 1.5.0 work. So you branch from the tag, not from main:

git switch -c release/1.4.1 v1.4.0
# fix the bug, bump pubspec to 1.4.1+43
git commit -am "fix(checkout): handle empty cart on resume"
git tag -a v1.4.1 -m "Release 1.4.1 (build 43)"
git push origin release/1.4.1 v1.4.1

# bring the fix back to main
git switch main
git cherry-pick <fix-commit-sha>

The cherry-pick back to main is the step people forget, and then the bug comes back in 1.5.0. I put it in the PR checklist.

A related case is a version that’s in review when you find a bug. If it hasn’t been approved yet, it’s often simplest to remove it from review (or reject the build in Play Console), fix it on the release branch, and resubmit with a higher build number. Either way, the build number must always go up. Both stores reject a build number they’ve seen before.

7. Protect main with CI

main should always be releasable. The way to make that true without relying on discipline is branch protection plus CI:

  • Require a pull request to merge into main.
  • Require CI to pass: dart format --set-exit-if-changed ., flutter analyze and flutter test at minimum.
  • Block force-pushes and deletion of main.
  • On teams of three or more, require at least one approval.

GitHub, GitLab and Azure Repos all support these settings. The CI side is where most of the value is. I walk through a full pipeline in Flutter CI/CD with Azure DevOps, and the same ideas carry over to GitHub Actions.

8. Keep secrets out of Git

Once a secret is pushed, deleting it in the next commit doesn’t help. It’s still in history, and in every clone. Prevention is much easier than cleanup.

For Flutter projects, my .gitignore always includes:

# Android signing
android/key.properties
*.jks
*.keystore

# Local environment
.env
*.env
config/*.local.json

# iOS signing and local config
ios/**/*.mobileprovision
ios/**/*.p12

The Android upload key and its passwords live in a password manager and in the CI secret store, nowhere else.

Firebase config files (google-services.json and GoogleService-Info.plist) are a judgment call. They identify your Firebase project but aren’t secret keys in the usual sense; your security comes from Firebase rules and API key restrictions, not from hiding them. I commit them on most projects because it makes setup easier. I keep them out when a project has several flavors pointing at different Firebase projects and CI injects the right one, or when the client simply prefers it. Pick one approach and write it down in the README.

If a real secret does get pushed, rotate it first. Then clean history if you need to. Rotating is the part that actually protects you.

Takeaways

  • For one to five developers, trunk-based development with short-lived branches is usually enough.
  • Keep PRs small and focused, with test notes and screenshots.
  • Tag every store release with its version so you can always check out what users run.
  • Hotfix from the release tag, then cherry-pick the fix back to main.
  • Protect main with required PRs and CI checks.
  • Ignore signing files and env files from day one, and rotate anything that leaks.

A good workflow for a small team is one that nobody has to think about. Keep it that boring.

Comments

Questions, corrections or your own experience — leave a comment below (GitHub sign-in).