Live and test environments

Every store gets a sandbox with test payments, free test labels and owner-only email; what promote and refresh copy, and the agent walkthrough.

Written 2026-09-15. Every store has a live environment (the store shoppers buy from) and, once the owner turns on Test mode, a sandbox: a complete second copy of the store with its own address, its own data and its own media library, where payments run on Stripe test mode, shipping labels are free test labels and every email goes to the owner. The model is Stripe's "environments", not a row flag: nothing in the sandbox can touch a live order, a live customer or your statement, and nothing in the live store changes until you promote and publish.

1. The model

LiveSandbox
Address<slug>.store-on-formahand.app and your custom domains<slug>-test.store-on-formahand.app only — never a custom domain, never your primary address, always X-Robots-Tag: noindex and never cached
Datayour live products, orders, customers and media libraryits own copies, kept apart from live in both directions
Storefrontthe theme as publishedthe same theme, plus a fixed amber banner "Test mode — orders here are not real" on every page
Paymentsyour Stripe connected account / PayPal keys / manualits own payment account in test mode: a Stripe test-mode connected account (onboarded in the dashboard's embedded form with Stripe's test data), sandbox PayPal keys, or manual
Shipping labelsFormahand Shipping, billed on your statementfree test labels with simulated tracking, and nothing accrued
Emailbuyers and youonly the store owner's address, subject prefixed [Test] ; a sandbox with no owner address on file sends nothing
Eventsmode: "live" on every eventmode: "test"; subscriptions and flows are the sandbox's own
Usage, walls, statementsmetered and walled by your plannever metered, never walled (rate limits still apply), never on a statement; the Free plan's "1 store" does not count it
Tokensa live tokena test token, fh_test_…, which only ever reaches the sandbox
Domains, plan, billing, team, cardlive onlynot applicable

The value in the API and in every event payload is mode: live or test. "Sandbox" is simply what the dashboard calls the test environment.

2. Addresses

Both environments answer on the storefront domain, store-on-formahand.app (custom domains), never on formahand.com: the -test suffix did not change, only the domain it hangs off. <slug>-test.formahand.com addresses from before 2026-09-23 answer a permanent redirect to <slug>-test.store-on-formahand.app, path and query intact, like every other pre-move store address.

Only <slug>-test.store-on-formahand.app reaches the sandbox; every other hostname reaches the live store. Every sandbox response carries X-Robots-Tag: noindex and Cache-Control: no-store, and the amber test banner is injected into every HTML page, so a sandbox can never be indexed or mistaken for the real store. The public storefront read API (/api/storefront/*) works on the sandbox address. The idle-store holding page is a live-store rule; a sandbox is never paused. Custom domains are live-only.

3. What the sandbox does differently

  • Checkout on the sandbox address reserves stock in the sandbox and opens Stripe Checkout in test mode on the sandbox's own connected account (303). The return URL of a PayPal checkout carries &mode=test.
  • A payment can never cross the boundary. A live payment event is never accepted for a sandbox order and a test one is never accepted for a live order; a mismatched event is refused and nothing is written. The same holds for shipping updates.
  • Email goes to the store owner only, with [Test] prefixed: order confirmations, shipping confirmations, flow emails and campaigns alike. A campaign sends one copy to the owner, never to the list.
  • Labels: a sandbox label is a free test label. Nothing accrues, no card is needed, and the card-setup route refuses in the sandbox. If test-mode Formahand Shipping is not available on your store, the sandbox offers your own self-ship rates only — a live shipping account is never used from a sandbox.
  • Usage and walls: sandbox API calls are not metered and do not bump the activity clock; automation runs are not metered and never walled; the flow count, media and assistant walls do not apply; the domain, plan and billing tools are live-only.
  • Events: every event carries the extension attribute mode ("live" | "test"); a subscription created in the sandbox delivers only sandbox events.
  • Scheduled work — tracking updates, domain checks, usage meters, statements and billing — runs for your live store only. Event deliveries run for both.

4. Dashboard

The Live | Test pill floats at the bottom right of the dashboard. Choosing Test the first time explains what the sandbox is and creates it (POST /api/environment { action: "enable" }), then selects it; the amber bar reads "Preparing your test store…" until it is ready, usually within a minute. Switching reloads the dashboard onto the chosen environment: every view then reads and writes that environment, the Payments tab onboards the sandbox's own processor account, and the amber bar You are in Test mode stays across the dashboard with a link to the test storefront and a "Back to Live" button.

Settings → Store → Test mode shows the sandbox address and status, what promote copies, and the buttons Preview promote (dry run), Promote to live and Refresh sandbox from live (both confirm first) with a per-resource summary of created / updated / skipped and any conflicts.

Settings → Agents & API mints tokens for either environment; a test token is fh_test_….

5. Promote and refresh

One environment is exported into a bundle and imported into the other through the same API the dashboard itself uses, so every write is validated exactly as a dashboard edit would be.

Copied, idempotently (products by slug, collections by handle, pages by slug, templates by key, flows by name, discounts by code):

ResourcePromote (sandbox → live)Refresh (live → sandbox)
Storefront draft (schema version 3): theme settings, section instances with the version each is pinned at, module settings, hero image, navigation, content pagesbecomes a new live draft revision; publishing stays explicit (Online Store → Publish, or publish_storefront)imported and published so the sandbox mirrors the live storefront
Sections: the store's own, catalogue and add-on sections, every kept versioncopied before the draft, so every pin finds its version; a version live already has is left as it is (versions never change, and a live page may render it)same, except a version the sandbox holds with different content is overwritten from live
Products with variants and imagescreated or updated live; images are copied into the target environment's media library and URLs rewritten to its addresssame
Collections (members mapped through product slugs), pages, navigation, SEOlive immediatelysame
Shipping settings and rates; the shipping mode when it is self or formahandlive immediately (a courier platform mode is noted, not switched: its credentials belong to the target)same
Discountsrecreated on the target's own Stripe account; codes that already exist are skipped; without a ready Stripe account they are reported as conflictssame
Email templates (edited or custom ones)upserted by keysame
Flowscreated or updated disabled; webhook secrets and agent bearers do not travel (a note names the flow)same
Integration configsconfig only, never secretssame

Never copied: orders, customers, events, deliveries, event subscriptions (their endpoints differ per environment), shipping labels, usage, statements, tokens, domains, payment accounts. dryRun: true returns the same summary without writing; sections.items lists each section and the versions that would move. A resource the target refuses (a reserved variant, a bad handle) is listed under conflicts; the rest still lands.

6. API and MCP

GET /api/environment → { live, sandbox, sandboxEnabledAt, selected, credentials }; POST /api/environment { action: "select" | "enable" | "promote" | "refresh", dryRun? } (select is session-only; a token's environment is fixed by its prefix). Scopes settings:read / settings:write. POST /api/tokens { mode: "test" } mints a sandbox token.

MCP: get_store returns mode and the environment's addresses; get_environment, enable_sandbox, promote_to_live { dryRun? }, refresh_sandbox; describe_system and get_started explain and sequence everything (agents). Every tool description says which environment it acts on; the domain, plan and billing tools refuse a test token.

7. Limits

  • One sandbox per store, and it cannot be deleted from the dashboard.
  • The sandbox has no custom domain, no plan of its own, no card and no statement.
  • Promote copies content, not history: sandbox orders stay in the sandbox.
  • Stripe test-mode onboarding runs in the dashboard's own embedded form (with test data); an agent still cannot complete it.
  • Sandbox tokens are rate limited like live ones.

8. Agent walkthrough

  1. Mint a live token with settings:write (or use the dashboard) and call enable_sandbox; poll get_environment until sandbox.runtimeStatus is ready — usually within a minute.
  2. Mint a test token (Settings → Agents & API → Test) with the scopes you need. get_store now answers mode: "test" and the -test address.
  3. Build: set_shipping, upsert_product + set_product_image, upsert_collection, get_storefront_draft → edits → publish_storefront. Place an order on the sandbox address with Stripe's 4242… test card; the confirmation lands in the owner's inbox as [Test] ….
  4. promote_to_live { dryRun: true } → read the summary → promote_to_live.
  5. With the live token: get_storefront_draft (the promoted revision) → publish_storefront.

9. How long setup takes

A new store, and a new sandbox, are usually ready within a minute. An agent polls get_environment until sandbox.runtimeStatus is ready; the onboarding wizard and the dashboard show the same progress.

Setup reports the stage it is on rather than a spinner. GET /api/onboarding?status=1 answers store.step, store.stepLabel and store.failed, where step is one of creating-database, creating-storage, applying-schema, seeding, deploying, health-check and store-ready. The wizard renders them as a step list, and the dashboard carries a slim "Your store is being built · <step>" strip until the live store is ready, with storefront links inert until then.

Nothing here can strand you: the wizard has no timeout that becomes an error. After 45 s it offers "Go to my dashboard, we'll finish in the background" — the store is durable and the strip follows it — and a failed attempt reads "We hit a snag and are retrying automatically" with the step it stopped on. Setup is retried automatically up to 10 times; after that it stays failed and Formahand support picks it up.