Multi-Environment Builds in io.Connect Seed Project

When deploying io.Connect Desktop across multiple environments (e.g., DEV, QA, UAT, PROD), you typically need different configurations, application URLs, and assets per environment. The seed project’s modifications system provides the foundation for all approaches described below.

When you run iocd build, the CLI applies modifications in a specific order:

  1. modifications/base/ — Applied first, in all modes (dev and build).
  2. modifications/build/ — Applied second, only when building an installer.

Later modifications override earlier ones, which means that mode-specific customizations in modifications/build/ are deep-merged on top of the shared base modifications. This means that to produce environment-specific builds, the question becomes: how do you get the right files into modifications/build/ before each build?

Below are three approaches. You can also combine them (e.g., use Approach A for separate builds with unique assets, plus Approach C for runtime switching on developer machines).

Approach A: Environment Folders with a Build Script

How It Works

You create a separate folder for each environment alongside modifications/. Each folder mirrors the same structure that would go inside modifications/build/. A build script copies the correct environment folder into modifications/build/ before running iocd build.

Step 1 — Create the Folder Structure

my-seed-project/
├── modifications/
│   ├── base/                                 # Shared across ALL environments
│   │   └── iocd/
│   │       ├── config/
│   │       │   └── system.json        # Common system settings
│   │       └── apps/
│   │           └── shared-app.json
│   └── build/                                 # ← Left empty in source control
│
└── environments/                        # One folder per environment
    ├── dev/
    │   └── iocd/
    │       ├── config/
    │       │   ├── system.json.merge   # DEV-specific overrides
    │       │   └── logger.json.merge
    │       ├── apps/
    │       │   └── my-app.json.merge
    │       └── assets/
    │           └── images/                      # DEV-specific icons/logos
    ├── qa/
    │   └── iocd/
    │       ├── config/
    │       │   ├── system.json.merge   # QA-specific overrides
    │       │   └── logger.json.merge
    │       ├── apps/
    │       │   └── my-app.json.merge
    │       └── assets/
    │           └── images/
    ├── uat/
    │   └── iocd/...
    └── prod/
        └── iocd/...

Step 2 — Write Environment-Specific Override Files

Each file uses the .json.merge extension so it only overrides the fields that differ. For example:

environments/dev/iocd/config/system.json.merge — sets the DEV gateway port:

{
    "gw": {
        "configuration": {
            "port": 8382
        }
    }
}

environments/qa/iocd/config/system.json.merge — sets the QA gateway port:

{
    "gw": {
        "configuration": {
            "port": 8383
        }
    }
}

environments/dev/iocd/config/apps/my-app.json.merge — points to the DEV URL:

{
    "details": {
        "url": "https://myapp-dev.example.com/"
    }
}

environments/qa/iocd/config/apps/my-app.json.merge — points to the QA URL:

{
    "details": {
        "url": "https://myapp-qa.example.com/"
    }
}

For unique assets (icons, logos), place replacement files directly in the environment folder — e.g., environments/prod/iocd/assets/images/logo.png.

Step 3 — Create a Build Script

Create a build script that copies the right environment into modifications/build/ before building.

:warning: Note that this script requires a POSIX-compatible shell. On Windows, use Git Bash, WSL, or an equivalent shell to run it.

scripts/build-env.sh:

#!/bin/bash
ENV=$1

if [ -z "$ENV" ]; then
    echo "Usage: ./scripts/build-env.sh <dev|qa|uat|prod>"
    exit 1
fi

if [ ! -d "environments/$ENV" ]; then
    echo "Error: Environment '$ENV' not found in environments/"
    exit 1
fi

echo "Building for environment: $ENV"

# 1. Clean previous build modifications.
# Note: This removes all contents of modifications/build/.
# If you have other build-specific modifications, place them in
# modifications/base/ instead, or merge them into the environment folders.
rm -rf modifications/build
mkdir -p modifications/build

# 2. Copy the environment-specific files into modifications/build/.
cp -r environments/$ENV/* modifications/build/

# 3. Build.
npx iocd build --output "dist/$ENV"

echo "Build complete: dist/$ENV"

Step 4 — Add npm Scripts

Add npm scripts to package.json for convenience:

{
    "scripts": {
        "build:dev": "./scripts/build-env.sh dev",
        "build:qa": "./scripts/build-env.sh qa",
        "build:uat": "./scripts/build-env.sh uat",
        "build:prod": "./scripts/build-env.sh prod"
    }
}

What Happens at Build Time

When you run npm run build:qa, the following occurs:

  1. The script clears modifications/build/.
  2. It copies everything from environments/qa/ into modifications/build/.
  3. iocd build runs and applies modifications in order:
    • First: modifications/base/ (shared settings e.g., common system.json).
    • Second: modifications/build/ (now contains QA overrides e.g., system.json.merge with port 8383).
  4. The QA merge files are deep-merged on top of the base, producing a final system.json with the QA-specific port.
  5. The installer is output to dist/qa/.

Approach B: Git Branches per Environment

How It Works

You maintain a main branch with the shared base configuration. For each environment, you create a long-lived branch where modifications/build/ contains that environment’s specific overrides. Building is simply a matter of checking out the right branch and running iocd build.

Step 1 — Set Up the Main Branch

On main, configure modifications/base/ with settings shared by all environments. The modifications/build/ folder is either empty or absent.

main branch:
├── modifications/
│   ├── base/
│   │   └── iocd/
│   │       ├── config/
│   │       │   └── system.json        # Common settings
│   │       └── apps/
│   │           └── shared-app.json
│   └── build/                         # Empty on main

Step 2 — Create Environment Branches

git checkout main
git checkout -b env/dev

Step 3 — Add Environment-Specific Files

Add environment-specific files to modifications/build/ on that branch:

env/dev branch:
├── modifications/
│   ├── base/                          # Inherited from main
│   │   └── iocd/...
│   └── build/                         # DEV-specific overrides
│       └── iocd/
│           ├── config/
│           │   ├── system.json.merge  # port: 8382
│           │   └── logger.json.merge
│           ├── apps/
│           │   └── my-app.json.merge
│           └── assets/
│               └── images/            # DEV icons

The merge files use the same format as in Approach A:

modifications/build/iocd/config/system.json.merge:

{
    "gw": {
        "configuration": {
            "port": 8382
        }
    }
}

modifications/build/iocd/config/apps/my-app.json.merge:

{
    "details": {
        "url": "https://myapp-dev.example.com/"
    }
}

Commit these files and repeat for env/qa, env/uat, and env/prod.

Step 4 — Build from the Appropriate Branch

git checkout env/qa
npx iocd build

No scripts needed — the correct merge files are already in modifications/build/ on that branch.

Keeping Branches Up to Date

When shared configuration changes on main (e.g., a new app is added to modifications/base/, or a component is updated), you need to pull those changes into each environment branch:

git checkout env/qa
git rebase main
# If there are conflicts in modifications/build/, resolve them.
git push --force-with-lease

:warning: Note that you must do this for every environment branch whenever main changes.

Approach C: Single Build with Config Overrides

How It Works

Instead of building separate installers per environment, you build once and bundle all environment-specific config override files into the package. Users launch different environments via shortcuts that pass different command-line arguments to the same executable.

io.Connect Desktop supports a configOverrides command-line protocol:

io-connect-desktop.exe -- config=config/system.json configOverrides config0=config/overrides/system-override-QA.json

Key rules:

  • Overrides are numbered config0 through config9 (up to 10).
  • Higher numbers are merged onto lower ones (e.g., config3 merges onto config2, which merges onto config1, etc.).
  • The base config= file is loaded first, then overrides are applied on top.

This means you can have a single installation with multiple shortcuts - one per environment - each pointing to the same executable but with different override arguments.

Step 1 — Create Environment-Specific Override Files

Place override JSON files in a config overrides directory via modifications. Each file contains only the settings that differ from the base system.json:

modifications/
├── base/
│   └── iocd/
│       ├── config/
│       │   └── system.json            # Base config (shared)
│       └── apps/
│           └── my-app.json            # Base app config
│
└── build/
    └── iocd/
        └── config/
            └── overrides/
                ├── system-override-DEV.json
                ├── system-override-QA.json
                ├── system-override-UAT.json
                └── system-override-PROD.json

config/overrides/system-override-DEV.json:

{
    "gw": {
        "configuration": {
            "port": 8382
        }
    }
}

config/overrides/system-override-QA.json:

{
    "gw": {
        "configuration": {
            "port": 8383
        }
    }
}

You can also layer overrides. For example, if DEV and QA share some settings but differ in others, use multiple override files:

-- config=config/system.json configOverrides config0=config/overrides/shared-non-prod.json config1=config/overrides/system-override-DEV.json

Here config1 (DEV-specific) merges onto config0 (shared non-prod settings), which merges onto the base system.json.

Step 2 — Handle Application-Specific Overrides

For app configurations that differ per environment (e.g., different URLs), you have two options:

  • Bundle all environment app configs and reference them via configOverrides (if the application definition supports override loading).
  • Include environment-specific application definition files in the overrides, and use separate system.json override files per environment that point to different app definitions.

Step 3 — Create Environment-Specific Shortcuts

After the build, create Windows shortcuts (.lnk) that launch the executable with different arguments:

PROD shortcut (default — no overrides, or explicit PROD override):

"C:\...\io-connect-desktop.exe" -- config=config/system.json configOverrides config0=config/overrides/system-override-PROD.json

QA shortcut:

"C:\...\io-connect-desktop.exe" -- config=config/system.json configOverrides config0=config/overrides/system-override-QA.json

DEV shortcut:

"C:\...\io-connect-desktop.exe" -- config=config/system.json configOverrides config0=config/overrides/system-override-DEV.json

These shortcuts can be:

  • Created manually by the end user or IT team after installation.
  • Distributed alongside the installer as .lnk files.
  • Generated by a post-install script.

What Happens at Runtime

When a user clicks the “QA” shortcut:

  1. The exe launches and loads the base config/system.json.
  2. It sees configOverrides and loads config0=config/overrides/system-override-QA.json.
  3. The QA override is deep-merged onto the base system config.
  4. The platform starts with the QA-specific port, URLs, and settings.

Limitations

:warning: Note the following limitations of this approach:

  • Assets: Binary assets (icons, logos) can’t differ per environment because the same build is shared. If environments need different visual branding, combine this with Approach A or B for the asset differences.
  • Shortcut distribution: The seed project’s installer generates a single default shortcut. Additional shortcuts must be created separately (manually, via script, or via Group Policy / MDM deployment).
  • App definitions: Application-level configs (like app URLs) may need to follow the same override pattern, or you may need environment-specific app definition files bundled and selected via overrides.

Comparison

Approach A (Script) Approach B (Branches) Approach C (Config Overrides)
Build count One per environment One per environment Single build for all environments
Visibility All env configs visible in one place — easy to compare and audit Configs spread across branches — must switch branches to inspect All override files visible in one place
Maintenance Change base config once, all envs pick it up automatically Must rebase every environment branch after base changes Base changes auto-apply
Merge conflicts None — environment folders don’t overlap Possible when rebasing, especially if base and env modify the same files None
Asset differences Fully supported Fully supported Not supported (same build)
Deployment One installer per env One installer per env One installer + shortcuts per env
Runtime flexibility None (baked at build time) None (baked at build time) Users can switch envs without reinstalling
Scalability Adding an environment = adding a folder Adding an environment = creating and maintaining another long-lived branch Adding an environment = adding an override file + shortcut
Complexity Low (script + folders) Medium (Git rebasing) Low-Medium (override files + shortcut distribution)

Choosing an approach:

  • Approach A — Best when environments are structurally similar (same apps, same configs, just different values), your base config changes frequently, or you want to easily compare settings across environments.
  • Approach B — Best when environments diverge significantly (different apps or components per environment), your team is comfortable with Git rebasing, or you need an independent audit trail per environment.
  • Approach C — Best when environments differ only in configuration (not visual assets), you want a single installer, or you need users/IT to be able to switch environments without reinstalling. This is also useful when the same machine needs access to multiple environments.