Skip to content

TECH -- Platform per-client deploy: tag/release version pinning (ID-113.3)

TECH — Platform per-client deploy: tag/release version pinning (ID-113.3)

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

Task: ID-113 — Platform per-client deploy: tag/release version pinning design + standup. Subtask: {113.3} TECH. Author date: 15/06/2026. Status: SPEC — technical plan. Fresh-Planner artefact (Q-PLANNER-2): authored by a different instance from the {113.2} PRODUCT author and the {113.1} RESEARCH author; both predecessors read in full. This TECH maps 1:1 to PRODUCT’s twelve PI-N invariants (one “Proposed changes” block per PI, each independently verifiable), resolves the two TECH-owned open decisions (OQ-113-2 upgrade ordering, OQ-113-3 forward-only rollback), confirms the exact Vercel pin mechanism against current config, defines the cut/promote runbook, and covers the on-prem/Coolify semver tag-pinning (bl-301 tie-in). UK English throughout (DD/MM/YYYY, “colour”, “organisation”, “behaviour”). ASCII only.

OQ-113-1 is RESOLVED upstream (Orchestrator, config-evidenced; ratified by Liam, see ledger ID-113.2 journal): Option A — client-agnostic version-named release branches (release/vX.Y.Z) fast-forwarded to vX.Y.Z tags; each per-client Vercel project pins its Production Branch to a release branch and opts in via its dashboard to the SAME repo-named "Vercel - knowledge-hub: ci-summary" deploy-gate check. This TECH does NOT re-open the mechanism; it specifies how to implement it.


This Task adds the release/version-pinning layer on top of the ratified ID-95 per-client topology (own Vercel project + own Supabase + branding from the client’s own tenant_config at build time). PRODUCT (PRODUCT.md) defines the behaviour; this TECH is the implementation plan. There is almost no new application CODE here: the layer is overwhelmingly git ref convention + Vercel/Coolify project settings + a CI workflow delta + a private docs-site crib + a runbook. The single application-source touch point is a build-time semver stamp (see PI-1 / PI-8 below). Everything else is process and configuration, which is why the verification surface is git/Vercel/Coolify state and operator runbook behaviour, not a runtime call graph.

Per the code-intelligence discipline, orientation ran before authoring. The GitNexus index is STALE (last indexed at 69b7bd0); per the dispatch brief, exact file:line claims were grounded with bun scripts/ast-dataflow-cli.ts / grep / direct file reads rather than the stale graph, with GitNexus reserved for graph-level orientation. The graph-level orientation (re-cited verbatim from the ratified {113.1}/{113.2} predecessors, which ran it live) returned "processes": [] for the release/versioning concept — versioning/release is a config-and-CI concern, not an indexed runtime call graph — and surfaced only the build-time branding leaves (runClientBrandingFetch, renderClientBrandingMap), both confirmed leaf build-time scripts with no runtime callers. The release/versioning surface itself is genuinely greenfield in the repo: gitnexus orientation: no existing symbols match -- greenfield surface.

Grounding (file:line verified live for this TECH, not paraphrased from predecessors):

  • vercel.json:1-91 — carries buildCommand/installCommand/framework (:3-5), regions: ["lhr1"] (:41), security headers, functions maxDuration map, and crons. No git/branch/productionBranch key anywhere — confirming the pin is a Vercel project dashboard setting, not a repo file. This grounds PI-3 (the pin is not committed) and PI-5 (no client identity in vercel.json).
  • package.json:11,13,15,16,59build = next build (:11); prebuild chains generate:skills -> generate:content-type-values -> generate:client-branding -> generate:branding (:13); generate:client-branding = fetch-client-branding.ts (:15); generate:branding = generate-client-branding-map.ts (:16); build:vercel is the full Vercel orchestration ending in next build (:59). This is where a build-time semver stamp attaches (PI-1/PI-8).
  • lib/client-config.ts:604-627loadBranding(idOverride?) resolves CLIENT_BRANDING_MAP[id] ?? CLIENT_BRANDING_MAP.default (:605-606), schema-validates, contrast-checks, and BRANDING is computed once at module init (:629). No runtime Supabase query — config is baked into the build. Grounds PI-8/PI-11 (served behaviour is fixed at build time; no runtime version axis).
  • ci.yml:1358-1368 (WP-G4.7) — the deploy-gate step Notify Vercel -- ci-summary status, if: always(), uses: vercel/repository-dispatch/actions/status@30f760c..., with name: "Vercel - knowledge-hub: ci-summary" (:1367, verified verbatim). The inline comment confirms: “project must opt in to the named check via the Vercel dashboard”. This is the deploy-gate hazard surface for PI-9 and the multi-project opt-in mechanism for OQ-113-1’s resolution.
  • onprem-deploy.yml:104-108env.IMAGE = ghcr.io/${{ github.repository_owner }}/kh-cocoindex-pipeline (:107), env.TAG = sha-${{ github.sha }} (:108), with the documented prior-tag rollback (“re-point Coolify to a prior sha- tag; no rebuild”, :104-106).
  • onprem-deploy.yml:239-247 — the actual publish: pack build "${IMAGE}:${TAG}" --path ... --cache-image "${IMAGE}:buildcache" ... --publish then echo "Published ${IMAGE}:${TAG}". This is the precise site where a second vX.Y.Z image tag is added (PI-10). The Coolify bump is a PATCH of COCOINDEX_IMAGE_TAG at onprem-deploy.yml:343-367 (prod) / :448-472 (staging); the workflow triggers on push: [main, staging] (:92-96), branch-keyed.
  • supabase/migrations/20260613090000_id95_5_tenant_config.sql:36-42tenant_config singleton (id boolean PRIMARY KEY DEFAULT true), RLS-closed, service-role-only; config-as-data, never set in migrations. Grounds PI-11.
  • Ref state (verified live): git tag --list shows only v0.2.0-task-view (a vendored sub-project tag) — no platform vX.Y.Z tags; git branch -r shows no release/* branches. Greenfield convention; no existing-ref migration required (PI-1/PI-2/PI-5).

Empirical-verification note (Q-EX2 scope). This TECH cites no external-library API symbols requiring an import-and-call check. The verification surface is git ref / Vercel project-setting / Coolify image-tag configuration / GitHub Actions workflow YAML and operator runbook behaviour. The two mechanism-dependent empirical claims (Vercel Production Branch can pin an arbitrary release/* per project; one repo-named ci-summary check can gate N per-client projects via per-project dashboard opt-in) were resolved as OQ-113-1 by the Orchestrator from config evidence (ci.yml:1358-1368 + vercel.json + the vercel/repository-dispatch action semantics) and ratified by Liam before this TECH. The only residual live-dashboard check (multi-project opt-in sanity check at first standup) is carried as a NON-blocking runbook step, not a TECH blocker.


PRODUCT carried OQ-113-2 and OQ-113-3 as “TECH proposes; operator ratifies”. This TECH PROPOSES both; they remain operator-ratifiable. (OQ-113-1 was Orchestrator-resolved before TECH; OQ-113-4/5/6 are operator-preference and resolved below where this TECH can pick a default, else carried.)

OQ-113-2 RESOLVED (proposed): migrate-DB-first, then repoint-Vercel

Section titled “OQ-113-2 RESOLVED (proposed): migrate-DB-first, then repoint-Vercel”

Decision: migrate the client’s Supabase to the target schema BEFORE repointing the Vercel Production Branch. Justification, grounded in the build-time model:

  • Forward-only migrations are additive-by-discipline (see OQ-113-3) and old code tolerates the new schema. KH migrations are forward-only (no down); the project convention is that a migration adds columns/tables/functions without removing what the currently-pinned (older) code reads. So a client running release A against a schema already migrated to B’s shape continues to work: A’s code reads the columns it always read; B’s new columns sit unused until B’s code ships. The migrate-first window is therefore a compatibility window, not a downtime window — the live (older) deploy keeps serving correctly while the DB is ahead.
  • The reverse ordering (repoint-first) creates a genuine skew fault. If Vercel is repointed to B before the DB is migrated, B’s code (which may read a column/table B’s migration introduces) runs against a schema that lacks it — a hard runtime failure (missing column / missing RPC) for the duration of the window. Because branding/config and code are baked at build time (lib/client-config.ts:604-627, no runtime negotiation), there is no graceful-degradation path: B’s build expects B’s schema.
  • The cutover window is bounded and observable. Migrate-first means: (1) apply the migration set to the client’s Supabase, (2) verify schema parity, (3) repoint + redeploy, (4) smoke-test. The only window where schema is “ahead” of code is steps 1-3, during which the OLD deploy serves correctly. This is the safe direction.

Consequence for PI-12: the crib upgrade-history row records the ordering taken (migrate-first) and the migration delta, so schema/code pairing is never left silently skewed. The runbook (below) enforces migrate-first as an ordered checklist.

Carried sub-decision -> OQ-113-7 (new, below): a migration that is NOT old-code-tolerant (a destructive/renaming migration) breaks the migrate-first safety argument. This TECH constrains such migrations out of the normal path via OQ-113-3 + the new OQ-113-7.

OQ-113-3 RESOLVED (proposed): roll-forward-only is the supported posture; rollback-unsafe migrations are gated to MAJOR + flagged irreversible

Section titled “OQ-113-3 RESOLVED (proposed): roll-forward-only is the supported posture; rollback-unsafe migrations are gated to MAJOR + flagged irreversible”

Supported rollback posture:

  1. Code-only rollback is always supported and trivial. Repoint the Vercel Production Branch back to the prior release/vX.Y.(Z-1) + redeploy; repoint Coolify COCOINDEX_IMAGE_TAG to the prior vX.Y.Z tag, no rebuild (the existing onprem-deploy.yml:104-106 ergonomics). This is the supported path when the release was migration-additive (the common case).

  2. Across a forward-only DB migration, the supported posture is ROLL-FORWARD-ONLY. KH migrations have no down; therefore a DB schema change is NOT auto-reverted. If a release that included a forward-only migration misbehaves, the supported remediation is to cut and roll FORWARD to a vX.Y.(Z+1) PATCH that fixes the defect at the new schema — NOT to roll the schema back.

  3. Migration-additive discipline keeps code rollback clean. To keep posture (1) viable across most upgrades, migrations SHOULD be additive/back-compatible (add, do not remove/rename in the same release the consuming code ships). When additive, code rollback (1) is fully safe because the prior code tolerates the superset schema.

  4. A rollback-UNSAFE migration (destructive/renaming) is gated to a MAJOR release and flagged irreversible in the crib. Where a destructive migration is unavoidable, it is a MAJOR bump (PI-1’s MAJOR = breaking/non-rollback-safe), and the crib upgrade-history row records the irreversibility explicitly (no clean code-rollback exists past that point).

NOT supported (stated explicitly per the brief):

  • Automatic DB rollback / down migrations — NOT supported. KH has no down-migration mechanism; nothing in this layer adds one.
  • Snapshot-restore as a routine rollback step — NOT supported as the primary path. A point-in-time restore of the client’s Supabase is an emergency, manual, out-of-band operation (data-loss window for anything written after the snapshot); it is named here as a last-resort escape hatch ONLY, never the designed rollback. The designed rollback is code-repoint (1) for additive releases and roll-forward (2) across forward-only migrations.
  • Compensating-migration as an automated step — NOT supported. A compensating migration (a new forward migration that undoes a prior one’s effect) is, mechanically, just a roll-forward PATCH (posture 2); it is allowed as such but there is no special “compensating rollback” machinery.

Consequence for PI-1 / PI-12: MAJOR is the version class for any non-rollback-safe change; the crib records irreversibility. Couples to OQ-113-7.


Proposed changes (1:1 against PRODUCT’s PI-N)

Section titled “Proposed changes (1:1 against PRODUCT’s PI-N)”

Each block names the concrete mechanism, the files/settings touched, and how the change is verifiable. Most blocks are config/process; the few that touch source name exact files.

PI-1 — immutable semver vX.Y.Z cut at known-good main (tag = source of truth)

Section titled “PI-1 — immutable semver vX.Y.Z cut at known-good main (tag = source of truth)”

Mechanism (git + CI precondition): A release is git tag vX.Y.Z <main-sha> at a main commit whose "Vercel - knowledge-hub: ci-summary" deploy-gate (ci.yml:1367) is green. The tag is the immutable source of truth and is never moved; a fresh defect ships as a new PATCH tag, never a retag. The cut runbook (below) records the green-ci-summary precondition as step 1.

Optional source touch (build-time version stamp): to make a deploy self-report its version (supports PI-8 verification and operator smoke-tests), add a build-time injected NEXT_PUBLIC_RELEASE_VERSION (read from the vX.Y.Z ref at build) surfaced on an existing health/version endpoint. This is the ONLY application-source change in the Task and is OPTIONAL (carried as OQ-113-8); the layer is correct without it (the Vercel dashboard Production Branch already answers “what version” — Option A’s stated advantage).

Verify: every vX.Y.Z tag resolves to a main ancestor (git merge-base --is-ancestor); the cut runbook entry records the green ci-summary precondition; no tag is ever re-pointed (audited by git tag immutability + the changelog crib).

Mechanism (git discipline, enforced by runbook + verification): the cut runbook tags only a main first-parent ancestor; never staging (deploy-only) and never an unmerged worker branch. Optional CI guard: a release-tag-push workflow that asserts git merge-base --is-ancestor <tag-sha> origin/main and fails the tag push otherwise (carried as OQ-113-9, low priority — the runbook check is sufficient at current scale).

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.

PI-3 — client deploy pinned to a release, never main-tracking

Section titled “PI-3 — client deploy pinned to a release, never main-tracking”

Mechanism (Vercel project setting — Option A): each knowledge-hub-{client} Vercel project sets Settings -> Git -> Production Branch = release/vX.Y.Z (the client-agnostic version-named release branch). This is a dashboard setting, NOT a vercel.json key (vercel.json carries no branch config, :1-91). A merge to main does not touch any release/* branch, so a pinned client never redeploys on a main merge. The release branch is fast-forward-only to its vX.Y.Z tag (the tag is the source of truth; the branch is the Vercel handle).

Verify: a no-op merge to main triggers no client production redeploy; each client project’s Production Branch resolves to exactly one release/vX.Y.Z (one Vercel dashboard line per client); no client project tracks main or a rolling branch tip.

PI-4 — upgrade is a deliberate repoint; never automatic

Section titled “PI-4 — upgrade is a deliberate repoint; never automatic”

Mechanism: upgrade = an operator changes the client project’s Production Branch from release/vA to release/vB (one dashboard dropdown / one Vercel API call) + redeploy. No automation advances it: cutting release/v(Z+1) creates a NEW branch and never touches any existing client project’s Production Branch setting. There is no webhook, cron, or CI step in this design that repoints a client.

Verify: cutting vX.Y.(Z+1) leaves every existing client still serving its prior pinned version; the only path that changes a served version is the recorded repoint step in the upgrade runbook; no automated-upgrade mechanism exists anywhere in the design.

PI-5 — every pin ref in the public repo is client-agnostic / version-named

Section titled “PI-5 — every pin ref in the public repo is client-agnostic / version-named”

Mechanism: release refs are release/vX.Y.Z (branch) and vX.Y.Z (tag) — VERSION only, never client identity. The client->version association lives ONLY in the private docs-site crib (PI-7). This extends the ID-95 PI-7 no-client-literal discipline to the release layer and is subject to the same private denylist ID-95 PI-9 enforces on migrations.

Verify: git for-each-ref over the public repo’s branches+tags greps clean of any client-name/client-domain token (private denylist); no pin ref name identifies a client; no committed file in the public repo maps a client to a version.

PI-6 — public repo cannot answer “which client runs this version?”

Section titled “PI-6 — public repo cannot answer “which client runs this version?””

Mechanism: the public repo holds release artefacts (refs/tags/code) but NOT the client->version association, which is private (PI-7). This layer adds zero ID-68-flip blockers: it is a client-agnostic process/config layer on an already client-agnostic repo.

Verify: no committed artefact in the public repo maps client->version; client identities appear only in the private docs-site crib; git ls-files in the public repo shows no client->version map.

PI-7 — private docs-site crib is the single source of truth for client->version

Section titled “PI-7 — private docs-site crib is the single source of truth for client->version”

Mechanism (new private docs-site artefact): create ONE committed crib in the PRIVATE docs-site holding the client->version table + append-only upgrade history. Location decision (resolves OQ-113-5): co-locate with the existing per-client ops crib at ${KH_PRIVATE_DOCS_DIR}/src/content/docs/runbooks/client-app-deploy.md is the natural single-ops-surface, BUT to keep the per-client pin table append-only and scannable, this TECH places the table in a dedicated sibling ${KH_PRIVATE_DOCS_DIR}/src/content/docs/runbooks/per-client-release-pins.md and cross-links it bidirectionally with client-app-deploy.md. (Single authoritative location; co-located neighbourhood.) Columns (per row, client-keyed): client, Vercel project, pinned release vX.Y.Z, Supabase project ref, pinned on-prem COCOINDEX_IMAGE_TAG (where applicable), last-upgrade date; plus a per-client append-only upgrade-history list (from->to version, date, migration delta, ordering taken). The cut/upgrade runbook updates 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 Production Branch + Coolify COCOINDEX_IMAGE_TAG) matches its crib row; a repoint without a same-step crib update is a runbook-checklist failure; the crib lives in the private docs-site (git ls-files in the public repo shows none).

PI-8 — served behaviour fully determined by pinned version + own tenant_config

Section titled “PI-8 — served behaviour fully determined by pinned version + own tenant_config”

Mechanism (no change — assertion grounded in build-time model): code is fixed by the pinned release/vX.Y.Z (Vercel build of that ref) and config/branding by the client’s own Supabase tenant_config baked at build time (lib/client-config.ts:604-627, loadBranding reads CLIENT_BRANDING_MAP only, no runtime Supabase query). No third version axis; no runtime version negotiation. The optional NEXT_PUBLIC_RELEASE_VERSION stamp (PI-1) makes the code-version axis observable.

Verify: a client deploy at a known pin renders branding hydrated from its own tenant_config (ID-95 PI-11 AC-E3 sequence), code matching the pinned vX.Y.Z; no runtime Supabase version query in the resolve path.

PI-9 — release/rename/repoint never silently un-gates production

Section titled “PI-9 — release/rename/repoint never silently un-gates production”

Mechanism (ordered deploy-gate verification — echoes ID-95 {95.10}): the deploy-gate status is the single repo-named "Vercel - knowledge-hub: ci-summary" (ci.yml:1367, posted via vercel/repository-dispatch, if: always(), client-agnostic). Per the resolved OQ-113-1, each per-client Vercel project opts in to that SAME named check via its dashboard (Settings -> Git -> required checks); the per-project opt-in is a runbook step, NOT a client-named check (PI-7-clean). Because the check name is repo-named and shared, N per-client projects gate on one CI dispatch — but a project rename or required-check mis-name on EITHER side (the CI dispatch name: OR the Vercel dashboard required-check string) silently un-gates that project. The runbook makes the deploy-gate verification an ORDERED step that precedes any rename/repoint.

Verify: after any release/rename/repoint the named required-check still gates the client project’s production deploys on BOTH the CI dispatch side (ci.yml:1367 unchanged) and the Vercel dashboard side; the runbook records the deploy-gate verification as a completed step strictly BEFORE any rename/repoint; a deliberately mismatched check name blocks the prod deploy (not silently allowed through).

PI-10 — same semver pins the on-prem/Coolify pipeline image (bl-301 tie-in)

Section titled “PI-10 — same semver pins the on-prem/Coolify pipeline image (bl-301 tie-in)”

Mechanism (CI workflow delta + Coolify setting):

  1. Add a vX.Y.Z image tag at release cut. Today onprem-deploy.yml:239-247 publishes ${IMAGE}:${TAG} where TAG = sha-${{ github.sha }} (:108). The versioning extension tags the SAME built image with vX.Y.Z in addition to the sha- tag. Two implementation options (TECH names both; the cut runbook picks):
    • (a) Re-tag-and-push at cut (no rebuild): on a release-tag-push event, docker pull ${IMAGE}:sha-<sha> (the already-published image for the tagged commit), docker tag it ${IMAGE}:vX.Y.Z, docker push. Preferred — reuses the content-addressable image; the semver tag is a pure alias of the known-good sha- image.
    • (b) Add ${IMAGE}:vX.Y.Z to the pack build ... --publish tag set when the build runs on a release ref. Rejected as primary because the existing build is main/staging push-keyed (onprem-deploy.yml:92-96), not release-keyed; (a) avoids re-architecting the trigger.
  2. Pin Coolify COCOINDEX_IMAGE_TAG to vX.Y.Z. A client’s (and, per bl-301, the platform’s own) Coolify deployment sets COCOINDEX_IMAGE_TAG to a deliberate vX.Y.Z (the same PATCH-in-place mechanism at onprem-deploy.yml:343-367, but to a semver tag, set as a deliberate operator/runbook step rather than the auto-bump-to-sha-<sha> that the main-push path performs). The upgrade flow is then identical in SHAPE to Vercel: a deliberate repoint to a newer version, preserving the prior-tag rollback ergonomics (:104-106).

Verify: a release cut produces a vX.Y.Z-tagged pipeline image as well as the sha- tag (both pullable from ghcr.io); a client’s COCOINDEX_IMAGE_TAG resolves to a vX.Y.Z semver (not a rolling main sha) for any client whose contract includes ingestion; the prior-tag rollback still works (repoint Coolify to a prior vX.Y.Z, no rebuild).

PI-11 — branding/config from the client’s own tenant_config regardless of version

Section titled “PI-11 — branding/config from the client’s own tenant_config regardless of version”

Mechanism (no change — non-regression assertion): the version-pinning layer touches refs/tags/Vercel-settings/Coolify-tags only; it does NOT change the branding source path. Every pinned version still hydrates branding at build time from the client’s own Supabase tenant_config (scripts/fetch-client-branding.ts -> lib/branding/clients/<id>.json -> generate-client-branding-map.ts -> lib/client-config.ts loadBranding), fail-closed (ID-95 PI-10/PI-11). No version introduces a per-client code path, a committed branding artefact, or a runtime branding query.

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; AC-E3 branded-render check passes); the pinning layer never touches the branding source path.

PI-12 — every client upgrade auditable from the crib’s append-only history

Section titled “PI-12 — every client upgrade auditable from the crib’s append-only history”

Mechanism: the crib upgrade-history (PI-7) is append-only — each upgrade appends a row (from->to version, date, migration delta, ordering taken per OQ-113-2). The cut/upgrade runbook’s final step appends this row in the same act as the repoint, so schema/code pairing is never left silently skewed. “Which client moved to which version, and when” is one crib lookup.

Verify: the crib upgrade history is append-only (no row overwritten); each upgrade row carries from/to version + date + migration delta + ordering; an upgrade leaving schema/code skewed without a recorded remediation is a runbook-checklist failure.


The cut / promote runbook (TECH-formalised; lands in the docs-site)

Section titled “The cut / promote runbook (TECH-formalised; lands in the docs-site)”

The runbook below is the operational deliverable (lands at runbooks/per-client-release-pins.md alongside the crib, or client-app-deploy.md — one ops surface). It formalises RESEARCH section 5 with the resolved orderings baked in.

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

Section titled “Cut a release (platform owner, at known-good main)”
  1. Verify main known-good: the "Vercel - knowledge-hub: ci-summary" gate is green at the target sha (PI-1 precondition).
  2. Choose the semver bump: MAJOR = breaking client-facing OR non-rollback-safe migration (per OQ-113-3); MINOR = additive/back-compatible; PATCH = fix-only. Record rationale in the changelog crib.
  3. git tag vX.Y.Z <known-good-main-sha> and push the tag (immutable source of truth, PI-1).
  4. Create/fast-forward release/vX.Y.Z to the tag commit; push (Vercel handle, FF-only, PI-3).
  5. If the release touches the ingestion pipeline: re-tag the sha-<sha> image as vX.Y.Z and push (PI-10 option (a)).
  6. Record the release in the changelog crib (version, sha, date, notable changes, migration deltas, rollback-safety classification per OQ-113-3).

Upgrade a client (deliberate; migrate-DB-first per OQ-113-2)

Section titled “Upgrade a client (deliberate; migrate-DB-first per OQ-113-2)”
  1. Pre-flight: review migration deltas between the client’s current pin and target vX.Y.Z; confirm rollback-safety classification (OQ-113-3); confirm the migration set is old-code-tolerant (OQ-113-7) or flag the upgrade irreversible.
  2. Migrate the client’s Supabase to the target schema FIRST (OQ-113-2). Verify schema parity. The OLD deploy keeps serving correctly during this window (compatibility window, not downtime).
  3. Deploy-gate verification BEFORE any repoint/rename (PI-9, ordered): confirm the ci-summary required check is intact on BOTH the CI dispatch side and the client project’s Vercel dashboard side.
  4. Repoint the client’s Vercel Production Branch to release/vX.Y.Z; trigger redeploy (push to the release branch, or Vercel redeploy).
  5. If the client runs the ingestion pipeline: repoint Coolify COCOINDEX_IMAGE_TAG to vX.Y.Z (PI-10) + Coolify deploy.
  6. Verify: deploy-gate passed for the release branch; smoke-test the client URL (branded render, AC-E3; optional NEXT_PUBLIC_RELEASE_VERSION matches vX.Y.Z).
  7. Update the crib row in the SAME step (PI-7/PI-12): new pinned version + upgrade date, appending to the append-only history (from->to, date, migration delta, ordering taken).
  8. Rollback (if needed): code-repoint to prior release/vX.Y.(Z-1) + redeploy (and Coolify prior-tag) for additive releases; roll FORWARD to a fix PATCH across a forward-only migration (OQ-113-3). Record the remediation in the crib.

The verification surface is git/Vercel/Coolify/docs-site state and operator runbook behaviour, not a runtime test suite (no new application logic ships except the optional version stamp). Each PI maps to a concrete, mostly operator-executable check; bun run test covers only the optional stamp if added.

InvariantVerification
PI-1git merge-base --is-ancestor <tag> main; tag immutability (no retag in git reflog/changelog); cut-runbook records green ci-summary.
PI-2every vX.Y.Z resolves to a main ancestor; optional OQ-113-9 CI guard fails a non-main tag push.
PI-3no-op main merge triggers no client redeploy; each Vercel project Production Branch = one release/vX.Y.Z; none tracks main.
PI-4cutting v(Z+1) leaves existing clients on their pins; no automated-upgrade mechanism in the design.
PI-5git for-each-ref greps clean of client tokens (private denylist); no client->version file in the public repo.
PI-6git ls-files (public) shows no client->version map; client identities only in private crib.
PI-7exactly one crib location; live Vercel + Coolify pins match crib rows; repoint-without-crib-update is a checklist failure.
PI-8branded render at a known pin (AC-E3); code matches vX.Y.Z; no runtime Supabase version query (grep resolve path).
PI-9post-rename/repoint the named check gates on both sides; a mismatched name BLOCKS prod (negative test on a throwaway project at standup).
PI-10release cut produces both vX.Y.Z + sha- tags (pullable); client COCOINDEX_IMAGE_TAG is a semver; prior-tag rollback works.
PI-11branded render via ID-95 build-time fetch at any pin; no client-branding/ dir, no if (client === 'x'); AC-E3 passes.
PI-12crib history append-only (no overwrite); each row has from/to + date + delta + ordering.

If the optional NEXT_PUBLIC_RELEASE_VERSION stamp (OQ-113-8) ships: add a behaviour-first test (reference test-philosophy.md) asserting the version endpoint returns the build-injected vX.Y.Z; run via bun run test (never bun test).


  • Deploy-gate silent un-gate (PI-9, the headline hazard). A Vercel project rename or a dashboard required-check mis-name silently un-gates prod. Mitigation: ordered deploy-gate verification BEFORE any rename/repoint (runbook step 3), echoing ID-95 {95.10}; a negative test at standup (deliberately mismatched name must BLOCK).
  • Schema/code skew on the wrong upgrade ordering (OQ-113-2). Repoint-first against an un-migrated DB is a hard runtime failure with no graceful path (build-time-baked config). Mitigation: migrate-DB-first is the enforced runbook ordering; the OLD deploy serves during the compatibility window.
  • Forward-only migration with no clean rollback (OQ-113-3 / OQ-113-7). A destructive migration past which code-rollback is unsafe. Mitigation: gate such migrations to MAJOR, flag irreversible in the crib, default to additive/old-code-tolerant migrations; roll FORWARD to a fix PATCH rather than rolling the schema back. Snapshot-restore is a last-resort escape hatch ONLY, never the designed path.
  • Tag/branch drift (vX.Y.Z tag vs release/vX.Y.Z branch). Two refs at one commit can diverge. Mitigation: the branch is fast-forward-only to the tag (cut runbook step 4); the tag is the source of truth.
  • bl-301 sequencing. If the platform-owned cocoindex pipeline (bl-301) stands up before this convention is set, it pins a rolling main sha and must be re-pinned. Mitigation: ship the convention (this Task) before bl-301 standup; bl-301 adopts semver-tag pinning from day one. No hard ordering, but noted in the crib.

Open decisions for ratification (residual + new)

Section titled “Open decisions for ratification (residual + new)”

OQ-113-1 resolved upstream (Option A, Liam-ratified). OQ-113-2/OQ-113-3 proposed above (operator-ratifiable). The following are carried/new for Liam:

  1. OQ-113-4 (carried, lower priority) — upgrade cadence / staleness signal. Purely platform-owner-initiated, or a client-facing “N versions behind” signal? Affects whether the crib needs a “latest available” column + staleness policy. Default proposed: platform-owner-initiated only; no staleness column at v1.
  2. OQ-113-5 RESOLVED (proposed) — crib location. Dedicated runbooks/per-client-release-pins.md, bidirectionally cross-linked with the existing runbooks/client-app-deploy.md. Operator may instead fold it into client-app-deploy.md as one surface; PI-7 requires only one authoritative location. Operator confirms.
  3. OQ-113-6 (carried) — pre-release / staging channel. Do clients ever pin to a pre-release (release/vX.Y.Z-rc.1) for UAT before a GA upgrade? Affects the naming convention (semver pre-release suffix). Default proposed: GA-only at v1; the -rc.N suffix is reserved (semver-compatible) for future use, no machinery built now.
  4. OQ-113-7 (NEW) — old-code-tolerance contract for migrate-first. The migrate-DB-first safety argument (OQ-113-2) assumes the migration is old-code-tolerant (additive). Should this be a hard release-classification gate (every release’s migration set is classified “old-code-tolerant: yes/no” at cut, with “no” forcing a coordinated cutover + MAJOR)? Proposed: yes — classify at cut (runbook step 6); couples to OQ-113-3.
  5. OQ-113-8 (NEW, optional) — build-time NEXT_PUBLIC_RELEASE_VERSION stamp. Ship the optional version stamp (the only application-source change) so a deploy self-reports its vX.Y.Z on a health/version endpoint? Proposed: nice-to-have, not required (the Vercel dashboard Production Branch already answers “what version”); defer to a follow-up unless Liam wants in-app observability now.
  6. OQ-113-9 (NEW, low priority) — CI guard on release-tag pushes. Add a workflow asserting a pushed vX.Y.Z tag is a main ancestor (PI-2 hard enforcement) vs relying on the runbook check? Proposed: runbook check suffices at current scale; add the guard when release frequency rises.