A proposed design principle for the Civic IPFS addon that treats every point of contact with Hubzilla core as a named, versioned, documented contract — so that core can evolve freely without the addon silently breaking, and any developer can audit the boundary in minutes.
View article
View summary
This post is addressed to developers. It describes not what the Civic IPFS addon does, but how it intends to be built — specifically, how it intends to relate to Hubzilla core.
Before anything else, a correction to the earlier post. I stated that custom Zot6 packet types would be interceptable via Libzot::fetch(). After reading the actual source, that is wrong. Libzot::import() handles 'activity', 'response', and 'sync' types explicitly and returns silently for anything else — no hook fires for unknown types. This means the addon cannot intercept custom packet types through the existing delivery path without either patching core or finding a different architectural seam. That design question is now genuinely open and is noted as such at the end of this post.
What this post is about is a broader principle, independent of that specific question.
The problem with addon maintenance
Most addons are written against the core they can see at the time of writing. Hook names are hardcoded as strings, config keys appear in multiple places, the shape of data arrays passed through hooks is assumed rather than documented. When core evolves — a hook is renamed, a data array gains or loses a key, a config namespace changes — the addon breaks, sometimes silently.
The standard response is to update the addon when it breaks. This works, but it puts the maintenance burden entirely on discovery: something stops working in production, someone notices, someone fixes it.
There is a better model, and it has a name borrowed from mechanical engineering: a differential coupling. A differential allows two shafts to rotate at different speeds while remaining connected — it absorbs the angular difference rather than transmitting it as stress. The equivalent in software is a boundary layer that explicitly models the gap between what the addon expects and what the core actually provides, and makes that gap visible and auditable rather than silent.
For an addon that intends to carry the Hubzilla name in any meaningful sense, this is not optional polish. It is the minimum expression of respect for the core project.
What the boundary layer looks like in practice
Every point where the addon touches the core is declared in a single namespace: CivicIpfs\CoreBoundary. This namespace contains no logic. It is a living specification document embedded in the code itself.
Each class in this namespace corresponds to one type of dependency:
HookContract — every hook name the addon registers against, with the data array shape verified at a specific core version, the source file and line number it was verified from, and a human-readable description of what the hook does.
ConfigContract — every Config::Get() and Config::Set() call the addon makes, including the family name, the key, the expected type, the default, and the core version where that behaviour was verified.
EnvelopeContract — the shape of data arrays passed through each hook, documented field by field with types and whether they are guaranteed or optional.
VersionGate — a class that runs at addon load time, reads STD_VERSION from the running core, compares it against the version each contract was verified against, and logs a structured warning if they differ. Not an error. Not a crash. A warning that says: the core has moved since these contracts were last verified, review them before assuming they are still correct.
Here is what a fragment of HookContract looks like:
namespace CivicIpfs\CoreBoundary;
/**
* Documents every Hubzilla hook this addon depends on.
*
* Each constant is a hook name. Each block of documentation records:
* - the data array shape passed to the callback
* - the source file and line where this hook was verified
* - the core version at time of verification
*
* When core is upgraded, open this file first.
* Update VERIFIED_CORE_VERSION only after re-reading the source.
*/
final class HookContract
{
const VERIFIED_CORE_VERSION = '11.2.1';
const VERIFIED_ZOT_REVISION = '6.0';
/**
* Fires when an activity or response has been received and stored.
* Verified: Zotlabs/Lib/Libzot.php line 1979
*
* Data shape (by reference):
* 'item_id' => int — stored item ID
* 'item' => array — the full item array
* 'sender' => array — sender xchan data
* 'channel' => array — recipient channel data
*/
const HOOK_ACTIVITY_RECEIVED = 'activity_received';
/**
* Fires to allow addons to declare additional supported protocols.
* Verified: Zotlabs/Lib/Libzot.php line 2929
*
* Data shape (by reference):
* 'channel_id' => int — the channel being described
* 'protocols' => array — mutable list, default ['zot6']
*/
const HOOK_CHANNEL_PROTOCOLS = 'channel_protocols';
// ... and so on for every hook the addon touches
}
And ConfigContract:
namespace CivicIpfs\CoreBoundary;
/**
* Documents every Config::Get() and Config::Set() call this addon makes
* against Hubzilla's configuration system.
*
* Verified: Zotlabs/Lib/Config.php, boot.php lines 2441-2447
*/
final class ConfigContract
{
/**
* The directory realm this instance belongs to.
* Read via get_directory_realm() which checks this key first,
* then falls back to DIRECTORY_REALM constant ('RED_GLOBAL').
* Verified: boot.php lines 2441-2447
*/
const REALM_FAMILY = 'system';
const REALM_KEY = 'directory_realm';
const REALM_DEFAULT = 'RED_GLOBAL'; // DIRECTORY_REALM constant, boot.php line 106
/**
* Shared secret appended to directory sync requests as ?t=token.
* Used by Libzotdir::sync_directories().
* Verified: Zotlabs/Lib/Libzotdir.php
*/
const REALM_TOKEN_FAMILY = 'system';
const REALM_TOKEN_KEY = 'realm_token';
// ... and so on
}
Why this matters for the Hubzilla project specifically
An addon that silently assumes the internal behaviour of core is a maintenance liability for the core developers too. Any refactoring that touches a hook name, a data array, or a config namespace has to account for the possibility that some addon somewhere depends on the old form. If that dependency is invisible — buried in a string literal somewhere — the core developer cannot know the blast radius of their change.
An addon built on a documented boundary layer inverts this. The core developer who wants to change a hook name can search for that hook name across all addons in the ecosystem and find every dependency immediately. The dependency is visible by design.
This is what respect for a codebase looks like in code rather than words.
The open question this post cannot answer
As noted above, the mechanism for receiving custom Zot6 packet types without patching core is not yet established. The options as I understand them are:
One: the addon registers against an existing hook that fires early enough in the delivery path to inspect the raw envelope before type-specific processing discards it. This requires identifying such a hook — none of the hooks visible in Libzot::import() fires before the type check.
Two: the custom packet vocabulary is built on top of the existing 'activity' type using a custom verb or object type within the ActivityStreams encoding, rather than as a new top-level Zot envelope type. This would make the packets processable by the existing path and interceptable via 'activity_received'. It changes the packet design substantially.
Three: the addon requests a new hook in core at the right point in Libzot::import() — one that fires for any packet type that the import function does not handle natively. This is a minimal core change, one or two lines, but it requires Association agreement and a proper patch.
Each option has different implications for the boundary layer design. This decision needs input from people who know the Zot6 delivery path well. If you are one of those people, the question is: where is the right seam?
The principle, restated plainly
The Civic IPFS addon will treat every dependency on Hubzilla core as a named, versioned, source-verified contract. Those contracts will live in a single namespace that any developer can read in under ten minutes to understand exactly what the addon assumes about the core and when those assumptions were last verified. The addon will detect at load time when the core has moved beyond those verified versions and will say so clearly in the log.
Core can evolve in any direction. That is correct. The addon's job is to track that evolution explicitly, not to assume it away.
That is the design principle. Whether it is interesting enough to build toward is the question this post is asking.