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:
modifications/base/— Applied first, in all modes (dev and build).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.
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:
- The script clears
modifications/build/. - It copies everything from
environments/qa/intomodifications/build/. iocd buildruns and applies modifications in order:- First:
modifications/base/(shared settings e.g., commonsystem.json). - Second:
modifications/build/(now contains QA overrides e.g.,system.json.mergewith port 8383).
- First:
- The QA merge files are deep-merged on top of the base, producing a final
system.jsonwith the QA-specific port. - 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
Note that you must do this for every environment branch whenever
mainchanges.
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
config0throughconfig9(up to 10). - Higher numbers are merged onto lower ones (e.g.,
config3merges ontoconfig2, which merges ontoconfig1, 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.jsonoverride 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
.lnkfiles. - Generated by a post-install script.
What Happens at Runtime
When a user clicks the “QA” shortcut:
- The exe launches and loads the base
config/system.json. - It sees
configOverridesand loadsconfig0=config/overrides/system-override-QA.json. - The QA override is deep-merged onto the base system config.
- The platform starts with the QA-specific port, URLs, and settings.
Limitations
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.