Skip to content

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 numbered PI-N with 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_config model (specs/id-95-per-client-topology/).


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.

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'}) -> incoming calls from Function:scripts/fetch-client-branding.ts:main; outgoing accesses to Const:scripts/fetch-client-branding.ts:log, calls to Class:scripts/fetch-client-branding.ts:FetchClientBrandingError; "processes": []. A leaf build-time script (no runtime callers), invoked only via its own main.
  • gitnexus_context({name: 'renderClientBrandingMap', repo: 'knowledge-hub'}) -> incoming calls from Function: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_config build-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).

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”
  1. PI-1 (a release is an immutable semver vMAJOR.MINOR.PATCH cut at a known-good main). A release is created only at a main commit whose CI deploy-gate (ci-summary, §3.3 RESEARCH) is passing, and is named vMAJOR.MINOR.PATCH per 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: every vX.Y.Z tag resolves to a main ancestor commit; the cut runbook records the green ci-summary precondition; a created release tag is never moved to a different commit (a fresh defect ships as a new PATCH, never a retag).

  2. PI-2 (releases are cut from main only — no release originates off a feature/working branch). A release commit is always an ancestor on main’s first-parent history. The platform ships to main continuously; cutting a release is a deliberate, separate act that selects a known-good main commit — it is never produced from an unmerged feature/worker branch and never from staging (which is deploy-only). Verify: git merge-base --is-ancestor <release-commit> main succeeds for every release; no vX.Y.Z tag points at a commit absent from main.

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”
  1. 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 single vX.Y.Z), NOT from main and NOT from the moving tip of any shared branch. Merging to main MUST NOT change what a pinned client runs. (The exact Vercel pin mechanism — Production-Branch-tracks-release/vX.Y.Z vs 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 to main triggers no client production redeploy and changes no client’s served version; each client project’s pin resolves to exactly one vX.Y.Z; no client project’s production deploy resolves from main or a rolling branch tip.

  2. 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 main merge, 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 release vX.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)”
  1. 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-ref over 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).

  2. 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”
  1. 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-files in the public repo shows no client->version map).

  2. 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 Supabase tenant_config row + 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 own tenant_config (ID-95 PI-11 AC-E3 sequence), with code matching the pinned vX.Y.Z; no runtime Supabase version query exists in the resolve path (lib/client-config.ts loadBranding reads 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”
  1. PI-9 (cutting a release, repointing a pin, or renaming a project never silently un-gates production). The ci.yml deploy-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)”
  1. 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-addressable sha-<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 pins COCOINDEX_IMAGE_TAG to a deliberate vX.Y.Z tag rather than to the rolling main sha. 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 a vX.Y.Z-tagged pipeline image as well as the sha- tag; a client’s COCOINDEX_IMAGE_TAG resolves to a vX.Y.Z semver (not a rolling main sha) for any client whose contract includes the ingestion pipeline; the prior-tag rollback path still works (repoint to a prior vX.Y.Z, no rebuild).

G. No regression of the ID-95 branding model

Section titled “G. No regression of the ID-95 branding model”
  1. PI-11 (branding/config comes from the client’s OWN Supabase tenant_config regardless 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_config row + branding bucket, ID-95 PI-10/PI-11), fail-closed (a client build with no branding row fails; a default/no-creds control build resolves default green). 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 pinned vX.Y.Z resolves branding via the ID-95 build-time fetch (no client-branding/ dir in any repo; no if (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.
  1. 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.

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.

  1. 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.Z branch 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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 crib runbooks/client-app-deploy.md (single ops surface), or under reference/? RESEARCH §7 proposes runbooks/; client-app-deploy.md already exists in runbooks/ and is the natural neighbour. Resolution: TECH/operator picks one; PI-7 requires only that exactly one authoritative location exist.

  6. 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.


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_config branding model are NOT re-opened — PI-11 only asserts they hold unchanged across versions; their mechanics live in specs/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).