what i'm doing

Douglife Media

7/18/2026

From ChromaControl to ChromaConnect: Three Changes That Made the Project Coherent

How a branding rename became an architectural cleanup, a monorepo migration, and a better release pipeline.

A project rename can look cosmetic from the outside. Change a few strings, rename a solution, update an installer, and move on.

That was not what happened here.

The application started as ChromaControl and grew around a set of related repositories, package boundaries, runtime assets, generated installers, and public download links. Renaming it to ChromaConnect exposed how many places a product identity actually lives: namespaces, project files, binaries, environment variables, package names, CI workflows, documentation, and release assets.

The most useful outcome was not simply that the application now has a different name. The work made the relationship between the app, its lighting service, the SDK, and the Native OpenRGB runtime much easier to understand and build.

This post focuses on three key changes:

  1. Renaming the product all the way through the code and runtime.
  2. Replacing internal package boundaries with a single monorepo.
  3. Making releases and downloads match the product users actually see.

“Update internal C# namespaces and project file names, anything that says ChromaControl to ChromaConnect”

That was the right starting point: treat the name as a system-wide contract, not a label painted onto the front door.

1. Renaming the Product, Not Just the Window Title

The first change was the broadest: move the application from ChromaControl to ChromaConnect across the public and internal surfaces.

That meant changing more than the main project name. The rename touched:

  • The solution and project filenames.
  • C# namespaces and using directives.
  • Test project and fixture names.
  • Shared project names and generated assembly names.
  • Installer project names and output paths.
  • OpenRGB runtime filenames.
  • Synapse environment variable names.
  • Documentation, release notes, and web pages.
  • CI and packaging references.

A partial rename is worse than no rename in some ways. It creates a product with one name in the UI, another in the executable, a third in its package metadata, and a fourth in its configuration. That confusion eventually becomes a debugging problem.

Namespaces are part of the product surface

The application now uses namespaces such as:

using ChromaConnect.Common.Protos.Lighting;
using ChromaConnect.SDK.OpenRGB;
using ChromaConnect.SDK.Synapse;
using ChromaConnect.Service.Lighting.Extensions;

The important detail is consistency. A namespace is not only an organizational choice inside the source tree. It appears in stack traces, generated documentation, reflection metadata, test output, and sometimes user-facing diagnostics.

The solution and project names were updated as well:

ChromaConnect.sln
src/App/ChromaConnect.App.csproj
src/Service/ChromaConnect.Service.csproj
src/Common/ChromaConnect.Common.csproj
src/Installer/ChromaConnect.Installer.csproj

This also made build commands communicate the current architecture clearly:

dotnet restore ChromaConnect.sln -p:EnableWindowsTargeting=true
dotnet build ChromaConnect.sln -c Release
dotnet test ChromaConnect.sln -c Release --no-build

Runtime names matter too

The Native OpenRGB executable was renamed to ChromaConnect.OpenRGB.exe, and the Synapse key environment variable became:

CHROMACONNECT_KEY_SYNAPSE

That kind of detail is easy to overlook because it is not visible in the main application UI. It still matters. Runtime process names show up in task managers, logs, troubleshooting guides, and security software. Environment variables become part of deployment and support workflows.

The lesson is simple: when changing a product name, search for both source identifiers and operational identifiers. The latter are often the ones that survive longest in production.

2. Replacing Internal Packages with a Monorepo

The second change began as a question about package boundaries.

The app originally consumed internally controlled SDK and Native projects through NuGet packages. That arrangement can be useful when a library has independent consumers and a stable release cadence. It was less useful here because the app, SDK, and Native runtime were changing together.

The practical consequences were familiar:

  • A local build depended on package versions being published first.
  • CI had to find and check out sibling repositories.
  • Project references depended on external directory paths.
  • A change crossing app, SDK, and Native code required coordinated repository updates.
  • The source graph was less visible than the actual runtime graph.

The key architectural question was not “Can these projects be packaged?” It was “Where should the source of truth live?”

“They wont, make a monorepo of these”

That decision removed an unnecessary boundary. The source is now organized inside one repository:

src/
  App/
  AppHost/
  Common/
  Installer/
  Keys/
  Native/
    ChromaConnect.Native.OpenRGB/
      runtimes/
        win-x64/native/
        win-x86/native/
  SDK/
    ChromaConnect.SDK.OpenRGB/
    ChromaConnect.SDK.OpenRGB.Sample/
    ChromaConnect.SDK.Synapse/
    ChromaConnect.SDK.Synapse.Sample/
  Service/
tests/

The app now references the SDK locally:

<ItemGroup>
  <ProjectReference Include="..\SDK\ChromaConnect.SDK.OpenRGB\ChromaConnect.SDK.OpenRGB.csproj" />
  <ProjectReference Include="..\SDK\ChromaConnect.SDK.Synapse\ChromaConnect.SDK.Synapse.csproj" />
</ItemGroup>

The SDK references the Native runtime in the same repository:

<ItemGroup>
  <ProjectReference Include="..\..\Native\ChromaConnect.Native.OpenRGB\ChromaConnect.Native.OpenRGB.csproj" />
</ItemGroup>

The resulting dependency graph is visible in the source tree:

ChromaConnect.Service
  -> ChromaConnect.SDK.OpenRGB
      -> ChromaConnect.Native.OpenRGB
  -> ChromaConnect.SDK.Synapse

ChromaConnect.Common
  -> ChromaConnect.Keys

This is a better fit for the current project because the components are versioned and validated together. A clean checkout now contains everything required to restore and build the solution. CI no longer needs to clone external SDK and Native repositories or pass custom MSBuild root properties.

Removing one package boundary did not mean removing NuGet

The change was deliberately narrow. Third-party dependencies remain ordinary NuGet dependencies. The projects still use packages such as gRPC, Entity Framework Core, OpenTelemetry, and test tooling.

What disappeared was only the internal package boundary for projects controlled by the same product. That distinction matters. A monorepo is not an argument against packages; it is an argument for putting the package boundary where independent versioning and distribution actually begin.

A local replacement for a small API

The old Keys dependency was also small enough to replace locally. Its behavior was straightforward: register a known Synapse key in configuration. The replacement keeps that behavior while removing another internal package dependency:

public static class KeyExtensions
{
    public static IConfigurationBuilder AddChromaConnectKeys(
        this IConfigurationBuilder configuration)
    {
        return configuration.AddInMemoryCollection(new Dictionary<string, string?>
        {
            ["CHROMACONNECT_KEY_SYNAPSE"] =
                "0e881c6f-1194-4c62-813d-a609beafa8b5"
        });
    }
}

“Replace its small API locally if its functionality is simple enough.”

That is a useful rule for internal dependencies: if the package provides a tiny, stable behavior and adds more coordination cost than abstraction value, local ownership can be clearer.

3. Making the Release Path Match the Product

The third change was about the last mile: making the name users see in documentation and download links match the name produced by the build.

A release can build successfully and still fail as a product. The installer can exist in an artifact store under one filename while the website links to another. That is what happened with the v1.0.3 release.

The public link expected this asset:

ChromaRGBConnectSetup-latest.exe

But the release contained the older name:

ChromaControlSetup-1.0.3.exe
ChromaControlSetup-latest.exe

The URL returned a 404 even though an installer had been built and uploaded. The failure was not in compilation. It was a mismatch between the release workflow, the renamed product, and an already-published release.

The workflow now generates the expected names

The release workflow prepares a stable alias after downloading the installer artifact:

- name: Prepare latest installer alias
  shell: bash
  run: |
    installer=$(find release -type f -name 'ChromaRGBConnectSetup-*.exe' | head -n 1)
    if [ -z "$installer" ]; then
      echo "No installer was downloaded." >&2
      exit 1
    fi
    cp "$installer" release/ChromaRGBConnectSetup-latest.exe

The release upload then includes both the versioned installer and the stable alias:

- name: Publish GitHub Release
  uses: softprops/action-gh-release@v2
  with:
    files: release/**/ChromaRGBConnectSetup-*.exe

That gives the public site a predictable URL:

https://github.com/TheDouglife/chroma-rgb-connect/releases/latest/download/ChromaRGBConnectSetup-latest.exe

Repairing an existing release

Changing the workflow does not retroactively change assets already attached to old releases. The v1.0.3 release therefore needed a direct repair: the existing installer was copied under the expected alias and uploaded to the release.

The corrected URL now resolves with a successful response:

HTTP 200 OK
Content-Length: 73,173,111
Content-Disposition: attachment; filename=ChromaRGBConnectSetup-latest.exe

This is a small operational fix, but it points to a larger release principle: validate the URL users receive, not only the command that created the artifact.

What Changed in the End

The three changes reinforce one another.

The rename established a coherent product identity across source, runtime, and public surfaces. The monorepo made the source dependency graph match the product dependency graph. The release repair connected that identity to the thing users actually download.

The final build graph is now self-contained:

dotnet restore ChromaConnect.sln -p:EnableWindowsTargeting=true
dotnet build ChromaConnect.sln -c Release
dotnet test tests\ChromaConnect.App.Tests\ChromaConnect.App.Tests.csproj -c Release --no-restore
dotnet test tests\ChromaConnect.Service.Tests\ChromaConnect.Service.Tests.csproj -c Release --no-restore

The validation result was:

Build:          passed
App tests:      28 passed
Service tests:   8 passed

And the repository now has one source of truth for the app, SDK, Native runtime, installer, and tests.

Closing Thought

The most important architectural change was not the directory move. It was recognizing that repository boundaries, package boundaries, product names, and release names all communicate the same thing: what belongs together.

When those signals disagree, the system feels more complicated than it really is. When they agree, development gets quieter. Builds become easier to reproduce, CI becomes easier to understand, and a download link points to an installer that actually exists.

“They wont, make a monorepo of these”

In the end, that idea did more than reorganize folders. It gave ChromaConnect one coherent place to build, test, release, and explain itself.