PRODUCT -- Platform per-client deploy: tag/release version pinning (ID-113.2)
PRODUCT — Platform per-client deploy: tag/release version pinning (ID-113.2)
Section titled “PRODUCT — Platform per-client deploy: tag/release version pinning (ID-113.2)”Task: ID-113 — Platform per-client deploy: tag/release version pinning design + standup.
Subtask: {113.2} PRODUCT.
Author date: 15/06/2026.
Status: SPEC — behaviour formalisation. The pin mechanism is DE-RISKED and the
ratified envelope NAMED in {113.1} RESEARCH.md (semver vX.Y.Z; client-agnostic
version-named release branches; platform owner cuts at known-good main; clients upgrade
deliberately; on-prem image tagged with the same semver). This PRODUCT pins the behaviour
of the release/version-pinning layer — it does NOT re-open the topology (ID-95) or
re-litigate the ratified inputs. It defines testable invariants an operator and a Checker
can hold the cut/upgrade flow to; the exact Vercel mechanism (release-branch tracking
vs deploy-from-tag) is a {113.3} TECH decision and is carried as an Open decision here,
NOT resolved.
Fresh-Planner artefact (Q-PLANNER-2): authored by a different instance from the
{113.1}RESEARCH author, read RESEARCH fresh. Every invariant is numberedPI-Nwith a one-line Verify means so a Checker can gate it against acceptance criteria. UK English throughout (DD/MM/YYYY, “colour”, “organisation”, “behaviour”). Cross-references are by path; this document does NOT re-dump RESEARCH content — it cites it. Builds directly on the ratified ID-95 per-client deploy +tenant_configmodel (specs/id-95-per-client-topology/).
Summary
Section titled “Summary”Knowledge Hub ships continuously to main, but no client ever runs main. The platform
owner cuts an immutable semver release (vMAJOR.MINOR.PATCH) at a known-good main, and
each client deployment is PINNED to a specific release rather than tracking the moving tip.
Upgrading a client is a deliberate, audited repoint to a newer release — never an automatic
follow-on of a main merge. The same semver pins both surfaces a client runs: the Vercel
application build AND the on-prem/Coolify ingestion-pipeline image (the bl-301 tie-in). Pin
refs in the public repo are client-agnostic (version-named release branches; no client
identity, per PI-7); the mapping of which client runs which version is the single source of
truth held privately in the docs-site ops crib. Branding and per-client config continue to
come from the client’s OWN Supabase (tenant_config) at build time regardless of version
(no regression of the ID-95 model). This PRODUCT pins: the cut invariant, the pin invariant,
the client-agnostic-ref invariant, the client->version-map source-of-truth invariant, the
deploy-gate-integrity invariant, the on-prem version-parity invariant, the
branding-from-tenant_config invariant, and the deliberate-upgrade-audit invariant — the set
that lets the platform owner ship to main continuously while every client upgrades on its
own schedule, auditably.
Problem
Section titled “Problem”Today there is no release layer. The public repo has zero platform semver vX.Y.Z tags and
zero release/* branches (git tag --list shows only the vendored v0.2.0-task-view;
git branch -r shows no release/* — §3.5 RESEARCH), so a Vercel project tracking a
branch tracks the moving tip of whatever branch it points at. ID-95 settled the per-client
topology (own Vercel project + own Supabase + branding from the client’s tenant_config)
but explicitly left the version-pinning layer undesigned ({95} is topology, not
release cadence). Without it, (a) a client cannot be held at a known-good version while the
platform ships forward, (b) “which client is on which version?” has no authoritative
answer, (c) a release/rename/repoint can silently un-gate production via the required-check
name coupling (§3.3 RESEARCH — the ci.yml ci-summary deploy-gate hazard), and (d) the
ingestion pipeline pins a rolling main sha rather than a deliberate version. This spec
formalises the behaviour an implementer and a Checker can hold the cut/upgrade flow to.
Context (code-intelligence orientation + grounding sources)
Section titled “Context (code-intelligence orientation + grounding sources)”Code-intel orientation ran before authoring (gitnexus, index: knowledge-hub; outputs cited verbatim, not paraphrased):
gitnexus_query({query: 'deploy release branding tenant_config version', repo: 'knowledge-hub'})->"processes": [](versioning/release is a config-and-CI concern, not an indexed runtime call graph), with these build-time branding definitions surfaced:Function:scripts/fetch-client-branding.ts:runClientBrandingFetch(lines 93-204),Method:scripts/fetch-client-branding.ts:writeBrandingJson#2(lines 213-217),Function:scripts/generate-client-branding-map.ts:renderClientBrandingMap(lines 45-86),Function:scripts/generate-client-branding-map.ts:main(lines 88-96).gitnexus_context({name: 'runClientBrandingFetch', repo: 'knowledge-hub'})-> incomingcallsfromFunction:scripts/fetch-client-branding.ts:main; outgoingaccessestoConst:scripts/fetch-client-branding.ts:log,callstoClass:scripts/fetch-client-branding.ts:FetchClientBrandingError;"processes": []. A leaf build-time script (no runtime callers), invoked only via its ownmain.gitnexus_context({name: 'renderClientBrandingMap', repo: 'knowledge-hub'})-> incomingcallsfromFunction:scripts/generate-client-branding-map.ts:main; no outgoing edges;"processes": []. Also a leaf build-time codegen step.
ccc search fallback was not required: gitnexus returned the relevant build-time symbols
directly (no greenfield disclaimer needed for the branding surface). The release/versioning
surface itself is genuinely greenfield in the repo — confirmed by direct inspection rather
than a graph query: git tag --list returns no platform vX.Y.Z tags; git branch -r
returns no release/* branches; vercel.json carries buildCommand/installCommand/
framework only and no git/branch/production-branch config (the pin is a Vercel project
setting, not a repo file); ci.yml:1262,1292 define the ci-summary deploy-gate job;
onprem-deploy.yml:108 sets TAG: sha-${{ github.sha }} with a documented
“re-point Coolify to a prior sha- tag; no rebuild” rollback (line 106).
Interpretation for this Task. The branding/config pipeline is entirely build-time
(fetch from the client’s Supabase tenant_config -> write JSON -> codegen a static map ->
baked into the bundle), not request-time. Therefore the version of the platform code and
the client config baked into it are both fixed at build time, so pinning the build’s git
ref is sufficient to pin the entire client-facing behaviour — no runtime version
negotiation exists or is needed. This grounds PI-1, PI-7, and PI-8.
Empirical-verification note (Q-EX2 scope). This PRODUCT cites no external-library API
symbols; the verification surface is git ref / Vercel project-setting / Coolify image-tag
configuration and operator runbook behaviour, not third-party library import-and-call. No
pre-ratification import-and-call block is therefore required at PRODUCT altitude. (The
single mechanism-dependent empirical claim — whether Vercel’s Production Branch can pin an
arbitrary release/* per project and whether one shared ci-summary check can gate
multiple per-client projects — is OQ-113-1, a live-Vercel verification the Orchestrator
performs before {113.3} TECH.)
Grounding sources read in full for this spec (cite for the Checker):
{113.1}RESEARCH.md (ratified envelope §1; Vercel pin-mechanism options §4 with Option A recommended; cut/upgrade runbook shape §5; on-prem tie-in §6; client->version map location/format §7; OQ-113-1..6 §10).- ID-95
PRODUCT.md(PI-7 no-client-literal definition; PI-1/PI-2 Vercel-project-as-unit; PI-10/PI-11 branding-from-tenant_configbuild-time fetch; PI-4 Vercel rename; the deploy-gate/required-check ordering gate echoed from{95.10}). - ID-95
TECH.md(T-B build-time prebuild branding fetch mechanics, PI-11/PI-2/AC-E3). runbooks/client-app-deploy.md— the existing per-client deploy ops companion this spec’s crib must co-locate with or cross-reference (OQ-113-5 home).
Behaviour
Section titled “Behaviour”Invariants are grouped by surface. Each PI-N carries a one-line Verify means. “Public
repo HEAD” means the canonical (going-public) branch of the application repo; “client
deploy” means a knowledge-hub-{client} Vercel project’s production build; “release” means
an immutable semver vMAJOR.MINOR.PATCH cut at a known-good main; “pin ref” means the
client-agnostic git ref (the version-named release branch and/or its semver tag) a client’s
deploy is pinned to.
A. Release cut — semver from known-good main; tag is the immutable source of truth
Section titled “A. Release cut — semver from known-good main; tag is the immutable source of truth”-
PI-1 (a release is an immutable semver
vMAJOR.MINOR.PATCHcut at a known-goodmain). A release is created only at amaincommit whose CI deploy-gate (ci-summary, §3.3 RESEARCH) is passing, and is namedvMAJOR.MINOR.PATCHper semver: MAJOR = a breaking client-facing or non-rollback-safe migration change; MINOR = additive/back-compatible; PATCH = fix-only. The semver tag at the release commit is the SINGLE SOURCE OF TRUTH for what “that version” is; it is immutable and never re-pointed once cut. Verify: everyvX.Y.Ztag resolves to amainancestor commit; the cut runbook records the greenci-summaryprecondition; a created release tag is never moved to a different commit (a fresh defect ships as a new PATCH, never a retag). -
PI-2 (releases are cut from
mainonly — no release originates off a feature/working branch). A release commit is always an ancestor onmain’s first-parent history. The platform ships tomaincontinuously; cutting a release is a deliberate, separate act that selects a known-goodmaincommit — it is never produced from an unmerged feature/worker branch and never fromstaging(which is deploy-only). Verify:git merge-base --is-ancestor <release-commit> mainsucceeds for every release; novX.Y.Ztag points at a commit absent frommain.
B. Pin — per-client deploys are pinned to a release, never main-tracking
Section titled “B. Pin — per-client deploys are pinned to a release, never main-tracking”-
PI-3 (a client deploy is pinned to a specific release, never to
main). Each client’s Vercel production build resolves from a pinned release ref (the client-agnostic version-named release ref of a singlevX.Y.Z), NOT frommainand NOT from the moving tip of any shared branch. Merging tomainMUST NOT change what a pinned client runs. (The exact Vercel pin mechanism — Production-Branch-tracks-release/vX.Y.Zvs deploy-from-tag — is OQ-113-1, decided in TECH; PI-3 constrains the behaviour whichever mechanism wins: the pin resolves to exactly one immutable release.) Verify: a no-op merge tomaintriggers no client production redeploy and changes no client’s served version; each client project’s pin resolves to exactly onevX.Y.Z; no client project’s production deploy resolves frommainor a rolling branch tip. -
PI-4 (upgrade is a deliberate repoint to a newer release; never automatic). Moving a client from version A to version B is an explicit operator action that repoints the client’s pin from release A to release B and redeploys. There is NO mechanism by which a
mainmerge, a new release cut, or any background automation advances a client’s version without that deliberate repoint. A client stays on its pinned version indefinitely until repointed. Verify: cutting a new releasevX.Y.(Z+1)leaves every existing client still serving its previously pinned version (no auto-advance); the only path that changes a client’s served version is a recorded repoint step in the upgrade runbook; automated client upgrades appear nowhere (Non-goal).
C. Client-agnostic refs — no client identity in the public repo (extends ID-95 PI-7)
Section titled “C. Client-agnostic refs — no client identity in the public repo (extends ID-95 PI-7)”-
PI-5 (every pin ref in the public repo is client-agnostic / version-named). Release branches and tags in the public repo are named by VERSION only (
release/vX.Y.Z,vX.Y.Z) — never by client identity. No ref name, no commit, and no committed file in the public repo encodes which client runs which version. This is the ID-95 PI-7 no-client-literal discipline extended to the release layer. Verify:git for-each-refover the public repo’s branches and tags greps clean of any client-name/client-domain token (the same denylist ID-95 PI-9 enforces on migrations); no pin ref name identifies a client; a per-client mapping is absent from the public repo entirely (it lives only in the private crib — PI-7 below). -
PI-6 (the public repo cannot answer “which client runs this version?”). The public repo holds the release artefacts (refs, tags, code) but NOT the client->version association. Possessing the public repo reveals which versions EXIST, never which client is pinned to which — the association is private (PI-7). Going public (ID-68) is therefore not gated by this layer: versioning is a client-agnostic process/config layer on top of an already client-agnostic repo. Verify: no committed artefact in the public repo maps a client to a version; the client identities appear only in the private docs-site crib; this layer adds zero new ID-68-flip blockers.
D. Client->version map — single source of truth in the private docs-site crib
Section titled “D. Client->version map — single source of truth in the private docs-site crib”-
PI-7 (the private docs-site ops crib is the single source of truth for client->version). The authoritative record of which client runs which version lives as a committed table in the PRIVATE docs-site ops crib (client identities never enter the public repo, PI-5). The crib holds, per client: the client, its Vercel project, its pinned release
vX.Y.Z, its Supabase project ref, its pinned on-prem pipeline image tag (where applicable), the last upgrade date, and an append-only upgrade history (version -> date -> migration delta). The crib is the source of truth for the MAPPING; the live Vercel pin (PI-3) and Coolify image tag (PI-10) are its enforcement — the cut/upgrade runbook keeps them consistent by updating the crib row in the SAME step as the repoint. Verify: exactly one crib location holds the client->version table; every live client pin (Vercel + Coolify) matches its crib row; an upgrade that repoints without updating the crib row in the same step is a runbook failure the checklist catches; the crib lives in the private docs-site (git ls-filesin the public repo shows no client->version map). -
PI-8 (a client’s served behaviour is fully determined by its pinned version + its own
tenant_config). Because code and baked-in client config are both fixed at build time (Context), a client’s served application behaviour is determined by exactly two things: the pinned release version (code) and the client’s OWN Supabasetenant_configrow + branding bucket (config/branding, per ID-95 PI-10/PI-11). No third version axis exists; there is no runtime version negotiation. Verify: a client deploy at a known pin renders branding hydrated from its owntenant_config(ID-95 PI-11 AC-E3 sequence), with code matching the pinnedvX.Y.Z; no runtime Supabase version query exists in the resolve path (lib/client-config.tsloadBrandingreads the build-baked map only).
E. Deploy-gate integrity — a release/rename/repoint must not silently un-gate production
Section titled “E. Deploy-gate integrity — a release/rename/repoint must not silently un-gate production”- PI-9 (cutting a release, repointing a pin, or renaming a project never silently un-gates production).
The
ci.ymldeploy-gate dispatches a status literally named"Vercel - knowledge-hub: ci-summary"(§3.3 RESEARCH), and the Vercel-side branch-protection required-check references that exact string. Any release cut, pin repoint, or Vercel project rename MUST leave the production deploy-gate intact on BOTH sides (the CI dispatch name AND the Vercel required-check name). The deploy-gate verification is an ORDERED step that precedes any rename/repoint (echoing the ID-95{95.10}ordering gate), so production is never momentarily un-gated. Verify: after any release/rename/repoint the named required-check still gates the client project’s production deploys on both the CI and Vercel sides; the runbook records the deploy-gate verification as a completed step strictly BEFORE any rename/repoint; a deliberately mismatched check name is caught (production deploy is blocked, not silently allowed through).
F. On-prem/Coolify version parity — the same semver pins the pipeline image (bl-301 tie-in)
Section titled “F. On-prem/Coolify version parity — the same semver pins the pipeline image (bl-301 tie-in)”- PI-10 (the same semver pins the on-prem/Coolify ingestion-pipeline image, not just the Vercel app).
At release cut, the on-prem pipeline image is ALSO tagged
vX.Y.Z(alongside the existing content-addressablesha-<sha>tag,onprem-deploy.yml:108), so one semver names both the Vercel release and the pipeline image. A client’s (and, per bl-301, the platform’s own) Coolify deployment pinsCOCOINDEX_IMAGE_TAGto a deliberatevX.Y.Ztag rather than to the rollingmainsha. The pipeline upgrade flow is therefore identical in SHAPE to the Vercel flow: a deliberate repoint to a newer version, with the existing prior-tag rollback ergonomics preserved (re-point Coolify to a prior tag, no rebuild —onprem-deploy.yml:106). Verify: a release cut produces avX.Y.Z-tagged pipeline image as well as thesha-tag; a client’sCOCOINDEX_IMAGE_TAGresolves to avX.Y.Zsemver (not a rollingmainsha) for any client whose contract includes the ingestion pipeline; the prior-tag rollback path still works (repoint to a priorvX.Y.Z, no rebuild).
G. No regression of the ID-95 branding model
Section titled “G. No regression of the ID-95 branding model”- PI-11 (branding/config comes from the client’s OWN Supabase
tenant_configregardless of version). The version-pinning layer does NOT change WHERE branding/config comes from: every pinned version still hydrates branding at build time from the client’s OWN Supabase project (tenant_configrow + branding bucket, ID-95 PI-10/PI-11), fail-closed (a client build with no branding row fails; adefault/no-creds control build resolvesdefaultgreen). No version introduces a per-client code path, a committed branding artefact, or a runtime branding query — ID-95 PI-1/PI-7/PI-10/PI-11 hold across every release. Verify: a client deploy at any pinnedvX.Y.Zresolves branding via the ID-95 build-time fetch (noclient-branding/dir in any repo; noif (client === 'x')branch in app source; the AC-E3 branded-render check passes); the pinning layer touches refs/tags/Vercel settings/Coolify tags only, never the branding source path.
H. Deliberate, audited upgrade
Section titled “H. Deliberate, audited upgrade”- PI-12 (every client upgrade is auditable from the crib’s append-only history). The upgrade history in the crib (PI-7) is append-only: each upgrade records the from/to version, the date, and the migration delta (and, where relevant, the DB-migration ordering actually taken). “Which client moved to which version, and when” is answerable by ONE crib lookup. The schema (DB) and code (release) versions move together as part of the documented upgrade act — the EXACT ordering (migrate-DB-first vs repoint-first) and the forward-only-migration rollback story are TECH-decided (OQ-113-2, OQ-113-3), but PRODUCT requires that whichever ordering wins, the upgrade is recorded and the schema/code pairing is never left skewed silently. Verify: the crib upgrade history is append-only (no row is overwritten); each upgrade row carries from/to version + date + migration delta; an upgrade that leaves schema and code versions skewed without a recorded remediation is a runbook failure the checklist catches.
Open decisions for ratification
Section titled “Open decisions for ratification”PRODUCT deliberately does NOT resolve the mechanism questions below — PRODUCT defines
behaviour/invariants, not the mechanism. These are CARRIED from {113.1} RESEARCH §10 for
TECH/operator resolution. OQ-113-1 is BLOCKING for {113.3} TECH and is verified by the
Orchestrator (live Vercel) before TECH begins.
-
OQ-113-1 [BLOCKING for TECH] — Vercel pin mechanism (Option A vs B). Confirm against live Vercel project settings: can a project’s Production Branch be set to an arbitrary
release/vX.Y.Zbranch per project (Option A — RESEARCH-recommended), AND can the single shared"Vercel - knowledge-hub: ci-summary"required check (ci.yml) gate multiple per-client projects’ prod deploys? If the single shared check cannot gate multiple projects, a per-project dispatch (with a version-named, PI-5-clean check-name convention) is needed — which tilts toward Option B. This is the one fork that decides the mechanism; PI-3/PI-9 hold under either outcome. Resolution: Orchestrator verifies live before TECH. -
OQ-113-2 — upgrade ordering (migrate-DB-first vs repoint-first). Whether the client’s Supabase is migrated to the target schema BEFORE repointing the Vercel pin, or after. Forward-only migrations argue migrate-first; the trade is a brief downtime window vs a schema/code skew window. PI-12 requires the ordering be recorded whichever wins. Resolution: TECH proposes; operator ratifies.
-
OQ-113-3 — rollback across a forward-only migration. Code rollback (repoint to the prior release) is trivial, but a forward-only DB migration may not be rollback-safe. Decide whether MAJOR releases are constrained to migration-additive/rollback-safe-only (to keep rollback clean) or whether some upgrades are accepted as one-way (with the crib recording the irreversibility). Couples to PI-1’s MAJOR definition and PI-12. Resolution: TECH proposes; operator ratifies.
-
OQ-113-4 — upgrade cadence / staleness signal (carried, lower priority). Is the per-client upgrade purely platform-owner initiated, or is there a client-facing “N versions behind” signal? Affects whether the crib needs a “latest available” column + staleness policy. Resolution: operator preference; can land post-TECH.
-
OQ-113-5 — exact crib location in the docs-site IA (carried). Does the client->version crib live at
runbooks/per-client-release-pins.md, or co-locate with the existing per-client ops cribrunbooks/client-app-deploy.md(single ops surface), or underreference/? RESEARCH §7 proposesrunbooks/;client-app-deploy.mdalready exists inrunbooks/and is the natural neighbour. Resolution: TECH/operator picks one; PI-7 requires only that exactly one authoritative location exist. -
OQ-113-6 — pre-release / staging channel (carried). Do clients ever pin to a pre-release (
vX.Y.Z-rc.1) release ref for UAT before a GA upgrade, or is upgrade always straight to a GA tag? Affects the branch/tag naming convention (semver pre-release suffix). Resolution: TECH proposes the naming convention; operator ratifies.
Non-goals
Section titled “Non-goals”State explicitly what this PRODUCT does NOT cover:
- The exact Vercel pin MECHANISM is out of scope for PRODUCT — release-branch-tracking
(Option A) vs deploy-from-tag (Option B) is OQ-113-1, a
{113.3}TECH decision verified live by the Orchestrator. PRODUCT pins only the behaviour (PI-3/PI-9 hold under either). - The DB-migration ordering and forward-only rollback mechanics are out of scope — OQ-113-2 / OQ-113-3 are TECH/operator decisions; PRODUCT requires only that the ordering be recorded and schema/code never left silently skewed (PI-12).
- Automated client upgrades are explicitly a Non-goal — every upgrade is a deliberate, audited repoint (PI-4); no background automation advances a client’s version.
- Tier-3 BYOC build timing is out of scope (inherited from ID-95 PI-6a) — the versioning convention must not preclude it, but WHEN to stand up BYOC is a Liam sequencing call, not an ID-113 invariant.
- bl-301 platform-pipeline STANDUP is out of scope — ID-113 defines the versioning/tag-pinning CONVENTION the platform pipeline (and the existing client pipeline) adopt (PI-10); standing up the platform-owned cocoindex pipeline is bl-301’s own work.
- ID-68 (repo-public / git-history purge) is NOT gated by this layer (PI-6) — the public repo is already client-agnostic; versioning is a process/config layer on top.
- The ID-95 per-client topology and
tenant_configbranding model are NOT re-opened — PI-11 only asserts they hold unchanged across versions; their mechanics live inspecs/id-95-per-client-topology/. - The detailed cut/upgrade runbook prose is out of scope for PRODUCT — RESEARCH §5 sketches its shape; the formalised runbook is a TECH/docs-site deliverable, not a PRODUCT invariant (PRODUCT pins the behaviour the runbook must satisfy).