Skip to content

PRODUCT -- Per-client deployment/config approach (ID-95.2)

PRODUCT — Per-client deployment/config approach (ID-95.2)

Section titled “PRODUCT — Per-client deployment/config approach (ID-95.2)”

Task: ID-95 — Per-client Supabase/infra topology + platform-vs-client ownership boundary. Subtask: {95.2} PRODUCT. Author date: 11/06/2026. Status: SPEC — behaviour formalisation. The topology is DE-RISKED and NAMED in {95.1} RESEARCH.md (3-tier ladder; one canonical migration set; config-as-data). This PRODUCT pins the behaviour of the per-client deployment/configuration approach toward the private->public cutover — it does NOT re-open the topology. A {95.3} TECH may follow for the fan-out / overlay mechanics; this document stays behaviour-first, implementation-light.

Revision 11/06/2026 (post-ratification feedback). Liam ratified this PRODUCT subject to his OQ-dossier feedback. Applied in this revision: the private branding home moved OFF the docs-site repo (rejected as an option — docs-site holds platform docs + dev-workflow content, never branding artefacts) to a tenant_config row + branding Storage bucket in the client’s OWN Supabase project (PI-10/PI-11/PI-12 rewritten; former Open decision 1 DISSOLVED — no private-repo read exists anywhere in the deploy path); platform project named knowledge-hub-platform (decision 2 resolved); demo-bootstrap-spec.md archives now (decision 3 resolved); the MCP plugin URL sweep is carved out to ID-71 ai-tooling (PI-13/PI-14 — no client uses the MCP tools today); PI-15 strengthened with the verified taxonomy-sync broken-state finding; and the Coolify-server premise corrected — the existing deploy/coolify/** ingestion-pipeline setup was built FOR THE FIRST CLIENT (re-points to the client’s own domain at handover, like Vercel and Supabase), so the platform needs its OWN Coolify deployment, framed under the Tier-3 BYOC option with build timing out of scope (PI-6a / PI-3, ratified subject to this clarification). The tenant_config pivot is now RATIFIED (Open decision 0 RESOLVED, subject to the Coolify-server clarification); new Open decisions 5-6 are DEFERRED to {95.3} TECH (owner: “TECH to propose” on both).

Fresh-Planner artefact (Q-PLANNER-2). Every invariant is numbered PI-N with a one-line verification means so a Checker can gate it against acceptance criteria. UK English throughout (DD/MM/YYYY, “colour”, “organisation”). Cross-references are by path; this document does NOT re-dump RESEARCH content — it cites it.


Knowledge Hub serves each client from its own isolated deployment: one public application repository builds N independent Vercel projects (knowledge-hub-{client}), each pointed at a dedicated managed Supabase project (Tier 1) — with Coolify-hosted BYOC (Tier 3) as the named long-term sovereignty option. A client is stood up by setting per-project configuration (env, branding data in the client’s own DB, signup-policy data row) — never by committing a client literal to the public repo. This PRODUCT pins the deployment topology, the config-as-data invariant, the branding config-as-data seam, the no-deploy-target-literal invariant, and the client-content seam-flip trigger that together let the first client (Phew) be stood up without precluding multi-tenancy, ahead of the public-repo flip.

The platform must stand up clients without (a) any client’s data touching any other client’s, (b) any client literal or secret entering the soon-to-be-public migration/app history, or (c) foreclosing the multi-client topology that follows the first single-tenant client. The current shape is single-knob, not multi-tenant: branding is resolved at build from files committed to the public repo, the production self-URL is hardcoded across 17+ call sites, and there is no defined seam between platform-owned canonical artefacts and client-instance-specific ones. RESEARCH.md settled the topology; this spec formalises the behaviour an implementer and a Checker can hold the cutover to.

Context (code-intelligence orientation + grounding sources)

Section titled “Context (code-intelligence orientation + grounding sources)”

Code-intel orientation (gitnexus, cited not paraphrased; index: knowledge-hub):

  • gitnexus_query({query: 'client branding deploy overlay'}) -> top execution flow proc_276_rootlayout “RootLayout -> BuildBrandCss” (priority 0.113, 3 steps): RootLayout (app/layout.tsx:47-95, module Shell) -> buildBrandCss (lib/client-config.ts:654-667, module Shell). Standalone definitions returned: renderClientBrandingMap (scripts/generate-client-branding-map.ts:45-86), main (scripts/generate-client-branding-map.ts:88-96), loadBranding (lib/client-config.ts:603-626), validateBrandingContrast (lib/client-config.ts:497-541), BrandLogo (components/shell/brand-logo.tsx:12-47).
  • gitnexus_context({name: 'loadBranding'}) -> verdict level LOW (no upstream callers beyond the module file lib/client-config.ts; incoming calls = 1 = the module init that computes BRANDING once; outgoing calls = ClientLogger.warn, validateBrandingContrast; process participation = none, so it is a leaf in the orientation graph). The single module-init caller confirms the dossier’s “BRANDING computed once at module init” finding (id95-oq-dossier-2026-06-11.md:20).
  • gitnexus_context({name: 'renderClientBrandingMap'}) -> incoming caller count = 1 (main in the same script); no other callers — confirming the codegen seam is invoked only by the prebuild entry point, i.e. the overlay can add <id>.json to the glob input without touching any source the loader imports.

These three findings ground PI-10/PI-11 (branding = data-add, mechanism preserved): the branding map is GENERATED from a directory glob (renderClientBrandingMap), and the loader falls back to default (loadBranding LOW-verdict leaf), so a per-client deploy supplies branding by adding files, never by patching code.

Grounding sources read in full for this spec (cite for the Checker):

  • {95.1} RESEARCH.md (ratified direction: 3-tier ladder §5.4; one canonical migration set
    • catalog/fan-out/version-table §9; config-as-data §3.5; do-NOT-split-migrations §4).
  • id95-supporting-info.mdLiam’s supporting info for this session (mandatory input): OQ-95-6 (Vercel rename + kh.aisolutionhub.co.uk interim platform domain), Point-4 (client-content seam), OQ-95-2 (preview-branch recut + pre-re-ingest per-client project), OQ-95-5 (ID-69/ID-71 corpus-structure context), demo-bootstrap-spec.md staleness note.
  • id95-oq-dossier-2026-06-11.md — this session’s 4-OQ resolution dossier (file:line-cited). Its four PRODUCT-IMPLICATION blocks are requirements on this spec; its Residual-risks section feeds the edge cases below.
  • runbooks/client-app-deploy.md — the operational companion (knob table §2, hard sequence §3d, fail-closed signup gate §3c). This spec must stay consistent with it.
  • ID-68 DEPLOY-OVERLAY-DESIGN.md (§4 credential claim, §6 acceptance) + ID-68 PRODUCT.md:330-332 (AC-E3). Cross-referenced, not duplicated.

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.

A. Deployment topology — one public repo, N Vercel projects

Section titled “A. Deployment topology — one public repo, N Vercel projects”
  1. PI-1 (Vercel project = unit of client app isolation). Each client runs in its own Vercel project named knowledge-hub-{client}, built from the single public application repo, configured exclusively via per-project build-time env plus the branding overlay — never via a per-client repo branch and never via in-repo per-client code. Adding a client adds a Vercel project; it does not add a code path. Verify: two client projects build the identical HEAD commit, differing only in per-project env + overlaid branding files; no if (client === 'x') branch exists in app source (grep clean).

  2. PI-2 (per-project build-time configuration set). A client project is fully specified by its build-time env: NEXT_PUBLIC_CLIENT_ID (selects the branding map entry; REQUIRED, missing aborts the build — the S196 guard), the client Supabase URL + keys, and APP_URL (the client’s own domain), plus the per-client branding data (the tenant_config row + branding-bucket assets in the client’s own Supabase project — PI-10). No other per-client knob exists in the app layer. Verify: the knob set matches client-app-deploy.md:40-47 (note: the Auth-hook and custom-domain rows in that table are operational deployment steps, not app-layer build knobs — PI-2 covers only the env + branding-data subset); a build with NEXT_PUBLIC_CLIENT_ID unset fails closed (per lib/env-client.ts REQUIRED check), and no per-client value is read from any source other than env or the client’s own tenant_config/branding bucket.

  3. PI-3 (platform dev/demo deploy is a first-class, separate project). The platform operates its OWN Vercel project (knowledge-hub-platform — resolved, Open decisions 2) for development/demo at the interim domain kh.aisolutionhub.co.uk (APP_URL=https://kh.aisolutionhub.co.uk), with NEXT_PUBLIC_CLIENT_ID=default (neutral branding) and its OWN dedicated Supabase project. The platform dev/demo deployment NEVER co-tenants with client data. This completes a platform-owns-its-infra triad: the platform’s own Vercel project (here), its own Supabase project (here), and its own Coolify pipeline deployment (PI-6a, named-but-unbuilt) — each paralleling the client-owned equivalent. Sequencing: first-client readiness is PRIMARY; provisioning the platform project is secondary and does not gate the client cutover. Verify: the platform project’s Supabase URL/keys differ from every client project’s; the platform deploy renders default branding; no client row is reachable from the platform instance; the platform’s own Coolify deployment is recorded as named-but-unbuilt (PI-6a).

  4. PI-4 (Vercel project rename — current knowledge-hub becomes knowledge-hub-phew). The existing knowledge-hub Vercel project is renamed to knowledge-hub-phew (it is the first-client project). The platform dev/demo project is created fresh as knowledge-hub-platform (resolved, Open decisions 2). Rename preserves the project ID, env vars, custom domain (kh.phew.org.uk), and git integration; only the auto-generated *.vercel.app domain changes — which is why the no-deploy-target-literal sweep (PI-13) MUST precede the rename. Verify: post-rename the project ID/env/custom-domain/git-link are unchanged; the only functional breakage class is the old auto-domain, fully covered by PI-12’s sweep having run first.

B. Database topology — physical isolation, no contamination

Section titled “B. Database topology — physical isolation, no contamination”
  1. PI-5 (Tier 1 — managed Supabase project-per-client). Each client’s content lives in a dedicated managed Supabase project (a dedicated Postgres instance), provisioned in a region supplied as a per-client provisioning input. The region is treated as IMMUTABLE after project creation: residency is a contract input baked at provisioning, not a later change. The UK default is eu-west-2 (London). Verify: each client maps to exactly one Supabase project ref; the provisioning record carries an explicit region field; no flow attempts to change a project’s region post creation (it is documented as create-new-and-migrate only).

  2. PI-6 (no cross-client contamination by physical isolation). No client deployment can read or write another client’s data, because isolation is physical (separate Supabase instances), not logical (shared instance + RLS). Shared-instance + RLS is acceptable ONLY for the platform dev/demo instance, which holds no client data. Verify: from client A’s deployment, no credential, URL, or query path resolves client B’s data; the verification checklist item “no other client’s assets/data are reachable” (client-app-deploy.md:126-127) passes for every client.

    PI-6a (Tier 3 BYOC is the named long-term sovereignty option; build timing out of scope). Coolify-hosted BYOC (one Coolify server per client; RESEARCH §8/§10) is the named option for clients whose contract mandates data in their own tenancy. WHEN to build the BYOC tier is explicitly a Liam sequencing call and is OUT OF SCOPE for this PRODUCT — the spec only requires that the interim Tier-1 topology not preclude it (one canonical migration set + one one-directional push mechanism serve both eras). The existing Coolify ingestion-pipeline setup (deploy/coolify/**, routing kh-pipeline.aisolutionhub.co.uk) was built FOR THE FIRST CLIENT, not as a platform-generic host: the aisolutionhub.co.uk domain was used ONLY as an interim measure to complete DNS setup and re-points to the client’s own domain at handover — exactly like the Vercel project rename (PI-4) and the per-client Supabase project (PI-5). The platform-vs-client ownership boundary therefore applies to the Coolify/pipeline surface too: the PLATFORM requires its OWN Coolify deployment (a platform-version of the Coolify pipeline setup), paralleling the platform’s own Vercel project (PI-3) and its own Supabase project. WHEN to build the platform’s own Coolify instance is OUT OF SCOPE for this PRODUCT (same out-of-scope-for-build stance as the BYOC tier and the Non-goals BYOC line) — it is a Liam sequencing call. Verify: BYOC appears only as a named future tier with no build-timing commitment; no Tier-1 invariant here contradicts the BYOC delivery channel described in RESEARCH §8.2/§9.4; the spec records that the existing deploy/coolify/** setup is the client’s (re-points at handover) and that the platform’s own Coolify deployment is named-but-unbuilt with no build-timing commitment.

C. Config-as-data — migrations stay public, client values are data

Section titled “C. Config-as-data — migrations stay public, client values are data”
  1. PI-7 (no client literal / secret / environment-specific value in any migration). No migration in supabase/migrations/** contains a client name, a client domain, a secret, or any environment-specific value. Per-client values are exclusively data rows, env, or Supabase auth settings. public.signup_policy is the canonical worked example: the table and hook are schema (public, generic); the allowed domain is a data row INSERTed out-of-band per instance. Verify: a grep of supabase/migrations/** for client-name tokens / domain literals / credentials returns zero (the three current Phew-bearing files are purge targets, not new regressions); signup_policy migration carries no domain literal.

  2. PI-8 (migrations STAY PUBLIC — settled decision with a named enforcement set). Platform migrations are NOT split into a private repo (RESEARCH §4; dossier OQ-2). The privacy property is delivered by git-history purge, not repo topology, and types-gen reads the LIVE DB, so published migrations expose only generic DDL already in database.types.ts. The standing enforcement set is: migration-revoke-guard.yml (PR-blocking REVOKE lint on migration diffs), supabase-advisors.yml (nightly security/perf advisor diff vs committed baseline), and the filter-repo history purge as the one-time backstop for the residual Phew-bearing files. Verify: no separate migrations repo exists; the three named CI/enforcement mechanisms are live and named in the spec; the cutover does not move migration-revoke-guard out of the public repo (which would breach Inv 30 / lose the gate).

  3. PI-9 (CI client-name denylist lint on supabase/migrations/** — new invariant). A PR-blocking CI lint rejects any future migration introducing a client-name token (or client-domain literal) into supabase/migrations/**, so a config-as-data regression is blocked at PR time rather than discovered at purge time. Verify: a test PR adding a client-name token to a migration fails CI; a generic migration passes; the denylist token set is maintained alongside the redaction map.

D. Branding seam — config-as-data in the client’s own Supabase

Section titled “D. Branding seam — config-as-data in the client’s own Supabase”
  1. PI-10 (private branding/config home = the client’s OWN Supabase project — tenant_config + branding bucket). Per-client branding lives as DATA in the client’s own Supabase project: a generic public.tenant_config row holding the branding JSON document, plus a private branding Storage bucket holding the asset set (logo/favicon files). NO branding artefact lives in any repo — not the public app repo, not the private docs-site (which holds platform documentation plus development-workflow content such as specs and ledgers, but no branding artefact), not a per-client repo. The tenant_config table is generic schema shipped in the canonical public migration set (PI-7/PI-9 compliant: no client literal), RLS deny-all with REVOKE ALL FROM anon, authenticated, public (service-role access only) — the same config-as-data template as signup_policy. The per-client ops crib (signup domain, Supabase ref, Vercel project name, custom domain) remains documentation in the docs-site runbook tree. This SUPERSEDES both DEPLOY-OVERLAY-DESIGN.md §4’s knowledge-hub-internal/client-overlays/ reference AND the earlier client-branding/<id>/ docs-site home (rejected 11/06/2026: docs-site holds no branding artefact). Verify: no client-branding/ dir exists in any repo; the tenant_config migration greps clean of client tokens (PI-9 lint); the Phew deploy resolves branding from its own Supabase project; git ls-files in the docs-site shows no branding JSON/assets.

  2. PI-11 (branding hydration is a generic build-time data fetch; mechanism preserved — AC-E3). A client deploy hydrates branding via a generic, committed prebuild script that runs BEFORE generate:branding: it reads the tenant_config row and downloads the branding-bucket assets into lib/branding/clients/<id>.json + public/clients/<id>/* using the Supabase URL/keys ALREADY present in the project’s env — then the existing codegen glob, loader, and build-time schema/contrast validation run unchanged (Option 2 codegen retained, ID-68 DEPLOY-OVERLAY-DESIGN.md §3). FAIL-CLOSED both ways: a client build (NEXT_PUBLIC_CLIENT_ID != default) whose fetch yields no branding row FAILS the build (S196 analogue — never silently ship a default-branded client deploy); with default / absent creds the fetch no-ops and the control build resolves default and stays green. The public repo HEAD shows git ls-files zero rows for lib/branding/clients/phew.json and public/clients/phew/**. No private-repo read exists anywhere in the deploy path — the former GitHub-Actions-vs-Vercel-token credential fork is DISSOLVED (Open decisions 1). Verify (AC-E3 sequence, gated checklist): (1) rehearsal/preview deploy with the tracked files removed -> page renders Phew branding (hydrated from tenant_config); (2) git rm the two paths on the public HEAD; (3) production redeploy -> re-verify branded; (4) no-row/no-creds control build resolves default green (ID-68 DEPLOY-OVERLAY-DESIGN.md §6 (acceptance criteria 1-3)). Branding colours/states use Warm Meridian semantic tokens, never raw colours, and never colour alone for meaning.

  3. PI-12 (new client = data drop + env set, zero repo commits anywhere). Standing up a new client is: INSERT the tenant_config row + upload the branding assets to the client project’s branding bucket, write the ops crib, create the Vercel project with its env, provision the Supabase project, INSERT the signup_policy row, wire the auth hook, add the domain, deploy. None of these steps commits a client literal to ANY repo. Verify: the full new-client run (client-app-deploy.md §6) produces zero diffs to the public application repo AND zero per-client files in any private repo; the only client-identifying artefacts created live as data in the client’s own Supabase project or as Supabase/Vercel configuration.

E. No deploy-target literal — self-URLs derive from env

Section titled “E. No deploy-target literal — self-URLs derive from env”
  1. PI-13 (no hardcoded deploy-target self-URL in repo code or CI). No runtime code, CI workflow, or generated bundle hardcodes a deploy-target self-URL. All self-URLs derive from NEXT_PUBLIC_APP_URL (per-project env) — the established in-repo precedent is components/settings/mcp-url.ts (getMcpUrl() derives the MCP endpoint from NEXT_PUBLIC_APP_URL with a window.location.origin fallback; zero hardcoded host). The current knowledge-hub-seven-kappa.vercel.app hardcoding (17+ references including the taxonomy-sync callback and the ci.yml deploy-gate check name) is the enforcement DEBT this invariant retires. CARVE-OUT: the MCP plugin surface (.claude/plugins/knowledge-hub/1.0.0/**, 10 files, and the generated lib/mcp/plugin-bundle.ts) is owned by ID-71 ai-tooling — no client currently uses the MCP tools, so the plugin-side URL derivation neither gates this sweep nor the rename. Verify: grep of app source and .github/workflows/** for the literal knowledge-hub-seven-kappa returns zero outside the plugin tree; the plugin tree/bundle residue is tracked under ID-71 (regenerate via bun run build:plugin when ID-71 lands its derivation).

  2. PI-14 (the sweep ORDERING precedes the Vercel rename). The PI-13 sweep of runtime code + CI MUST complete (and the Vercel deploy-gate check name be verified on both sides) BEFORE the knowledge-hub -> knowledge-hub-phew rename. Renaming first silently breaks the auto-domain that the literals point at, including the deploy-gate that guards prod. The MCP plugin/bundle references are EXEMPT from this gate (ID-71 carve-out; no live client connectors exist, so accepted breakage on rename = none). Verify: the cutover sequence records the sweep + deploy-gate check-name verification as completed steps with the rename strictly after them; the plugin residue is logged under ID-71, not silently forgotten.

  3. PI-15 (callback-style workflows take their targets from per-environment config; taxonomy-sync is named-target and transitional). Any workflow that calls back into “an app” takes its target app URL from per-environment configuration, because under N client projects “the prod app” is no longer singular. For taxonomy-sync specifically: (i) it names which single environment/DB it reads and which app it calls back (the platform control plane, never a client project); (ii) it is currently BROKEN — since the {68.23} classification-prompt relocation (08/06/2026) the workflow fails without KH_PRIVATE_DOCS_DIR and structurally cannot commit the relocated prompt (it only has contents: write on the public repo); the automation shell (workflow + repository_dispatch + HMAC callback + drift banner + taxonomy_sync_state hash machinery) is retired or repaired as an EXPLICIT decision, with operator-run bun run sync:taxonomy as the de facto interim; (iii) it is TRANSITIONAL: repo-committed artefacts derived from a tenant DB (taxonomy snapshot, plugin vocabulary, classification prompt) MUST NOT acquire new consumers, and client-provenance taxonomy rows MUST NOT be committed to the public repo. Baseline vocabulary propagation to client DBs belongs to the PI-18 fan-out; per-tenant derived artefacts move to point-of-use generation against the target client DB (mechanics: {95.3} TECH / ID-71). Verify: no hardcoded host in taxonomy-sync.yml; the workflow either runs green end-to-end with the private-docs bridge resolved or is retired in favour of operator-run codegen; no NEW consumer of taxonomy_snapshot.json/plugin vocab/classification prompt is added; client-provenance rows stop being committed to the public repo.

  1. PI-16 (private per-client provisioning repo is created at Tier-3 BYOC stand-up, or on a client config-access/audit grant — not before). While the platform operates every deployment, the requirement is private-vs-public separation, not client-vs-client repo separation: per-client artefacts live as DATA in the client’s own Supabase project (PI-10) — at Tier 1 no per-client repo and no private overlay directory exists at all. A dedicated private per-client PROVISIONING repo is created exactly when a client becomes a party to its artefacts — at Tier-3 BYOC stand-up for that client (the Coolify GitOps channel structurally requires a per-client repo), or earlier if a client contract grants that client access/audit over its config, or when per-client artefacts outgrow branding (seed data, corpus manifests, compose files). Not before. Verify: no per-client provisioning repo exists for a Tier-1 platform-operated client; the trigger conditions are named verbatim so a Checker can confirm none is met before the repo is created.

  2. PI-17 (demo-bootstrap-spec.md is superseded — archive + pointer). The stale demo-bootstrap-spec.md (31/03/2026, pre-dates the config-table signup design, the overlay seam, and ID-95 topology) is archived to knowledge-hub-archive and left with a pointer to this PRODUCT + client-app-deploy.md, so the stale GUC-era onboarding flow cannot be followed by mistake. (Resolved 11/06/2026: archive NOW; any demo-seed mining happens from the archive copy if the platform dev/demo design wants it.) Verify: demo-bootstrap-spec.md is no longer a live spec in the docs-site specs tree; a pointer to the superseding artefacts exists at its former location or in the archive.

G. Canonical-content propagation (behaviour at PRODUCT altitude)

Section titled “G. Canonical-content propagation (behaviour at PRODUCT altitude)”
  1. PI-18 (one-directional propagation — platform never reads client data back). Platform-owned canonical knowledge (reference content, taxonomy, catalogue requirements) reaches isolated client DBs by ONE-DIRECTIONAL periodic push (platform -> client upsert by stable key, with tombstone deletes) plus API-layer serving for content that need not live in the client DB. The platform “main” DB is the control-plane canonical SOURCE the platform curates — NOT a live shared store every client reads from, and NOT a sink that reads client data back. Verify: propagation flows are platform->client only; no flow reads client content into the platform DB; the mechanism reuses the same catalog (client -> DB URL) + fan-out worker

    • per-client version table that delivers schema migrations (RESEARCH §9.4).
  2. PI-19 (no standing cross-DB dependency for regulated clients). For a hard-isolation (e.g. safeguarding) client, canonical-content propagation introduces NO standing cross-DB dependency: postgres_fdw and logical replication are excluded for regulated clients (both break the moment the DB sits in a client VPC). The same one-way push mechanism serves both eras — a direct DB connection while platform-hosted (Tier 1), and a git/GitOps-delivered versioned payload once the DB is in the client VPC (Tier 3) — with no bespoke cross-DB sync engine. Verify: no postgres_fdw foreign server or logical-replication subscription is created against a regulated client DB; the propagation design names the per-era transport (DB-direct interim, GitOps BYOC) without a standing inbound connection into a client VPC.

Mechanics (worker, version table, payload format) belong to a future {95.3} TECH; the invariants above are the behaviour the TECH must satisfy.

  1. PI-20 (fail-closed signup gate; allowed domain is per-instance data). Every client Supabase enforces signup via the generic hook_restrict_signup_to_allowed_domain reading public.signup_policy, which is FAIL-CLOSED: with no row / empty domain, every signup is rejected. The allowed domain is set out-of-band as a data row, never in migration history. Because signup_policy is data (PI-7), it must be re-INSERTed after any DB recut/cutover that produces a fresh data state. The same property generalises to a PER-INSTANCE RE-SEED MANIFEST: the signup_policy row, the tenant_config row, and the branding-bucket objects (PI-10/PI-11) are all per-instance data and must all be re-seeded after any recut producing fresh data state. Verify: a fresh client DB with no signup_policy row rejects all signups; after the row is inserted, an allowed-domain signup succeeds and an off-domain one is rejected with the 403 message (client-app-deploy.md:120-127); a post-recut DB has the full re-seed manifest applied (signup row + tenant_config + branding bucket), not just the signup row.

  2. PI-21 (per-project cron + region are repo-global — per-client hygiene required). The repo-global vercel.json crons run in EVERY project built from the repo (each client + the platform), each against its own env. This is correct per-client behaviour but requires per-project CRON_SECRET + Supabase env hygiene, and AI-calling crons multiply spend per client. regions: ["lhr1"] is repo-global, so a non-UK-residency client needs a repo-level change (a known constraint, not silently handled). Verify: each client project has its own CRON_SECRET and Supabase env; the spec names the cron-multiplication cost and the repo-global region constraint as accepted/known rather than assuming per-project regions.

  3. PI-22 (live-DB residue is a data-side concern, owned by provisioning). The history purge cleans git, NOT databases: the prod DB still carries seeded client-identifying rows (e.g. the ‘Phew Design Limited’ guide row) and the historical record of the dropped client-named hook. DB-side de-identification of the platform/demo instance is owned by the per-client provisioning process, not by the public-repo flip. Verify: the platform dev/demo instance’s de-identification is tracked as a provisioning step distinct from the git purge; no PRODUCT invariant assumes the purge removes DB rows.

  4. PI-23 (ledger/continuation-prompt client mentions are inside the ID-68 perimeter). Client-identifying mentions outside supabase/migrations/** (e.g. docs/reference/tasks/**, continuation-prompts) are confirmed inside the ID-68 purge/relocation perimeter — OQ-2’s clean verdict is migrations-only, so these are handled by ID-68, not assumed clean here. Verify: the ID-68 purge/relocation perimeter explicitly covers the public-repo ledger/prompt tree; this spec does not claim those paths are already client-free.


Decisions 0-3 were RESOLVED by Liam’s 11/06/2026 feedback (recorded here for the Checker): decision 0 is the revision-level ratification of the tenant_config pivot (ratified subject to the Coolify-server clarification — AC-1/AC-2). Decisions 5-6 are NEW and remain OPEN — Liam deferred both to {95.3} TECH (“TECH to propose” on both).

  1. RESOLVED/RATIFIED (11/06/2026) — tenant_config pivot. The PI-10/PI-11 branding home (tenant_config row + branding bucket in the client’s own Supabase, build-time fetch) was proposed by Liam in the 11/06/2026 feedback and is spec’d here as the primary behaviour after evaluation. Owner verdict (verbatim): “Ratified, but note the coolify server clarification.” The tenant_config home beat per-client-repo and Coolify/VPC homes on credential surface, AC-E3 compatibility, provisioning ergonomics, and the signup_policy precedent. The Coolify/VPC home loses NOT because no client Coolify server exists — it does (the existing deploy/coolify/** setup was built FOR THE FIRST CLIENT; see PI-6a) — but because Coolify is a deploy/host vehicle, not a branding-data store: a branding JSON document plus ~5 image assets belong in a tenant_config row + a Storage bucket, not a Coolify app. The Coolify-server clarification is recorded in the Revision note above and in PI-6a/PI-3 (AC-1/AC-2): the existing Coolify instance is the client’s (re-points at handover) and the platform needs its OWN Coolify deployment.

  2. RESOLVED/DISSOLVED (11/06/2026) — overlay execution locus. Superseded by the PI-10/PI-11 pivot: branding is hydrated from the client’s OWN Supabase project at build time, using credentials already present in the project env. Neither former arm survives: no GitHub-Actions overlay leg, no private-repo read token in any Vercel project — there is no private-repo read anywhere in the deploy path. (Nuance recorded: the service-role key is visible to the build step, but it already sits in that same project’s env for runtime — no new credential surface.) OQ oq-618a70db51ab78c3 closes as “neither”.

  3. RESOLVED (11/06/2026) — platform Vercel project naming. A NEW knowledge-hub-platform Vercel project with APP_URL=https://kh.aisolutionhub.co.uk; the current project renames to knowledge-hub-phew when Liam is ready. Sequencing: Phew readiness PRIMARY, platform provisioning secondary (PI-3).

  4. RESOLVED (11/06/2026) — demo-bootstrap-spec.md archives NOW (PI-17).

  5. Reference (do NOT re-open) — ID-68 OQ-G(b) migration-blob purge Branch A/B. Whether the three Phew-bearing migration files are history-rewritten (Branch A) or path-preserved with token redaction (Branch B) is an ADJACENT ID-68 ratification, not an ID-95 decision. Rewriting applied-migration blobs changes file hashes operators may diff against live schema_migrations. Referenced here only because it is the standing-residual that keeps the three files in history until ID-68 ratifies it; it does not gate any ID-95 invariant.

  6. OPEN — tenant_config scope. Branding-only document now, vs a general per-client config document (phew.json already carries classificationDisambiguation, which is client config, not branding). signup_policy stays a separate table regardless (its supabase_auth_admin grant profile differs). {95.3} TECH proposes; Liam ratifies.

  7. OPEN — durable master copy of brand assets. Under PI-10 the client project is the system of record for its own branding; a recut-to-NEW-project also wipes Storage. Somewhere durable must hold the master branding set: operator archive, the platform control-plane DB (pushed via the PI-18 one-way channel), or re-obtain from client. Small, but needs a named owner before the first recut.


State explicitly what this PRODUCT does NOT cover:

  • NO Spine work (ID-45 / ID-64 / ID-101). The re-ingest cutover that produces the first-client handover DB is referenced only insofar as signup_policy is DATA and must be re-INSERTed post-recut (PI-20); the cutover mechanics themselves are out of scope.
  • {68.30} is excluded — owned by the ID-102 track.
  • BYOC Tier-3 BUILD timing is out of scope (PI-6a) — the tier is named and de-risked; WHEN to build it is a Liam sequencing call.
  • The ID-45 re-ingest cutover mechanics are out of scope — only referenced (the signup_policy row is data and must be re-INSERTed post-recut).
  • Brand pick / long-term domain are out of scopekh.aisolutionhub.co.uk is the interim platform domain (OQ-95-6); the permanent brand/domain is tracked separately.
  • The canonical-content propagation MECHANICS (worker, version table, payload format) are out of scope for PRODUCT — they belong to a future {95.3} TECH; this spec pins only the behaviour (PI-18, PI-19).
  • AGPL-pullmd conveyance (OQ-95-4/OQ-66-7) and client-FS corpus folder structure (OQ-95-5/ID-69, with ID-71 context) are dependencies tracked under their own Tasks, not resolved here.
  • MCP plugin URL derivation (the plugin tree’s + plugin-bundle.ts’s seven-kappa references) is ID-71 ai-tooling scope (PI-13/PI-14 carve-out) — no client uses the MCP tools today.
  • taxonomy-sync redesign mechanics (point-of-use per-tenant generation, retirement or repair of the automation shell) — PI-15 pins the behaviour and the transitional constraints; mechanics belong to {95.3} TECH / ID-71.
  • Docs-site root-content disposition (harness/, ops/, eval-fixtures/ homes; the Option S-B “docs + internal harness” charter vs “docs only”) is ID-68 curation territory, not an ID-95 invariant — except where PI-10 already pins that no per-client branding artefact may live there.