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-Nwith 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.
Summary
Section titled “Summary”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.
Problem
Section titled “Problem”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 flowproc_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 filelib/client-config.ts; incoming calls = 1 = the module init that computesBRANDINGonce; 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 “BRANDINGcomputed once at module init” finding (id95-oq-dossier-2026-06-11.md:20).gitnexus_context({name: 'renderClientBrandingMap'})-> incoming caller count = 1 (mainin 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>.jsonto 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.md— Liam’s supporting info for this session (mandatory input): OQ-95-6 (Vercel rename +kh.aisolutionhub.co.ukinterim 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.mdstaleness 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-68PRODUCT.md:330-332(AC-E3). Cross-referenced, not duplicated.
Behavior
Section titled “Behavior”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”-
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; noif (client === 'x')branch exists in app source (grep clean). -
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, andAPP_URL(the client’s own domain), plus the per-client branding data (thetenant_configrow + 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 matchesclient-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 withNEXT_PUBLIC_CLIENT_IDunset fails closed (perlib/env-client.tsREQUIRED check), and no per-client value is read from any source other than env or the client’s owntenant_config/branding bucket. -
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 domainkh.aisolutionhub.co.uk(APP_URL=https://kh.aisolutionhub.co.uk), withNEXT_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 rendersdefaultbranding; no client row is reachable from the platform instance; the platform’s own Coolify deployment is recorded as named-but-unbuilt (PI-6a). -
PI-4 (Vercel project rename — current
knowledge-hubbecomesknowledge-hub-phew). The existingknowledge-hubVercel project is renamed toknowledge-hub-phew(it is the first-client project). The platform dev/demo project is created fresh asknowledge-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.appdomain 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”-
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). -
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/**, routingkh-pipeline.aisolutionhub.co.uk) was built FOR THE FIRST CLIENT, not as a platform-generic host: theaisolutionhub.co.ukdomain 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 existingdeploy/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”-
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_policyis 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 ofsupabase/migrations/**for client-name tokens / domain literals / credentials returns zero (the three current Phew-bearing files are purge targets, not new regressions);signup_policymigration carries no domain literal. -
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 thefilter-repohistory 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 movemigration-revoke-guardout of the public repo (which would breach Inv 30 / lose the gate). -
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) intosupabase/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”-
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 genericpublic.tenant_configrow holding the branding JSON document, plus a privatebrandingStorage 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. Thetenant_configtable is generic schema shipped in the canonical public migration set (PI-7/PI-9 compliant: no client literal), RLS deny-all withREVOKE ALL FROM anon, authenticated, public(service-role access only) — the same config-as-data template assignup_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 bothDEPLOY-OVERLAY-DESIGN.md§4’sknowledge-hub-internal/client-overlays/reference AND the earlierclient-branding/<id>/docs-site home (rejected 11/06/2026: docs-site holds no branding artefact). Verify: noclient-branding/dir exists in any repo; thetenant_configmigration greps clean of client tokens (PI-9 lint); the Phew deploy resolves branding from its own Supabase project;git ls-filesin the docs-site shows no branding JSON/assets. -
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 thetenant_configrow and downloads the branding-bucket assets intolib/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-68DEPLOY-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); withdefault/ absent creds the fetch no-ops and the control build resolvesdefaultand stays green. The public repo HEAD showsgit ls-fileszero rows forlib/branding/clients/phew.jsonandpublic/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 fromtenant_config); (2)git rmthe two paths on the public HEAD; (3) production redeploy -> re-verify branded; (4) no-row/no-creds control build resolvesdefaultgreen (ID-68DEPLOY-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. -
PI-12 (new client = data drop + env set, zero repo commits anywhere). Standing up a new client is: INSERT the
tenant_configrow + 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 thesignup_policyrow, 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”-
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 iscomponents/settings/mcp-url.ts(getMcpUrl()derives the MCP endpoint fromNEXT_PUBLIC_APP_URLwith awindow.location.originfallback; zero hardcoded host). The currentknowledge-hub-seven-kappa.vercel.apphardcoding (17+ references including thetaxonomy-synccallback and theci.ymldeploy-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 generatedlib/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 literalknowledge-hub-seven-kappareturns zero outside the plugin tree; the plugin tree/bundle residue is tracked under ID-71 (regenerate viabun run build:pluginwhen ID-71 lands its derivation). -
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-phewrename. 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. -
PI-15 (callback-style workflows take their targets from per-environment config;
taxonomy-syncis 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. Fortaxonomy-syncspecifically: (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 withoutKH_PRIVATE_DOCS_DIRand structurally cannot commit the relocated prompt (it only hascontents: writeon the public repo); the automation shell (workflow + repository_dispatch + HMAC callback + drift banner +taxonomy_sync_statehash machinery) is retired or repaired as an EXPLICIT decision, with operator-runbun run sync:taxonomyas 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 intaxonomy-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 oftaxonomy_snapshot.json/plugin vocab/classification prompt is added; client-provenance rows stop being committed to the public repo.
F. Client-content seam-flip trigger
Section titled “F. Client-content seam-flip trigger”-
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.
-
PI-17 (
demo-bootstrap-spec.mdis superseded — archive + pointer). The staledemo-bootstrap-spec.md(31/03/2026, pre-dates the config-table signup design, the overlay seam, and ID-95 topology) is archived toknowledge-hub-archiveand 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.mdis 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)”-
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).
-
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_fdwand 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: nopostgres_fdwforeign 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.
H. Signup gate + edge cases
Section titled “H. Signup gate + edge cases”-
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_domainreadingpublic.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. Becausesignup_policyis 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: thesignup_policyrow, thetenant_configrow, 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 nosignup_policyrow 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. -
PI-21 (per-project cron + region are repo-global — per-client hygiene required). The repo-global
vercel.jsoncrons 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-projectCRON_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 ownCRON_SECRETand Supabase env; the spec names the cron-multiplication cost and the repo-global region constraint as accepted/known rather than assuming per-project regions. -
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.
-
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.
Open decisions for ratification
Section titled “Open decisions for ratification”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).
-
RESOLVED/RATIFIED (11/06/2026) — tenant_config pivot. The PI-10/PI-11 branding home (
tenant_configrow + 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 existingdeploy/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 atenant_configrow + 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. -
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”.
-
RESOLVED (11/06/2026) — platform Vercel project naming. A NEW
knowledge-hub-platformVercel project withAPP_URL=https://kh.aisolutionhub.co.uk; the current project renames toknowledge-hub-phewwhen Liam is ready. Sequencing: Phew readiness PRIMARY, platform provisioning secondary (PI-3). -
RESOLVED (11/06/2026) —
demo-bootstrap-spec.mdarchives NOW (PI-17). -
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. -
OPEN —
tenant_configscope. Branding-only document now, vs a general per-client config document (phew.jsonalready carriesclassificationDisambiguation, which is client config, not branding).signup_policystays a separate table regardless (itssupabase_auth_admingrant profile differs).{95.3}TECH proposes; Liam ratifies. -
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.
Non-goals
Section titled “Non-goals”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_policyis 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_policyrow is data and must be re-INSERTed post-recut). - Brand pick / long-term domain are out of scope —
kh.aisolutionhub.co.ukis 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.