← Field Notes
Homelab Homelab Operations
Published Last updated 8 min read

When Compatibility Became Part of the Architecture

What maintaining old and current versions of the same application taught me about treating compatibility as a boundary instead of a temporary inconvenience.

In this article
  1. One application had become two environments
  2. Preserve the known-good state before making the clever change
  3. Names are operational controls
  4. Isolation does not have to mean virtualization
  5. The two environments I was actually protecting
  6. A successful launch is not enough
  7. Automation would have been premature
  8. Compatibility debt is real debt
  9. The larger lesson
  10. Publication and privacy note

I recently wanted two things from the same application that did not agree with each other.

One workflow needed an older version because a collection of mods and multiplayer tooling depended on it. I also wanted the current version available so I was not permanently freezing the entire installation around one compatibility requirement.

My first instinct was to treat that as a temporary problem. Get the old version working, get the current version working, and eventually everything would converge again.

What actually helped was accepting that compatibility had become part of the architecture.

That changed the job from “make both versions work somehow” to “give each version a boundary I can understand and recover.”

One application had become two environments

The executable was only part of the state.

Each version also interacted with launchers, user data, mods, configuration, caches, and assumptions made by other software. Once those assumptions diverged, swapping one directory back and forth stopped being a clean solution.

That is where compatibility work becomes dangerous. The visible application may look self-contained while the real state is scattered across several locations.

If I treat all of that state as interchangeable, I can get a successful launch while quietly damaging the environment I intend to return to later.

So I started thinking about the installations the same way I think about services in the homelab: define the boundary first.

Preserve the known-good state before making the clever change

The useful move was not another compatibility tweak. It was making sure I still had a known-good copy of the pieces that mattered.

Before changing launch behavior, I wanted to know:

  • which application version I was actually starting;
  • which launcher belonged to that version;
  • where that version expected its user data;
  • which mods were intended for it;
  • what I could replace from a clean source;
  • and what state existed only on my machine.

That inventory sounds basic, but it stopped the troubleshooting session from becoming archaeology.

This is the same reason I prefer recovery planning before deployment. A rollback is only real when I know what has to survive.

Related: A Project Is Finished When It Can Survive You Walking Away and The Application Host Is Replaceable on Purpose.

Names are operational controls

At one point, the difference between “old” and “current” existed mostly in my head.

That is fine for about five minutes.

After that, ambiguous names become risk. A launcher pointed at the wrong tree, a copied directory lands in the wrong place, or I forget which environment I modified yesterday.

Clear naming did more for the setup than another layer of automation would have.

I wanted the filesystem and launch path to answer a simple question without requiring memory: which environment am I touching?

That is a small example of a broader rule I keep relearning. Good operations are full of boring identifiers. A useful name reduces the number of assumptions I have to carry while troubleshooting.

Isolation does not have to mean virtualization

My infrastructure instincts initially pushed toward a more elaborate answer.

Separate containers. Separate virtual machines. Scripts that reshuffle state. Maybe an entirely different host.

Those are valid isolation tools when the workload calls for them. They were not automatically the right answer here.

The actual requirement was smaller: keep two compatibility states from accidentally overwriting each other.

That could be achieved with separate application trees, explicit launch paths, and deliberate handling of user data. The boundary needed to be understandable more than it needed to be sophisticated.

That mirrors why Docker Became My Default, Not My Rule. Isolation is valuable, but the mechanism should fit the failure I am trying to contain.

The two environments I was actually protecting

The application in this case was BeamNG.drive on Fedora through Steam/Proton, with BeamMP and the RLS Career Overhaul mod set creating the compatibility requirement.

The older target was BeamNG.drive 0.39. My surviving notes do not record the exact newer BeamNG or BeamMP launcher version, so I am not filling those numbers in from memory after the fact. The mod files I had recorded included RLS Career Overhaul 2.7.1, RLS Career Collection 5.7, and the RLS Tanker Hotfix 0.1.

I kept the boundary deliberately simple rather than virtualizing the whole game:

BoundaryOlder compatibility environmentCurrent environment
Game tree0.39 downloaded through Steam tooling and placed where the normal Steam launch path expected itNewer game tree preserved separately rather than overwritten
LauncherSeparate BeamMP launcher directory for the old environmentSeparate launcher directory reserved for the current environment
User dataBeamNG user data was deliberately pointed into the Proton compatibility path with a symlink so the launcher/game agreed on the state locationSame state boundary had to be treated carefully because shared user data can couple versions
ModsRLS Career Overhaul compatibility set belonged to the older workflowNot assumed compatible merely because the newer game launched
Switching mechanismExplicitly named directories and manual launch targetsExplicitly named directories and manual launch targets

I had temporarily renamed the newer game tree rather than destroying it, then copied the older build into the canonical Steam location. That made the rollback path understandable without pretending the two installs were completely independent.

The result matrix is just as important as the layout:

TestResult
Launch the intended 0.39 environmentPassed — the older environment launched
Load the intended modded/career contentPassed in the recorded session
Join through BeamMPPassed — multiplayer connection reached the guest flow
Preserve the same guest identity after leaving/reloadingFailed / unresolved — the session returned as a new guest and the server required guest mode
Return to the current-version environment and prove it end to endNot recorded as a completed retest

The guest-identity problem is important because it remained a real unresolved boundary. I did not find a supported way in that session to force the server’s required guest mode to retain the same guest identity across reloads. A working launch and connection therefore did not equal full workflow persistence.

A successful launch is not enough

The old environment eventually launching did not prove the job was finished.

I still needed to know whether the workflow I cared about actually behaved correctly. In this case that meant checking the modded experience, multiplayer behavior, and persistence rather than stopping at the main menu.

That distinction exposed another issue: reconnecting could produce a fresh guest identity instead of the continuity I expected.

That was useful evidence. It meant the compatibility problem was no longer just “can the old application run?” There was now an identity and persistence boundary to understand too.

I did not solve that by pretending the launch was good enough. I separated the questions:

  1. Can the intended version start?
  2. Does it load the intended local content?
  3. Can it connect through the intended multiplayer path?
  4. Does state persist the way the workflow expects?
  5. Can I return to the current version without repairing the installation again?

That is a much better definition of success than “it opened.”

It is also the same thinking behind The Test Matrix I Use Before I Trust a Change.

Automation would have been premature

I was tempted to script the switching process.

The appeal is obvious. One command for old, one command for current, and no manual file handling.

But automation is a terrible way to hide a boundary I do not fully understand yet.

If I cannot explain which state belongs to which version, a script only makes the mistake faster and harder to see.

I would rather perform a slightly boring manual process that I understand than maintain an elegant switcher whose assumptions I have forgotten.

Once the boundary is stable and repetitive, automation can earn its place. Before that, explicit steps are documentation.

Compatibility debt is real debt

Keeping an older version available creates an obligation.

I now have another environment to identify, another set of assumptions to remember, and another place where a future update can break expectations. That cost may be completely worthwhile, but it is not free.

This changed how I think about compatibility decisions generally.

“Do not update it” is not a complete maintenance strategy.

A better strategy records:

  • why the older version exists;
  • what depends on it;
  • what state must remain separate;
  • how to prove it still works;
  • and what condition would let me retire it.

Without that information, yesterday’s workaround becomes tomorrow’s mystery dependency.

That is the same line I draw between Maintenance and Endless Tinkering. Keeping an old environment because it serves a defined purpose is maintenance. Keeping it forever because I am afraid to find out what depends on it is something else.

The larger lesson

Version conflicts are easy to frame as annoyances caused by software moving at different speeds.

Sometimes they are.

But once I deliberately keep two states alive, the conflict becomes an operational design problem. I need boundaries, names, validation, recovery, and eventually a retirement condition.

The lesson was not that I should freeze software or avoid updates. It was that compatibility is state, and state deserves to be managed intentionally.

The simplest working architecture ended up being the one where I could point at each environment and explain why it existed, what belonged to it, and how I would know if it stopped working.

That is not glamorous. It is order from complexity.

Publication and privacy note

This Field Note is based on a real BeamNG.drive/BeamMP compatibility troubleshooting session. Public product and mod versions are included where they were recorded; local usernames, exact home-directory paths, account and guest identifiers, server details, private endpoints, credentials, tokens, private addresses, and internal infrastructure identifiers remain omitted.

The point is the boundary and troubleshooting method, not a map of the system that produced it.

JO

Written by

Jessie Owens

I run Eldritch IT and write about the systems, repairs, infrastructure decisions, and business lessons behind the work.