ADR 0008: Application contract evolution and compatibility
Compatibility status: pre-freeze legacy/migration-preservation wording below is superseded by ADR 0018. The historical rationale remains; v0.4 carries no legacy or migration obligation.
Status
Roadmap sequencing note (2026-09-21): the architectural decision remains unchanged, but release sequencing has evolved. v0.7 implements the Kubernetes Runtime, v0.8 completes Kubernetes feature parity and production acceptance, and OpenShift/Enterprise specialization follows afterward. Version labels in this ADR are sequencing context, not part of the contract decision.
Accepted for v0.4.0.
Context
BaseHarbor must let one application contract survive the path from local development and single-host Compose to future Kubernetes and OpenShift deployments. The current public baseharbor.yaml schema is Manifest v1 and still contains fields whose present realization is Compose-oriented. Breaking that schema merely to make the implementation look provider-neutral would violate the pre-v1 compatibility promise and would force existing applications to be re-operationalized.
ADR 0005 requires contracts to describe capabilities rather than infrastructure products. ADR 0006 introduces the provider-neutral PortableContract domain view. ADR 0007 keeps runtime provider selection in protected deployment state rather than in the application contract.
Decision
1. Manifest v1 remains a compatibility surface
v0.4 does not replace existing Manifest v1 files. A valid v0.2/v0.3 manifest remains valid unless it relied on behavior that was already invalid or unsafe.
The compatibility boundary is one-way:
repository baseharbor.yaml (Manifest v1 compatibility)
|
v
PortableContract
|
+---------+---------+
| |
v v
capability providers runtime provider
Provider implementations consume normalized application intent. They must not require applications to rewrite provider-specific infrastructure objects into the common contract.
2. Field classification
Manifest v1 fields are classified as follows.
| Manifest concept | Classification | v0.4 treatment |
|---|---|---|
app.name |
portable application identity | retained as stable logical identity |
app.environment |
deployment context | accepted for compatibility; not intrinsic application identity and not a runtime-provider selector |
| named PostgreSQL instances | portable SQL capability intent | translated to database.sql requirements |
| named Redis/Valkey instances | portable key-value capability intent | translated to cache.key-value requirements |
secrets.required names/generation intent |
portable secret requirement | translated without secret values |
services.secrets.enabled |
portable managed-secret intent | translated without selecting OpenBao/Vault |
| workload Compose path/service selection | provider-specific compatibility input | remains supported as repository workload-source input; it is not copied into provider-neutral capability identity |
| generated Compose networks/project names/ports/volumes | provider implementation detail | never part of the portable contract |
| FQDN, TLS source directory, selected TLS mode | deployment/operator state | stored outside baseharbor.yaml |
| runtime provider/profile | deployment/platform state | stored outside baseharbor.yaml |
| delivery provider/mode and reconciliation ownership | deployment/operator state | stored outside baseharbor.yaml; never an Argo/Flux application field |
| OpenBao paths/AppRoles/policies | capability-provider implementation detail | never part of the portable contract |
Future object storage, persistent storage, exposure, TLS intent, health, backup relevance, observability, generic inputs and availability intent are portable contract domains when they express application requirements. Their concrete provider products, topology objects and security policy remain environment/platform concerns.
3. Versioning rules
The top-level contract version is the compatibility gate.
- Additive optional fields may be introduced within a supported contract version when absence has an unambiguous safe meaning.
- New required semantics that cannot be represented safely by an older reader require a new contract version.
- Unknown required contract versions fail closed with a clear unsupported-version error; BaseHarbor never silently ignores required semantics.
- Provider-specific escape hatches, if ever necessary, must be optional and explicitly namespaced. They must not redefine the meaning of portable fields.
- Secret values, provider credentials and operator security policy never become committed contract fields merely to simplify a provider implementation.
- Logical resource names are stable application-facing identities. Changing the provider or topology must not silently rename them.
4. Compatibility adapters are explicit and removable only by release policy
PortableContractFromManifest is the v0.4 compatibility adapter. Provider-neutral code should depend on the portable view rather than grow new dependencies on Manifest v1 product/Compose fields.
A later contract version may gain first-class workload/endpoints, object storage, persistent storage, exposure/TLS intent, health, backup/observability, input and availability declarations. Manifest v1 translation remains supported until a separately documented release policy explicitly removes it.
5. Runtime, capability and delivery providers are independent axes
A deployment may combine, for example:
runtime: openshift
SQL capability: external PostgreSQL
object storage: Ceph RGW
secrets: Vault
Kubernetes or OpenShift therefore must not become shorthand for PostgreSQL, secrets or object-storage provider selection.
Delivery selection is likewise deployment/operator state. Direct runtime mutation and delegated/GitOps reconciliation must be selectable without changing portable application intent. Argo CD, Flux, Git repository layout, Helm values and reconciler-specific resources remain delivery/runtime implementation details.
For one managed resource set there must be exactly one active reconciliation owner.
6. Lifecycle stability is the acceptance test
The intended lifecycle is:
application source + baseharbor.yaml
|
+--> laptop / homelab (Compose)
+--> test / staging (Compose or future Kubernetes)
+--> production (provider selected by deployment)
+--> Kubernetes (future v0.7 provider)
+--> OpenShift / enterprise (future post-v0.8 specialization)
Moving between those stages may change deployment state, provider selection, topology and policy. It must not require rewriting the application's logical capability identities or introducing Kubernetes/OpenShift objects into the common application contract.
Consequences
- Compose remains a first-class implementation without remaining the conceptual API.
- Existing repositories continue to work through the Manifest v1 compatibility adapter.
- New provider-neutral features should extend
PortableContractand its compatibility translation before adding provider-specific realization. - Kubernetes/OpenShift work can be implemented later without changing the v0.4 architectural boundary.
- Breaking contract evolution is explicit, versioned and fail-closed rather than inferred from implementation details.