Multiple White-Label Apps with GitHub Forks

Multiple White-Label Apps with GitHub Forks

Managing white-label applications in a growing mobile ecosystem can quickly become a nightmare—especially when each brand or client needs their own features, visuals, or logic.

At Transapp, we started with a simple approach: a monorepo and a branch per app. But as the number of apps and their differences grew, so did the pain. This post explains why we moved from a branch-based approach to a fork-based model using GitHub, and how it helped us regain control of our white-label app development.


The Problem: Branches per White Label

Our initial structure looked like this:

  • main – shared base app
  • app/whitelabel-a, app/whitelabel-b, app/whitelabel-c, etc. – one branch per white-label variant

Managing white-label apps with long-lived branches quickly became unsustainable. Here’s why:

  • Merge conflicts were constant
    Every update to the shared main branch had to be merged manually into each white-label branch, often leading to repetitive work, diverging file structures, and broken features.

  • Update paralysis set in
    Teams delayed merges out of fear of conflict or regression, causing slow feature rollouts and overdue security patches. Over time, technical debt built up in each branch, making updates even harder.

  • No true isolation between clients
    Code written for one app could unintentionally affect another. Developers and reviewers struggled to manage overlapping changes across branches.

  • CI/CD pipelines became complex and brittle
    Each branch required custom logic for app icons, credentials, and environments. Maintaining version consistency across branches was nearly impossible. Some branches even had sub-branches, leading to a branch explosion and chaotic release workflows.

As the number of clients grew, this structure became more of a burden than a solution.

---
title: Example with Branch Based Solution
---
%%{init: {'theme': 'base'}}%%
gitGraph
   commit id: "Initial commit"
   commit id: "Base architecture"
   branch app/whitelabel-a
   checkout app/whitelabel-a
   commit id: "Whitelabel-a branding"
   branch app/whitelabel-a-feature
   checkout app/whitelabel-a-feature
   commit id: "Whitelabel-a feature"
   checkout app/whitelabel-a
   merge app/whitelabel-a-feature
   checkout main
   branch app/whitelabel-b
   checkout app/whitelabel-b
   commit id: "Whitelabel b branding"
   checkout main
   commit id: "Core feature"
   checkout app/whitelabel-a
   merge main tag: "Manual merge required"
   checkout app/whitelabel-b
   merge main tag: "Manual merge required"
   checkout main
   commit id: "Core feature"
   commit id: "Core feature"
   checkout app/whitelabel-a
   merge main tag: "Manual merge required"
   checkout app/whitelabel-b
   merge main tag: "Manual merge required"
   

The Solution: Forks for Scalability

We transitioned to a fork-based structure using GitHub:

  • transapp-core → upstream repository
  • belice-flutter, red-flutter, transporte-det-flutter, etc. → forks of transapp-core

Each fork contains its own branding, features, configurations, and store credentials—while remaining connected to the core app:

  • Isolation: Every fork can evolve independently
  • Control: Teams choose when to sync with the upstream
  • Clean History: Each app has a clear Git history
  • Custom CI/CD: Pipelines can be tailored per app without worrying about global breakage

Keeping Forks in Sync with Upstream

To ensure forks don’t fall behind, we added a GitHub Action that syncs the fork’s main branch with the upstream transapp-core/main daily:

📄 .github/workflows/upstream-sync.yml

name: ⬆️ Update from Upstream

on:
  schedule:
    - cron: '0 8 * * *' # Every day at 8AM UTC
  workflow_dispatch: # Allow manual trigger

jobs:
  sync-upstream:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repo
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          token: $

      - name: Setup Git
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

      - name: Add upstream
        run: |
          git remote add upstream [email protected]:OrganizationName/organization-repository.git
          git fetch upstream

      - name: Check for changes
        id: check
        run: |
          git checkout main
          git pull origin main
          git fetch upstream main
          BEHIND=$(git rev-list --count main..upstream/main)
          echo "BEHIND=$BEHIND"
          echo "has_changes=$([ "$BEHIND" -gt 0 ] && echo true || echo false)" >> $GITHUB_OUTPUT

      - name: Exit if no changes
        if: steps.check.outputs.has_changes == 'false'
        run: echo "Nothing new to sync from upstream."

      - name: Check for existing PR
        if: steps.check.outputs.has_changes == 'true'
        id: pr
        env:
          GH_TOKEN: $
        run: |
          prs=$(gh pr list --label update --state open --json number)
          echo "PR exists: $prs"
          echo "exists=$([ "$prs" != "[]" ] && echo true || echo false)" >> $GITHUB_OUTPUT

      - name: Exit if PR already exists
        if: steps.pr.outputs.exists == 'true'
        run: echo "Update PR already exists, exiting."

      - name: Create update branch and merge
        if: steps.pr.outputs.exists == 'false'
        id: create-branch
        run: |
          DATE=$(date +%Y-%m-%d)
          BRANCH="chore/update-upstream-$DATE"
          git checkout -b $BRANCH
          git merge upstream/main --no-edit || echo "conflicts=true" >> $GITHUB_OUTPUT
          git push origin $BRANCH
          echo "branch=$BRANCH" >> $GITHUB_OUTPUT

      - name: Create Pull Request
        if: steps.pr.outputs.exists == 'false'
        env:
          GH_TOKEN: $
        run: |
          gh pr create \
            --title "chore: sync with upstream/main" \
            --body "Automated daily sync with upstream." \
            --head $ \
            --base main \
            --label update \
            --assignee Assignee \
            --draft

Simplified GitHub Action: update-upstream.yml

This action does the following:

  1. Checks if main is behind upstream/main
  2. Skips if no changes are found
  3. Verifies if an open PR already exists
  4. If not, creates a new branch, merges upstream, and opens a PR

Why Use a Pull Request Instead of Auto-Merging?

Creating a PR instead of merging directly offers:

  • A safe checkpoint to review incoming changes
  • A place to resolve conflicts manually if needed
  • CI/CD verification before merging to main
  • Better visibility and accountability in the workflow

Before vs After: Branch-Based vs Fork-Based White Label Strategy

ChallengeBranch-Based ApproachFork-Based Solution
Merge ConflictsConstant manual merging into each white-label branchUpstream updates pulled via GitHub Actions
Feature Delivery DelaysUpdates postponed to avoid conflict riskEach fork chooses when to sync safely
Code IsolationClient-specific code easily leaks across branchesForks are completely isolated
CI/CD ComplexityOne big pipeline with too many conditionsOne simple pipeline per fork
Version TrackingNo consistent versioning across branchesEach fork manages its own versioning
Developer FocusFrequent context switching between appsClear separation between apps
Git HistoryMixed and hard to follow commit historyClean, app-specific history in each fork
---
title: Example with Fork Based Solution
---

%%{init: {'theme': 'base'}}%%
gitGraph
commit id: "Initial commit"
   commit id: "chore: add whitelabels colors"
   branch update/upstream
   checkout  update/upstream
   commit id: "chore: update from upstream"
   checkout main
   merge update/upstream tag: "update from upstream"
   checkout main
   commit id: "feat: add new feature"
   commit id: "fix: fix bug"

Final Thoughts

Switching from branches to forks wasn’t just a technical shift—it was a mindset change. It gave us cleaner workflows, stronger app isolation, and better collaboration across teams. Most importantly, it helped us scale our mobile ecosystem without scaling our headaches.

If you’re struggling with white-label app chaos, consider GitHub forks. They might just be the structure you need to grow with confidence.


© 2025. All rights reserved.