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
| Live | Sandbox | |
|---|---|---|
| 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 |
| Data | your live products, orders, customers and media library | its own copies, kept apart from live in both directions |
| Storefront | the theme as published | the same theme, plus a fixed amber banner "Test mode — orders here are not real" on every page |
| Payments | your Stripe connected account / PayPal keys / manual | its 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 labels | Formahand Shipping, billed on your statement | free test labels with simulated tracking, and nothing accrued |
| buyers and you | only the store owner's address, subject prefixed [Test] ; a sandbox with no owner address on file sends nothing | |
| Events | mode: "live" on every event | mode: "test"; subscriptions and flows are the sandbox's own |
| Usage, walls, statements | metered and walled by your plan | never metered, never walled (rate limits still apply), never on a statement; the Free plan's "1 store" does not count it |
| Tokens | a live token | a test token, fh_test_…, which only ever reaches the sandbox |
| Domains, plan, billing, team, card | live only | not 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):
| Resource | Promote (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 pages | becomes 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 version | copied 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 images | created or updated live; images are copied into the target environment's media library and URLs rewritten to its address | same |
| Collections (members mapped through product slugs), pages, navigation, SEO | live immediately | same |
Shipping settings and rates; the shipping mode when it is self or formahand | live immediately (a courier platform mode is noted, not switched: its credentials belong to the target) | same |
| Discounts | recreated on the target's own Stripe account; codes that already exist are skipped; without a ready Stripe account they are reported as conflicts | same |
| Email templates (edited or custom ones) | upserted by key | same |
| Flows | created or updated disabled; webhook secrets and agent bearers do not travel (a note names the flow) | same |
| Integration configs | config only, never secrets | same |
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
- Mint a live token with
settings:write(or use the dashboard) and callenable_sandbox; pollget_environmentuntilsandbox.runtimeStatusisready— usually within a minute. - Mint a test token (Settings → Agents & API → Test) with the scopes you need.
get_storenow answersmode: "test"and the-testaddress. - 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's4242…test card; the confirmation lands in the owner's inbox as[Test] …. promote_to_live { dryRun: true }→ read the summary →promote_to_live.- 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.