Software

Shared Plugin Architecture: Thirteen Faces, One Product

How a growing plugin family became a deliberately designed suite

homeseerarchitectureshared codedrifttoolingsoftwaresoftware-automation

Thirteen Faces, One Product

Somewhere along the way, I realized I had been thinking about these plugins the wrong way.

Thirteen plugins is not really thirteen independent projects.

It is one product with thirteen faces.

They do very different things.

One talks to televisions.

Another talks to beds.

Another talks to sprinklers.

Another spends an unreasonable amount of time calculating where Jupiter is.

But from the user’s side, they should feel as though they came from the same place.

The navigation should behave the same way.

Health should be reported the same way.

Logs should work the same way.

Pages should refresh the same way.

Controls should look familiar.

If someone understands one plugin, the next one should already feel partly learned.

The moment one has a different header, another uses a different health indicator, and a third invents its own way to display logs because I happened to write it on another Tuesday, the collection stops feeling designed.

It starts feeling accumulated.

And “accumulated” is not what I want to ship.

What is shared

There is a common layer across the plugin family.

Page styling.

Navigation and sub-navigation.

The health/status pill.

Logging controls.

Live-refresh behavior.

Packaging scripts.

And a small shared library containing concepts nearly every plugin needs, such as:

Health checks.

Dependency declarations.

Log levels.

Common support types.

None of those things is the interesting feature printed on a plugin’s store page.

They are the connective tissue.

And connective tissue is generally only noticed when something is wrong with it.

That is fine.

Consistency should mostly disappear into the experience.

But every plugin must still stand alone

There is one rule more important than all the shared code:

Every plugin must function completely on its own.

Some are natural companions.

Plex, Jellyfin, and Emby obviously belong to the same family.

Roku and the larger AV work complement each other.

Some plugins can discover that a sibling is installed and use information it exposes.

That is useful cooperation.

It must never become required assembly.

Installing the Roku plugin should not secretly require the AV plugin.

Installing Jellyfin should not require Plex.

A user who wants one integration should be able to install one integration.

A suite whose pieces work only when assembled is not really a suite.

It is one giant plugin that arrived in several boxes.

The goal is:

shared design, optional cooperation, independent operation.

The first distribution system was copy and paste

The shared files originally had a highly sophisticated distribution mechanism.

I copied them into each repository.

By hand.

This worked beautifully for approximately one day.

Then they started drifting.

One plugin got an improvement.

Another got a slightly different version.

A third missed both.

Something changed while debugging one project and never made it back into the others.

Nothing failed spectacularly.

That would have been easier.

The differences accumulated quietly.

Which is exactly how this problem usually works.

The packaging mistake that made it visible

The first especially obvious example involved the packaging scripts.

Three plugins were packaged using an older copy of the script.

That version produced an icon path HomeSeer could not resolve correctly.

The plugins worked.

The packages built.

They installed.

Nothing in the actual feature code was broken.

Then the icons vanished from the updater.

By the time I noticed, those releases had already been published.

The proper fix therefore meant another release rather than quietly pretending the published versions had never happened.

Three plugins had acquired a visible defect because three repositories contained slightly different copies of something that was supposed to be the same.

That was enough evidence.

One canonical copy

The shared material now has one canonical source.

Changes happen there.

Then an explicit synchronization step pushes those files into each plugin repository.

The important word is:

explicit.

This is still copying.

It is not linking.

So drift remains technically possible.

Someone can change a shared file inside one repository and forget to synchronize it.

But now synchronization is an actual action.

It is something that happens.

It has a direction.

There is a known source.

The old system depended on everyone silently assuming:

Surely this repository has the latest copy.

The new system can answer:

When was it last synchronized, and from what?

That is a much healthier question.

Visible imperfection is better than invisible assumption

I could make the shared files hard-linked, generated during every build, pulled from another repository, or turned into a package.

There are several ways to make drift impossible.

For this collection, I deliberately chose something simpler.

The important improvement was not mathematical elimination of all possible drift.

It was making drift visible and intentional.

There is a canonical version.

There is a synchronization step.

If a repository differs, that difference can now be recognized as a difference instead of accidentally becoming its own local truth.

That has proven remarkably valuable.

It also clarifies what really belongs in the shared layer

Once something has to earn its place in a canonical shared source, another useful question appears:

Is this genuinely common?

It is easy to copy a helper into three repositories.

That does not necessarily mean it belongs in the platform layer.

Maybe those three plugins merely happen to need similar behavior today.

Maybe one of them will need to diverge tomorrow.

The shared layer should contain concepts that are structurally common to the suite.

Not every snippet of code that happens to look alike.

That boundary has become clearer as the family has grown.

Live refresh without swatting the user’s hand away

One of those genuinely shared pieces is live refresh.

The easy approach is:

Fetch the current state.

Redraw the relevant area.

Repeat.

That works until somebody is actually interacting with the page.

They click into a control.

A refresh fires.

The DOM gets replaced.

Focus disappears.

A selection resets.

A button jumps.

The “live” interface starts periodically brushing the user’s hand off the controls.

That is not especially friendly.

So the shared refresh logic uses keyed DOM reconciliation.

It updates the parts whose state actually changed rather than recreating the whole section.

A health indicator can update.

A device can change state.

A value can refresh.

The control the user is currently interacting with remains where it was.

This is exactly the kind of implementation detail I hope nobody ever notices.

If someone notices it, I probably got it wrong.

Then I proved the same lesson in a completely different project

The slightly embarrassing part is that I managed to demonstrate the same copy-and-paste problem again this month in an unrelated codebase.

Two websites contained separate copies of the same authentication stack.

A bug was reported.

I fixed it in both.

At least I thought I did.

One of the changes was applied using an automated replacement.

But earlier work had already reshaped the code in one copy.

So the search pattern no longer matched.

The replacement changed nothing.

And helpfully reported success.

The other copy received the actual fix.

So far, irritating but survivable.

Then came a consolidation.

And naturally, the copy containing the real fix was the one that got deleted.

The surviving version was the one where the automated edit had silently done nothing.

So the bug shipped.

I found it only because I happened to test that exact screen afterward.

There is a certain elegance to demonstrating the same architectural lesson twice, in two unrelated systems, during roughly the same period.

Apparently once was not sufficiently educational.

Copy and paste has a hidden invoice

The first copy is nearly free.

That is why it is tempting.

Copy the class.

Copy the helper.

Copy the script.

Done.

The hidden cost appears later.

Now there are two places to patch.

Two places to test.

Two places to remember.

Two places that can evolve independently while still looking nearly identical.

And the dangerous part is that nobody had to consciously decide:

I would like to maintain this twice from now on.

The decision was disguised as:

I just need this over here too.

That is why I have stopped thinking of copy and paste as merely a shortcut.

Copy-and-paste is a decision to maintain something twice.

It just rarely introduces itself that way.

Sharing code is not the same as coupling products

There is a useful tension here.

I want these plugins to share as much good infrastructure as possible.

I do not want them to become dependent on one another.

Those are different goals.

Shared source can improve consistency.

Shared concepts can improve usability.

Shared libraries can eliminate duplicated bugs.

But the deployed plugins should remain independent products.

That is why the common layer is about implementation and design rather than runtime dependency.

The user should not have to install the suite to use the piece they want.

The suite should simply reward them with familiarity when they use more than one.

The larger lesson

Thirteen plugins create enough surface area that inconsistency stops being cosmetic.

It becomes architecture.

If every plugin independently invents:

Navigation.

Status.

Logs.

Packaging.

Refresh behavior.

Health reporting.

Dependency handling.

Then every plugin also independently gets to develop bugs in those things.

And because they were written at different times, they will develop different bugs.

Solving the common problem once does more than make the pages look alike.

It reduces the number of places where that class of problem can exist.

That is the part I care about.

What I like about this one

Nobody buys a plugin because its packaging scripts are consistent with twelve others.

Nobody gets excited because all thirteen use the same health-check abstraction.

Nobody posts a screenshot saying:

Look at this excellent keyed DOM reconciliation.

Good.

Those things are not the product feature.

They are the reason the product family feels deliberate rather than improvised.

Thirteen plugins can absolutely be thirteen independent programs.

Or they can behave like one family.

The difference is a lot of work nobody is supposed to notice.

Which, increasingly, seems to be where some of the most worthwhile engineering lives.

And if I ever start wondering whether maintaining another copy of something is really such a big deal, apparently I now have several projects available to remind me.