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 asfile:line. Open questions for Liam are collected in §7 asOQ-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-goodmain. Each client Vercel project sets its Production Branch to its pinnedrelease/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.ymldispatches 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 tagsha-${{ github.sha }}(onprem-deploy.yml:108) with re-point-to-prior-tag rollback. Versioning extension = additionally tag the imagevX.Y.Zat release-cut and pin Coolify’sCOCOINDEX_IMAGE_TAGto a semver tag rather than the rollingmainsha. - 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.
1. Question and constraints
Section titled “1. Question and constraints”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):
- Versioning = semver
vMAJOR.MINOR.PATCH. - 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).
- Release ownership/cadence = platform owner cuts releases at known-good
main; clients upgrade deliberately. - 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.
3. Current-state inventory (grounding)
Section titled “3. Current-state inventory (grounding)”3.1 Vercel build wiring
Section titled “3.1 Vercel build wiring”vercel.jsonexists 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 invercel.json. This matters: the pin mechanism is a Vercel project setting, not a repo file.package.jsonbuild chain (package.json:13,15,16,59):prebuild->generate:client-branding(fetch) thengenerate: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.tsreadsNEXT_PUBLIC_CLIENT_ID,SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY(lines 229-232), queriestenant_config.config(fetch-client-branding.ts:119-123), writeslib/branding/clients/<id>.json(215-217) and assets topublic/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.tsglobslib/branding/clients/*.json(line 25) -> emitslib/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 byNEXT_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.sqldefinestenant_configas 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.
3.3 CI / deploy-gate coupling
Section titled “3.3 CI / deploy-gate coupling”ci.yml:1358-1368(WP-G4.7) dispatches a deploy-gate status to Vercel viavercel/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 atPLAN.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.ymltriggers onpushto[main, staging](onprem-deploy.yml:75-97):main-> prod deploy (guardgithub.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’sCOCOINDEX_IMAGE_TAGvia PATCH + webhook deploy. Rollback is already “re-point Coolify to a priorsha-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 avX.Y.Ztag deliberately” rather than tracking the rollingmainsha. - bl-301 scope clarification: bl-301 (
ledgers/backlog/301.md, statusparked) is about standing up a platform-owned cocoindex deployment (today the on-prem Coolify pipeline is the client pipeline; the platform instancezjqbrdctesqvouboziaehas 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: onlyv0.2.0-task-view(a vendored sub-project tag), plusarchive/*ands262-*markers. No platform semvervX.Y.Ztags yet.git branch -r: feature/worker branches +staging; norelease/*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.
Option A — Version-named release branches (release/vX.Y.Z) [RECOMMENDED]
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.Zcarries 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.Ztag and therelease/vX.Y.Zbranch 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
mainor 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-summarydeploy-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)”- Verify
mainis known-good (CI green, theci-summarygate passing). - Choose the semver bump (MAJOR = breaking client-facing/migration change; MINOR = additive; PATCH = fix). Record rationale in the docs-site changelog crib.
- Tag:
git tag vX.Y.Z <known-good main sha>and push the tag (source of truth). - Create/fast-forward the release branch:
release/vX.Y.Z-> the tag commit; push. (Fast-forward-only; the branch never diverges from the tag.) - If the release touches the ingestion pipeline, ensure the on-prem image is tagged
vX.Y.Z(see §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)”- 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?). - Repoint the client’s Vercel project Production Branch to
release/vX.Y.Z. - Trigger a redeploy (push to the release branch, or Vercel redeploy).
- Verify the deploy-gate (
ci-summary) passed for the release branch; smoke-test the client URL. - Update the client->version map row in the docs-site crib (new version + upgrade date, appending to upgrade history).
- 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 existingsha-<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_TAGto a semvervX.Y.Ztag rather than the latestmainsha — 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 areference/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:
Client Vercel project Pinned release Supabase project ref Last upgraded Notes 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).
8. Dependencies and relationships
Section titled “8. Dependencies and relationships”- 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.
9. Recommendation
Section titled “9. Recommendation”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).
10. Open questions for Liam (OQ-113-N)
Section titled “10. Open questions for Liam (OQ-113-N)”- 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.Zbranch 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/vsreference/), 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;:1367literal 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": [].