Skip to content

RESEARCH -- Platform per-client deploy: tag/release version pinning (ID-113.1)

RESEARCH — Platform per-client deploy: tag/release version pinning (ID-113.1)

Section titled “RESEARCH — Platform per-client deploy: tag/release version pinning (ID-113.1)”

Task: ID-113 — Platform per-client deploy: tag/release version pinning design + standup. Subtask: {113.1} RESEARCH. Author date: 15/06/2026. Status: DECISION-SUPPORT. Read-only research grounding the Vercel pin-mechanism decision, the cut/promote runbook shape, the client->version map location, and the on-prem/Coolify tag-pinning tie-in (bl-301). Promoted from bl-321 (S359). Builds directly on the ID-95 per-client topology decisions (deploy model, tenant_config).

This is the {113.1} artefact. {113.2} PRODUCT, {113.3} TECH, {113.4} PLAN are separate fresh-Planner instances (Q-PLANNER-2) if the Orchestrator elects to upgrade. Every load-bearing in-repo claim is cited as file:line. Open questions for Liam are collected in §7 as OQ-113-N.


  • Recommended Vercel pin mechanism: version-named release branches (release/vX.Y.Z), each fast-forwarded to a semver tag cut at a known-good main. Each client Vercel project sets its Production Branch to its pinned release/vX.Y.Z. Upgrade = repoint the client project’s Production Branch to a newer release branch and redeploy. This is preferred over Vercel deploy-from-tag because Vercel’s first-class production-deploy trigger is a git branch, not a tag — branches give a clean one-line dashboard mapping and a natural redeploy trigger (a fast-forward push), whereas tag-pinning needs a per-deploy hook/CLI invocation. CONFIRM against the live Vercel project settings (OQ-113-1) — the branch-vs-tag fork is the single load-bearing decision in this Task.
  • Required-check naming gotcha is real and pre-identified (ID-95 dossier): ci.yml dispatches a deploy-gate status literally named "Vercel - knowledge-hub: ci-summary" (ci.yml:1367). The Vercel-side branch-protection required-check references this exact string. Per-client projects either reuse this single platform check (preferred — one build gate for all releases) or each needs its own dispatch; either way a project rename/repoint can silently un-gate prod unless both sides are updated together. ID-95 {95.10} already records this as an ordering gate (sweep + deploy-gate verify BEFORE any rename).
  • client->version map lives in the docs-site ops crib as a small committed table (one row per client: client -> pinned release/vX.Y.Z -> Supabase project ref -> upgrade history), client-agnostic in the public repo (PI-7), client identities only in the PRIVATE docs-site.
  • On-prem/Coolify (bl-301 tie-in): the pipeline is trigger-keyed to a branch (main/staging, onprem-deploy.yml:75-97) but already deploys a content-addressable image tag sha-${{ github.sha }} (onprem-deploy.yml:108) with re-point-to-prior-tag rollback. Versioning extension = additionally tag the image vX.Y.Z at release-cut and pin Coolify’s COCOINDEX_IMAGE_TAG to a semver tag rather than the rolling main sha.
  • This is NOT ID-68-flip-blocking. The public repo is already client-agnostic (zero client data); versioning is a process/config layer on top, not a prerequisite for going public.

ID-113 must design how a client deployment is pinned to a specific platform version rather than tracking main, so that the platform owner can ship to main continuously while each client upgrades deliberately. The deploy model itself is ratified (ID-95): public repo = client-agnostic platform code -> each client = own Vercel project + own Supabase project -> client branding/config pulled from the client’s own Supabase tenant_config (built under ID-95). What is undesigned is the release/version pinning layer: the versioning scheme, the cut process, and the per-client upgrade flow.

Ratified inputs (do not relitigate):

  1. Versioning = semver vMAJOR.MINOR.PATCH.
  2. Pin mechanism = client-agnostic version-named release branches (NO client-named refs in the public repo, per the PI-7 family); client->version map in the docs-site ops crib. Vercel deploy-from-tag is the named alternative — release branches preferred for clean Vercel mapping, CONFIRM in research (this doc).
  3. Release ownership/cadence = platform owner cuts releases at known-good main; clients upgrade deliberately.
  4. Same versioning extends to the on-prem/Coolify ingestion pipeline (currently branch-keyed) — the bl-301 platform-pipeline tie-in.

The single biggest fork inside the ratified envelope is how Vercel pins a project to a version: a release branch the project tracks, or a tag the project deploys from (OQ-113-1). Everything downstream (the cut runbook, the upgrade flow, the required-check coupling) cascades from that choice.


2. Code-intelligence orientation (Context)

Section titled “2. Code-intelligence orientation (Context)”

Per the code-intelligence discipline, orientation ran before authoring. Outputs cited verbatim (not paraphrased):

gitnexus_query({query: 'deploy release version branding tenant_config', repo: 'knowledge-hub'}) returned no execution-flow processes ("processes": []) — versioning/release is a config-and-CI concern, not an indexed runtime call graph — but surfaced the build-time branding definitions:

  • 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 calls to Class:scripts/fetch-client-branding.ts:FetchClientBrandingError; "processes": []. It is 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.

Interpretation for this Task. The branding/config pipeline is entirely build-time (fetch from Supabase tenant_config -> write JSON -> codegen a static map -> baked into the bundle), not request-time. That is the key fact for version-pinning: 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 (branch or tag) is sufficient to pin the entire client-facing behaviour. No runtime version negotiation is needed.


  • vercel.json exists at repo root. buildCommand: "bun run build:vercel" (vercel.json:3), installCommand: "bun install --frozen-lockfile" (vercel.json:4), framework: "nextjs" (vercel.json:5). No git/branch/production-branch config in the JSON — branch/production routing is configured Vercel-side (dashboard / project settings), not in vercel.json. This matters: the pin mechanism is a Vercel project setting, not a repo file.
  • package.json build chain (package.json:13,15,16,59):
    • prebuild -> generate:client-branding (fetch) then generate:branding (codegen).
    • generate:client-branding = bun run scripts/fetch-client-branding.ts (package.json:15).
    • generate:branding = bun run scripts/generate-client-branding-map.ts (package.json:16).
    • build:vercel = full orchestration (skills + content-type + client-branding fetch + branding codegen + mcp-apps + next build) (package.json:59).

3.2 Build-time client-config / branding pipeline

Section titled “3.2 Build-time client-config / branding pipeline”
  • scripts/fetch-client-branding.ts reads NEXT_PUBLIC_CLIENT_ID, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY (lines 229-232), queries tenant_config.config (fetch-client-branding.ts:119-123), writes lib/branding/clients/<id>.json (215-217) and assets to public/clients/<id>/* (219-223). Fail-closed (PI-11): a client build (id !== 'default' + creds present) THROWS on missing/invalid config — never silently falls back to default.
  • scripts/generate-client-branding-map.ts globs lib/branding/clients/*.json (line 25) -> emits lib/branding/client-branding-map.generated.ts (lines 26-31).
  • lib/client-config.ts (687 lines) resolves the active config at runtime from the generated map keyed by NEXT_PUBLIC_CLIENT_ID (loadBranding(), lines 604-627; lookup at 605-606) — no runtime Supabase query. Config is baked into the build.
  • supabase/migrations/20260613090000_id95_5_tenant_config.sql defines tenant_config as a singleton (id boolean PRIMARY KEY DEFAULT true, lines 36-42), RLS-closed, service-role-only; config set out-of-band (config-as-data, PI-7), never in migrations.
  • ci.yml:1358-1368 (WP-G4.7) dispatches a deploy-gate status to Vercel via vercel/repository-dispatch/actions/status, named literally "Vercel - knowledge-hub: ci-summary" (ci.yml:1367), if: always() so Vercel sees both success and failure. The Vercel-side branch-protection required-check references this exact string; half the config lives in the Vercel dashboard. ID-95’s dossier flagged this (id95-oq-dossier-2026-06-11.md:58,91) and {95.10} records the ordering gate (PLAN.md:233,278; risk row at PLAN.md:354): sweep + deploy-gate verify BEFORE any Vercel project rename, or prod silently un-gates.

3.4 On-prem / Coolify ingestion pipeline (bl-301 tie-in)

Section titled “3.4 On-prem / Coolify ingestion pipeline (bl-301 tie-in)”
  • onprem-deploy.yml triggers on push to [main, staging] (onprem-deploy.yml:75-97): main -> prod deploy (guard github.ref == 'refs/heads/main', line 335), staging -> staging deploy (line 441). Branch-keyed trigger.
  • It already builds a content-addressable image tag TAG: sha-${{ github.sha }} (onprem-deploy.yml:108), NEVER :latest, and bumps Coolify’s COCOINDEX_IMAGE_TAG via PATCH + webhook deploy. Rollback is already “re-point Coolify to a prior sha- tag, no rebuild” (onprem-deploy.yml:104-106). So the pipeline is image-pinned but branch-triggered — the versioning gap is purely “tag the image with the semver release too, and let Coolify pin to a vX.Y.Z tag deliberately” rather than tracking the rolling main sha.
  • bl-301 scope clarification: bl-301 (ledgers/backlog/301.md, status parked) is about standing up a platform-owned cocoindex deployment (today the on-prem Coolify pipeline is the client pipeline; the platform instance zjqbrdctesqvouboziae has no ingestion path). ID-113’s tie-in is the versioning/tag-pinning approach that bl-301’s platform pipeline (and the existing client pipeline) should both adopt — not the standup itself.

3.5 Existing tags / branches (greenfield-ish)

Section titled “3.5 Existing tags / branches (greenfield-ish)”
  • git tag --list: only v0.2.0-task-view (a vendored sub-project tag), plus archive/* and s262-* markers. No platform semver vX.Y.Z tags yet.
  • git branch -r: feature/worker branches + staging; no release/* branches yet. So this is a fresh convention — no migration of existing refs required.

4. Options and trade-offs — Vercel pin mechanism

Section titled “4. Options and trade-offs — Vercel pin mechanism”

The ratified decision is “version-named release branches”, with deploy-from-tag named as the alternative to confirm. Both are evaluated against the one fact that decides it: Vercel’s production deploy is triggered by a git branch, not a tag.

Section titled “Option A — Version-named release branches (release/vX.Y.Z) [RECOMMENDED]”

Cut a semver tag at known-good main; create/fast-forward release/vX.Y.Z to that tag. Each client Vercel project sets Production Branch = release/vX.Y.Z. Upgrade = repoint the project’s Production Branch to a newer release branch + redeploy.

  • (+) Native Vercel mapping. Production Branch is a first-class project setting; one line per client in the Vercel dashboard. A push to the release branch is the natural redeploy trigger.
  • (+) Clean upgrade UX. Repoint Production Branch (one dropdown / one API call) -> redeploy. No per-deploy CLI.
  • (+) Client-agnostic (PI-7). release/vX.Y.Z carries no client identity; the public repo stays clean. The client->version mapping lives only in the private docs-site crib.
  • (+) Preview/rollback for free. Vercel keeps prior production deployments per branch; rollback = redeploy a prior deployment or repoint to the prior release branch.
  • (-) Branch proliferation. One long-lived branch per release version (not per client). Manageable — branches are cheap and fast-forward-only.
  • (-) Two refs to keep consistent. Both the vX.Y.Z tag and the release/vX.Y.Z branch point at the same commit. Mitigated by making branch creation a fast-forward to the tag in the cut runbook (the tag is the source of truth; the branch is the Vercel handle).

Option B — Deploy-from-tag (Vercel CLI / deploy hook per tag)

Section titled “Option B — Deploy-from-tag (Vercel CLI / deploy hook per tag)”

Cut a semver tag; deploy each client by invoking vercel deploy --prod (or a deploy hook) pinned to that tag’s commit.

  • (+) Single source of truth. Only the tag exists; no parallel branch.
  • (+) Immutable. Tags don’t move; a deployed version is unambiguous.
  • (-) Not Vercel’s native production trigger. Vercel production deploys hang off the Production Branch; tag-driven prod deploys require an external orchestrator (GitHub Action / CLI) per client, per upgrade — more moving parts, more places to drift.
  • (-) Murkier dashboard mapping. The Vercel project’s Production Branch would still point somewhere (likely main or a dummy), so “what version is this client on?” is no longer answerable from the project settings alone — it lives in the deploy command history. Weaker than Option A’s one-line answer.
  • (-) Required-check coupling is harder. The ci-summary deploy-gate dispatch (§3.3) is branch-oriented; gating ad-hoc tag deploys needs rethinking the gate.

Option C — Git submodule / vendored-version (rejected, noted for completeness)

Section titled “Option C — Git submodule / vendored-version (rejected, noted for completeness)”

Pin via a version manifest the client repo vendors. Rejected: there is no per-client repo (the public repo is the single platform repo); this reintroduces per-client refs and fights the ID-95 model.

Recommendation: Option A (release-branch-per-version). It is the only option where the pin is a native Vercel project setting, the upgrade is a one-line repoint, the dashboard answers “what version?” directly, and the existing branch-oriented deploy-gate keeps working. Option B’s immutability advantage is recovered by treating the vX.Y.Z tag as the source of truth and release/vX.Y.Z as a fast-forward-only Vercel handle. CONFIRM (OQ-113-1) against live Vercel project settings that Production Branch can be set to an arbitrary release/* branch per project and that the ci-summary required check can gate it.


5. The cut / promote runbook shape (proposed, for PRODUCT/TECH to formalise)

Section titled “5. The cut / promote runbook shape (proposed, for PRODUCT/TECH to formalise)”

5.1 Cut a release (platform owner, at known-good main)

Section titled “5.1 Cut a release (platform owner, at known-good main)”
  1. Verify main is known-good (CI green, the ci-summary gate passing).
  2. Choose the semver bump (MAJOR = breaking client-facing/migration change; MINOR = additive; PATCH = fix). Record rationale in the docs-site changelog crib.
  3. Tag: git tag vX.Y.Z <known-good main sha> and push the tag (source of truth).
  4. Create/fast-forward the release branch: release/vX.Y.Z -> the tag commit; push. (Fast-forward-only; the branch never diverges from the tag.)
  5. If the release touches the ingestion pipeline, ensure the on-prem image is tagged vX.Y.Z (see §6).
  6. Record the release in the docs-site ops crib (version, sha, date, notable changes, migration deltas).

5.2 Upgrade a client (deliberate, platform owner + client coordination)

Section titled “5.2 Upgrade a client (deliberate, platform owner + client coordination)”
  1. Pre-flight: review migration deltas between the client’s current pinned version and the target vX.Y.Z; confirm the client’s Supabase project is migrated to the target schema (one canonical migration set per ID-95) — schema and code versions must move together (OQ-113-2: ordering — migrate DB first or repoint first?).
  2. Repoint the client’s Vercel project Production Branch to release/vX.Y.Z.
  3. Trigger a redeploy (push to the release branch, or Vercel redeploy).
  4. Verify the deploy-gate (ci-summary) passed for the release branch; smoke-test the client URL.
  5. Update the client->version map row in the docs-site crib (new version + upgrade date, appending to upgrade history).
  6. Rollback path: repoint Production Branch back to the prior release/vX.Y.Z + redeploy (and, if a migration was forward-only, the DB rollback story — OQ-113-3).

6. On-prem / Coolify pipeline tag-pinning (bl-301 tie-in)

Section titled “6. On-prem / Coolify pipeline tag-pinning (bl-301 tie-in)”

The ingestion pipeline already deploys an immutable, content-addressable image tag with prior-tag rollback (§3.4) — the only gap is that the tag is a rolling main sha rather than a deliberate semver version, and the trigger is branch-push rather than release-driven.

Proposed approach (for TECH to formalise):

  • At release-cut, additionally tag the pipeline image vX.Y.Z (alongside the existing sha-<sha> tag) so the same semver names both the Vercel release and the pipeline image.
  • Pin a client’s (and the platform’s, per bl-301) Coolify COCOINDEX_IMAGE_TAG to a semver vX.Y.Z tag rather than the latest main sha — making the upgrade flow identical in shape to Vercel: deliberate repoint to a newer version.
  • Preserve the existing rollback ergonomics (re-point Coolify to a prior tag, no rebuild).
  • bl-301’s platform-owned pipeline standup adopts this versioned-tag convention from day one; the existing client pipeline migrates from main-sha pinning to semver-tag pinning as part of ID-113’s rollout.

7. client->version map — location and format (proposed)

Section titled “7. client->version map — location and format (proposed)”
  • Location: the PRIVATE docs-site ops crib (client identities never enter the public repo, PI-7). Suggested path: ${KH_PRIVATE_DOCS_DIR}/src/content/docs/runbooks/per-client-release-pins.md (or a reference/ crib) — to be confirmed against the docs-site IA in PRODUCT.

  • Format: a small committed markdown table, one row per client, plus an append-only upgrade history. Proposed columns:

    ClientVercel projectPinned releaseSupabase project refLast upgradedNotes

    Plus a per-client upgrade-history list (version -> date -> who -> migration delta) so the audit trail of “which client is on which version, and when did it move” is one lookup.

  • Source-of-truth discipline: the crib is authoritative for the client->version mapping; the Vercel Production Branch setting is the live enforcement. The cut/upgrade runbook (§5) keeps them consistent (update the crib row in the same step as the repoint).


  • ID-95 (per-client topology + deploy model) — the parent decision this builds on: client = own Vercel + own Supabase; branding/config from the client’s tenant_config (build-time fetch, §3.2). ID-113 adds the version-pinning layer on top. The {95.10} ordering-gate record (deploy-gate / required-check rename discipline) is directly relevant to per-client Vercel project setup (§3.3).
  • ID-68 (repo-public / git-history purge)NOT flip-blocking. The public repo is already client-agnostic (zero client data); versioning is a process/config layer that does not gate going public. Sequencing note only: PI-7-clean release branch names reinforce the same no-client-literal discipline ID-68 enforces.
  • bl-301 (platform-owned cocoindex pipeline standup) — the on-prem pipeline tie-in (§6). ID-113 defines the versioning/tag-pinning convention; bl-301 stands up the platform pipeline that adopts it. Recommend ID-113 ship the convention; bl-301 consume it (no hard ordering, but bl-301 should not standup before the convention is set, to avoid a re-pin).
  • ID-108 (platform dev instance) — origin of bl-301; context only.

Adopt Option A (release-branch-per-version) as the Vercel pin mechanism: cut semver vX.Y.Z tags at known-good main (tag = source of truth), fast-forward release/vX.Y.Z branches to each tag (branch = Vercel handle), and set each client project’s Production Branch to its pinned release branch. Extend the same semver to the on-prem pipeline image tag, pinning Coolify’s COCOINDEX_IMAGE_TAG to a deliberate vX.Y.Z. Keep the client->version map as a committed table in the private docs-site ops crib. Treat the ci-summary required-check coupling (§3.3) as a first-class hazard in the per-client project-setup runbook — verify the deploy-gate on both the CI and Vercel sides before any project rename/repoint, per the {95.10} pattern.

This is a PRODUCT + TECH (+ PLAN) Task: behaviourally it defines an operator-facing release/upgrade flow (PRODUCT-worthy: testable invariants like “an upgrade is a Production Branch repoint + crib update; a client never tracks main”), and technically it spans Vercel config, CI deploy-gate coupling, the on-prem image-tag chain, and a docs-site crib (TECH-worthy). PLAN likely warranted (multiple surfaces: Vercel, CI, Coolify, docs crib).


  • OQ-113-1 [BLOCKING for TECH]. Confirm against live Vercel project settings: can a project’s Production Branch be set to an arbitrary release/vX.Y.Z branch per project, and can the existing "Vercel - knowledge-hub: ci-summary" required check (ci.yml:1367) gate that branch’s prod deploys? If the single shared check cannot gate multiple per-client projects, do we need a per-project dispatch (and a check-name convention that survives PI-7 — i.e. version-named, not client-named)? This is the one fork that decides Option A vs B.
  • OQ-113-2. Upgrade ordering: migrate the client’s Supabase to the target schema before repointing the Vercel Production Branch, or after? (Forward-only migrations argue migrate-first; brief downtime window vs schema/code skew window.)
  • OQ-113-3. Rollback semantics when a release included a forward-only migration: code rollback (repoint to prior release branch) is trivial, but the DB may not be rollback-safe. Do we constrain MAJOR releases to be migration-additive-only to keep rollback clean, or accept that some upgrades are one-way?
  • OQ-113-4. Cadence / who triggers: is the per-client upgrade purely platform-owner initiated, or is there a client-facing “you’re N versions behind” signal? Affects whether the crib needs a “latest available” column and a staleness policy.
  • OQ-113-5. Where exactly in the docs-site IA does the client->version crib live (runbooks/ vs reference/), and does it co-locate with the existing per-client Supabase-project ops crib from ID-95 (single ops surface) or stay separate?
  • OQ-113-6. Pre-release / staging channel: do clients ever pin to a pre-release (vX.Y.Z-rc.1) release branch for UAT before a GA upgrade, or is upgrade always straight to a GA tag? Affects the branch/tag naming convention.

Appendix A — Citation index (file:line, verbatim from orientation)

Section titled “Appendix A — Citation index (file:line, verbatim from orientation)”
  • vercel.json:3-5 — buildCommand/installCommand/framework; no branch config in JSON.
  • package.json:13,15,16,59 — prebuild + client-branding fetch/codegen + build:vercel.
  • scripts/fetch-client-branding.ts:119-123 (tenant_config query), :215-223 (output paths), :229-232 (env vars), fail-closed PI-11.
  • scripts/generate-client-branding-map.ts:25 (glob), :26-31 (generated output).
  • lib/client-config.ts:604-627 (loadBranding), :605-606 (env-keyed lookup, no runtime Supabase).
  • supabase/migrations/20260613090000_id95_5_tenant_config.sql:36-42 — singleton table.
  • ci.yml:1358-1368 — WP-G4.7 Vercel deploy-gate dispatch; :1367 literal check name "Vercel - knowledge-hub: ci-summary".
  • onprem-deploy.yml:75-97 (branch triggers), :104-108 (image tag + rollback note), :335 (main prod guard), :441 (staging guard).
  • ID-95: id95-oq-dossier-2026-06-11.md:58,91 (deploy-gate coupling); PLAN.md:233,278,354 ({95.10} ordering gate); PRODUCT.md:216 (PI-7 definition).
  • ledgers/backlog/301.md — bl-301 scope (platform-owned cocoindex standup, parked).
  • gitnexus orientation (knowledge-hub repo): query -> "processes": [] + branding defs; context(runClientBrandingFetch) / context(renderClientBrandingMap) -> leaf build-time scripts, "processes": [].