Sandbox preview
Use the merchant App card Sandbox preview row. Do not start from
gs://finmatch-p or merchant-router.json.
Idle: Loading…. Then Off, On, On (paused), or Status unavailable. Click confirms, promote vs discard vs turn off: Row states, What happens on each click, Promote, discard, turn off.
Safe config experimentation for a merchant that stays routed to production. Live shoppers are unaffected. Testers open a capability URL on the live storefront and see a forked runtime config from Test.
When to use this
- Trying modal / calculator / Pay Monthly changes before they go live
- Letting a merchant or lender contact review a config change
- Avoiding the old anti-pattern of switching App → Environment to
t(that reroutes all live traffic)
Prerequisite
App environment must be Production (p). Sandbox forks P → T and
discard deletes the stamped T fork. If the merchant is still live on T
(or S), enable/fork/discard are rejected (409) so we never overwrite
or delete the live storefront row. Move the merchant to p first.
Operator flow
Live chrome is on the merchant App card, Sandbox preview row (and a bottom dock when a toggle is Sandbox). Idle state: Loading…. There is no button named Generate preview link, Show diff, or Also load T scripts and CSS.
Row states
| State | What you see | What to do |
|---|---|---|
| Loading… | First paint / status fetch | Wait. Do not click Turn on sandbox yet. |
| Status unavailable | Sub: Could not load sandbox status. Reload the page to retry. (load-error sub can also read Sandbox status unavailable: plus the error) | Reload the merchant page. Grid and actions stay hidden. |
| Off | Sub explains a private copy, or Sandbox needs this merchant on the production environment. / sandboxIneligibleReason | Eligible: Turn on sandbox. Ineligible: move App environment to Production first. The button is disabled when ineligible. |
| On | Sub: Shoppers still see production. Only your preview link sees the sandbox. | Edit, Open preview, promote or discard. |
| On (paused) | Sub: Paused — admin edits and preview links go to production. The draft is kept. | Dock Merchant configuration → Sandbox to resume. Open preview alerts instead of minting a sandbox link. |
Happy path
- Open the merchant in Admin → Merchants. App environment must stay
Production (
p). - Click Turn on sandbox. The button disables until the request
finishes. That forks the production runtime row into
gs://finmatch-t/configs/finmatch-merchant-config.jsonand stamps_sandboxprovenance. Shoppers still see production. Admin writes target T while the row is on (resolveMerchantEnvKeyis sandbox-aware). - The grid shows:
- Your admin edits save to — Production or Sandbox
- Preview link loads — Production build (config-only, live JS/CSS) or Test build (T scripts and CSS)
- Sandbox contents — Matches production, a N settings changed chip plus View changes, Unknown plus View changes, or Checking… while the diff loads
- Open preview (row or dock) issues a typed
sandbox_previewcredential (spv_…) viaauth-apiwith a 30-day expiry. Password prompt: Enter admin password to generate a sandbox preview link:. The plaintext URL is Preview link (shown once, expires …) and Copied to clipboard. when copy succeeds. Open it on the merchant domain ashttps://merchant.domain/?finmatch_sandbox=<token>. Regenerate if lost (do not expect a reveal). This session caches that URL so a later Open preview reuses it. - The storefront shows a bottom bar:
FinMatch sandbox: …with Exit sandbox. - The admin bottom dock has Merchant configuration and FinMatch app (each Live / Sandbox) plus Open preview.
- Promote or tear down with the action row (see Promote, discard, turn off).
What happens on each click
Turn on sandbox. POST /admin/sandbox/:id/enable. Failure alert:
Could not turn on sandbox: plus the error. If merchant-api returns
409 because this Production merchant still has an older non-sandbox
row in T, confirm Replace it with a fresh fork of the current
production config? Cancel leaves sandbox Off. Force does not
bypass the Production-only eligibility check.
Your admin edits save to. Dock / row Merchant configuration
Live pauses sandbox (enabled: false) so admin Saves go to
production and the draft is kept. Sandbox resumes. Failure:
Could not update merchant configuration: plus the error.
Preview link loads. Dock FinMatch app Live = Production build (live JS/CSS, sandbox config). Sandbox = Test build (T scripts and CSS). Failure: Could not update FinMatch app: plus the error.
View changes. Fills the diff box. Match: The sandbox configuration matches production. Drift adds ⚠ Production changed after this fork was taken. Mismatch lists changed keys. Failure: Diff failed: plus the error.
Open preview while On. Mints or reopens the cached URL as above. Failure: Failed to generate preview link: plus the error.
Open preview while On (paused). Alert: Sandbox is paused. Preview links load production until you resume sandbox editing. It does not mint a token.
Open preview while Off. Confirm: Sandbox is off. Turn it on and fork the production configuration now? Cancel does nothing.
Promote, discard, turn off
The action row never shows Discard changes and Turn off sandbox together.
| Control | When it appears | Confirm | Success / failure |
|---|---|---|---|
| Promote to production (label becomes Promote N change(s) when the diff has keys) | Contents drifted | Promote the sandbox configuration to production? Live shoppers will see these changes within about a minute. | Alert Sandbox promoted to production. Failure: Promote failed: plus the error. |
| Discard changes | Contents are not Matches production (including Unknown / still checking) | Discard the draft? Its unpromoted changes will be lost. Production is not affected. Unknown-diff confirm warns it could not verify whether the draft differs. | Deletes the T draft and clears preview-link cache. Failure: Discard failed: plus the error. |
| Turn off sandbox | Contents Matches production | Turn off sandbox? The draft matches production, so nothing will be lost. Preview links stop working. | Same discard endpoint. Production is unchanged. |
Production changed after you forked. Review View changes before promoting, or the sandbox overwrite will replace those later production edits.
Do not use Discard changes / Turn off sandbox to “undo” a promote that already succeeded — that promote already wrote P.
What stays the same
GCS and router notes. Live shoppers stay on production while the sandbox row is on.
| Concern | Behaviour |
|---|---|
| App environment | Remains p in merchant-router.json |
| Live shoppers | Always get gs://finmatch-p config |
| Merchant ID | Unchanged — one identity, forked runtime row |
| Snippet | Same finmatch-sdk.js?merchantID=… |
Modelled on Shopify theme share previews: expiring, plural, labelled preview links with a persistent on-page exit bar.
Cart permalink
Headless browsers visit the cart with an empty session, so a bare
/cart URL produces an empty screenshot. A cart permalink is a URL
that pre-populates the cart with line items as the page loads.
Shopify format: store /cart/ plus VARIANT_ID:QUANTITY, comma-separated
for multiple lines. Find a variant id in the product JSON on the page
source, or Shopify admin → Products → Variants. Tick Save as default
to write merchant.config.testUrls.cart. Other platforms: paste any URL
that loads the cart with at least one item.
Credential model
Merchant auth identities (role: merchant in
auth-credentials.json) carry a typed keys[] array:
| Type | Prefix | UI verb | Lifetime |
|---|---|---|---|
finance_assistant | sk_live_ | Copy / reveal (step-up) | Long-lived |
sandbox_preview | spv_ | Open preview (show-once) | 30 days, plural |
The Users page expands a merchant row into a credential registry (type, label, status, created, expiry, revoke).
apiKey remains a back-compat alias for the active Finance Assistant
key so finance-assistant and the Cloudflare KV mirror keep working.
auth-api writes that KV pair; operators do not paste it by hand.
Technical path
Preview URL ?finmatch_sandbox=spv_…
→ SDK sessionStorage + fresh=1
→ getMerchantConfig?merchantID=&sandbox=&fresh=1
→ hash token, lookup gs://finmatch-secrets/sandbox-tokens.json
→ read gs://finmatch-t/.../finmatch-merchant-config.json
→ response { …merchant, environment: "p", sandbox: true, sandboxConfigEnv: "t" }
→ SDK preview bar; separate localStorage key (never poisons live cache)
Fail-closed: expired, revoked, wrong-merchant, missing projection
entries, or router sandbox not enabled (including after
Discard changes / Turn off sandbox)
fall through to the normal production path with no error page. A valid
token alone is not enough — discard clears the router block so leftover
preview links stop forcing the T bucket. If the T fork row is missing
while sandbox is still marked enabled, getMerchantConfig also falls
through to live instead of 404ing. The SDK clears the preview session
only on a successful non-sandbox response (discard / expired token).
Transient fetch errors keep the session and any cached sandbox config
so in-site navigation can recover.
Admin / API endpoints
| Service | Endpoint | Purpose |
|---|---|---|
| merchant-api | GET /admin/sandbox/:id/status | Router sandbox + fork + drift |
| merchant-api | POST /admin/sandbox/:id/enable | Fork P→T + set router sandbox |
| merchant-api | GET /admin/sandbox/:id/diff | Changed keys + drift warning |
| merchant-api | POST /admin/sandbox/:id/promote | Apply T row onto P |
| merchant-api | POST /admin/sandbox/:id/discard | Remove T row + clear sandbox |
| auth-api | POST /admin/merchant-keys/issue | Mint typed key (show-once) |
| auth-api | POST /admin/merchant-keys/revoke | Revoke |
| auth-api | POST /admin/sandbox-tokens/reconcile | Rebuild projection |
Naming discipline
- Merchant ID — public identifier, never masked
- Finance Assistant key — long-lived API key
- Sandbox preview link — human-facing capability URL, not an API key