Skip to main content

Source of Truth Architecture

This is the canonical reference for where each FinMatch asset lives, who owns it, and how it gets to production. It reflects the post-cleanup state achieved by PRs #149-#171 (Jun 2026), in which all stale mirror copies, ghost runtime files, and duplicate snapshots were removed from Git in favour of a single source of truth per asset.

If anything below ever drifts from the live workflows or repository state, the live files (.github/workflows/*.yml, scripts/deploy-workflow.sh, cloud-run/SERVICES.md) are authoritative and this doc should be updated to match.

1. Guiding principle​

Every protected runtime asset has exactly one writable home. Git never holds a parallel snapshot of an asset that a Cloud Run service manages in GCS. Mirror copies are not a safety net — they are a drift hazard, so we removed them.

This applies to:

  • Runtime modules (scripts/*.js, css/finmatch.css)
  • Storefront configs (configs/finmatch-merchant-config.json, configs/lender-overrides.json, configs/lender-styling.json)
  • Runtime infrastructure files (cors.json, merchants.json, merchant-router.json, finmatch-rate-card.json, lender-originated-quotes.json, auth-credentials.json, partner-api-keys.json)
  • The SDK entrypoint (finmatch-sdk.js)

2. Branch → bucket → role map​

BranchAuthoritative forSynced to
finmatch-sharedAdmin UI, Cloud Run services, Cloud Functions, GitHub Actions workflows, Docusaurus docs, canonical SDK entrypoint (finmatch-finance-marketing-assets/s/scripts/finmatch-sdk.js), shared lender styling/overrides authored by admin.gs://finmatch-shared, gs://finmatch-admin, gs://finmatch-login, gs://finmatch-developer, gs://finmatch-finance-marketing-assets
finmatch-pProduction storefront runtime: scripts/, css/finmatch.css, configs/ (non-protected fields).gs://finmatch-p
finmatch-sStaging storefront runtime.gs://finmatch-s
finmatch-tTest storefront runtime (downstream mirror of finmatch-p for shared scripts//css/ only — explicit p → t sync PRs).gs://finmatch-t

finmatch-shared is the trunk for development; environment branches are runtime deploy targets, not upstream code branches for storefront JS/CSS.

3. What lives where (and what does NOT)​

3.1 finmatch-shared​

Yes:

  • admin/ — admin dashboard (HTML, JS, CSS).
  • admin-docs/ — Docusaurus admin docs.
  • cloud-run/<service>/ — Cloud Run service source (each with its own Dockerfile, package.json, deploy workflow).
  • cloud-functions/getMerchantConfig/ — Cloud Function source.
  • .github/workflows/*.yml — every workflow (single source of truth).
  • scripts/deploy-workflow.sh — the maintainer script that propagates workflow files to env branches.
  • scripts/analytics/ — operational analytics utilities.
  • finmatch-finance-marketing-assets/s/scripts/finmatch-sdk.js — the canonical SDK entrypoint. The only file in the repo allowed to write to gs://finmatch-finance-marketing-assets/s/scripts/finmatch-sdk.js.
  • finmatch-shared/ (subtree) — shared finance formulas, images, and the SSOT JSON files that the admin/API ecosystem reads.
  • platform/ — login.finmatch.io and api.finmatch.io static sites.
  • developer-docs/ — Docusaurus developer docs.
  • .cursor/skills/finmatch-architecture/SKILL.md — agent-facing source of truth for architecture and PR discipline.

No (deliberately removed; do not re-add):

  • ❌ Root scripts/*.js storefront runtime modules (deleted in PR #154; guardrail in sync-to-gcs.yml rejects re-introduction).
  • ❌ Root css/finmatch.css (deleted in PR #154).
  • ❌ Root configs/*.json storefront configs (deleted in PR #154).
  • ❌ cors.json at repo root (untracked in PR #169 — managed by merchant-api directly in gs://finmatch-secrets/cors.json).
  • ❌ Numbered duplicate files (* 2.js, * 3.md, etc.) — Finder/iCloud artefacts, blocked by .gitignore lines 35-39.

3.2 finmatch-p (and other env branches)​

Yes:

  • scripts/*.js — storefront runtime modules (finance-container.js, bon-voyage.js, finance-assistant.js, all <lender>-logic.js, shared utilities).
  • css/finmatch.css.
  • configs/finmatch-merchant-config.json and other configs/ storefront files (non-protected fields only — protected fields are written by merchant-api).
  • .github/workflows/*.yml — propagated from finmatch-shared via scripts/deploy-workflow.sh. Drift-checked by workflow-drift-check.yml.

No (deliberately removed; do not re-add):

  • ❌ finmatch-finance-marketing-assets/s/scripts/finmatch-sdk.js — SDK ghost mirror deleted from every env branch in PRs #160-#167. The canonical SDK lives only on finmatch-shared and is uploaded directly to its own GCS bucket from there.
  • ❌ cors.json (untracked in PRs #170-#171).
  • ❌ Historical migration scripts (migrate_merchants.py, scripts/migration/*) retired in PR #168.
  • ❌ Duplicate root *.md planning docs (retired from finmatch-p in PR #158, from finmatch-t in PR #159).

4. Protected runtime files (single-writer)​

These files are written exclusively by an admin/API runtime path and must never be edited via Git:

FileOwning writerLives in
merchants.jsonmerchant-api (/api/merchants/...)gs://finmatch-secrets/merchants.json (MERCHANTS_JSON_BUCKET; public copy deleted; no Git copy)
merchant-router.jsonmerchant-api (switch-env, create)gs://finmatch-secrets/merchant-router.json (MERCHANT_ROUTER_BUCKET; public copy deleted; no Git copy)
cors.jsonmerchant-api (CORS bootstrap)gs://finmatch-secrets/cors.json (CORS_JSON_BUCKET; public copy deleted; no Git copy)
auth-credentials.jsonauth-apigs://finmatch-secrets/auth-credentials.json (AUTH_CREDENTIALS_BUCKET; public copy deleted; no Git copy)
sandbox-tokens.jsonauth-api (derived projection of sandbox_preview keys; not source of truth)gs://finmatch-secrets/sandbox-tokens.json (SANDBOX_TOKENS_BUCKET; public copy deleted; no Git copy)
users.json / users-enhanced.jsonnone (legacy leftover; no live users-api)archived in gs://finmatch-secrets/; public copies deleted
partner-api-keys.jsonpartner-api (lib/auth.js)gs://finmatch-secrets/partner-api-keys.json (PARTNER_API_KEYS_BUCKET; public copy deleted)
finmatch-rate-card.jsoncredit-products-apigs://finmatch-shared/finmatch-rate-card.json
configs/finmatch-rate-card.json (env buckets)No live writer (legacy snapshot; excluded from sync-to-gcs.yml)gs://finmatch-{p,s,t}/configs/ — not SSOT; see Rate cards overview
lender-originated-quotes.jsoncredit-products-apigs://finmatch-secrets/lender-originated-quotes.json (LENDER_ORIGINATED_QUOTES_BUCKET; public copy deleted)
stripe-customers.jsonnone (legacy leftover)archived in gs://finmatch-secrets/; live Stripe snapshots are still gs://finmatch-admin/stripe/ until SECURITY.md 9.7 Phase 1.3
lender-styling.json / lender-overrides.jsonmerchant-api (admin UI)gs://finmatch-shared/
configs/finmatch-merchant-config.jsonmerchant-api (atomic write)gs://finmatch-{p,s,t}/configs/
contacts/contacts.jsoncontacts-apigs://finmatch-secrets/contacts/contacts.json (CONTACTS_BUCKET; public copy deleted; profiles store contactIds only)

sync-to-gcs.yml snapshots these objects pre/post sync and rejects any push that mutates them on disk.

Why no Git copies (Jun 2026 router-drift incident): stale Git copies of merchant-router.json / merchants.json / auth-credentials.json lingered on finmatch-shared, plus a legacy configs/merchant-router-finmatch-shared.json mirror on finmatch-p/finmatch-t. The router copy still showed a migrated merchant on env s while GCS correctly served p, sending a debugging agent down a false trail. These copies are never synced (the rsync -x-excludes them) and never read at runtime — getMerchantConfig and merchant-api read/write the GCS object directly. They were removed and .gitignored so they can't return.

5. Workflow guardrails​

WorkflowDefends against
sync-to-gcs.ymlMutations to protected runtime files; ghost-runtime additions on finmatch-shared (the post-cleanup guardrail added in PR #153 blocks any add/modify to root scripts/, css/, configs/ on finmatch-shared).
workflow-drift-check.ymlWorkflow files diverging on env branches from finmatch-shared.
merchant-config-monitoring.ymlCron-based tiered alerts on merchant config integrity.
admin-merchant-api-url-guard.ymlAdmin embedding the wrong merchant-api URL.
snap-rate-card-test.ymlNumeric drift in Snap finance calculations (£280 anchor + four scenarios).

The ghost-runtime guardrail is the keystone of the post-cleanup architecture: it prevents the very drift we just spent the cleanup removing.

6. The SDK entrypoint (special case)​

finmatch-sdk.js is the snippet entry that merchant storefronts load. It used to be mirrored to every env branch in finmatch-finance-marketing-assets/s/scripts/finmatch-sdk.js, with a "SDK canonicalization guardrail" trying to keep the mirrors in sync. This was both ineffective (drift accumulated) and unnecessary (the SDK is uploaded directly from finmatch-shared to its own legacy bucket).

Post-cleanup state (PRs #157, #160-#167):

  • The SDK lives only on finmatch-shared at finmatch-finance-marketing-assets/s/scripts/finmatch-sdk.js.
  • sync-to-gcs.yml uploads it from finmatch-shared to gs://finmatch-finance-marketing-assets/s/scripts/finmatch-sdk.js.
  • Env branches have no copy. The SDK guardrail and the SDK propagation step in deploy-workflow.sh were removed.

If a merchant snippet ever 404s on the SDK URL, that means gs://finmatch-finance-marketing-assets/s/scripts/finmatch-sdk.js is missing — fix it on finmatch-shared.

7. Decision tree: where does my change go?​

Does the change touch admin UI, Cloud Run, workflows, docs, or SDK?
└── YES → finmatch-shared

Does the change touch storefront runtime (scripts/, css/, configs/)?
└── YES → finmatch-p (or the merchant's current env branch)

Is the change a protected runtime file mutation (merchants.json, etc.)?
└── DO NOT EDIT IN GIT — use the admin dashboard / Cloud Run API.

For details on PR workflow, base branch selection, and the propagation of workflow updates, see .cursor/skills/finmatch-architecture/SKILL.md on finmatch-shared.

8. Rate card: writer vs readers​

The protected-files table names a single writer for the catalogue: credit-products-api → gs://finmatch-shared/finmatch-rate-card.json.

Storefront runtime today still reads two objects: env configs/finmatch-rate-card.json first (SDK rateCardsUrl), then shared root as top-up. Those env files are stale snapshots, not a second writable SSOT. Intention of travel: one event-driven catalogue via credit-products-api; merchant-scoped delivery through the resolver; retire env rate-card objects and dual-fetch logic.

Verified landscape and programme steps: Rate cards overview, Asset delivery and egress §3.