Going live: environments, providers, webhooks
What decides whether a running copy of Clear moves test money or real money, what each outside
provider needs from the API's environment, and the webhook endpoints the API exposes. API
variables are in apps/api/env.example unless marked otherwise; the
apps' are in their own .env.example. Who decides that a deployment goes live is in
Governance.
On this page
There is no single switch
The code has no one "environment" setting. The chain is chosen in one place, each provider by its own variable or by the mode of its key, and the Command Center per section. A deployment is live only when every setting below is on its live side; one left on a sandbox makes a deployment that is part test, part real.
Unless stated otherwise, a provider whose key is not set is off: its routes answer that it is not configured.
NODE_ENV
Neither the Dockerfile nor railway.json sets it. Set NODE_ENV=production on every deployed
environment: an unset value counts as production for some checks and not for others.
SEND_OTP_BYPASS_ENABLEDdevelopment or test, unset included: the bypass is ignoredSEND_SKIP_ESCROW_TX_VERIFICATIONproduction only: unset still lets the skip throughPAY_SECRETS_KEY, REWARDS_TOKEN_SECRETproduction only: missing, it refuses; otherwise it makes a key for this process alone, so nothing it signs outlives a restartproduction turns it on, unless POSTGRES_SSL_MODE or the URL's sslmode says otherwisedevelopment only: the redacted error goes back to the callerThe chain
ACTIVE_CHAIN_ID, else SEND_DEFAULT_CHAIN_ID8453 (Base) when neither is set; 84532 for Base SepoliaDATA_CHAIN_IDSclearNetwork.ts)VITE_MEMBER_APP_URLVITE_CLEAR_NETWORKmainnet, or testnet for anything elsepackages/command-typesContract addresses are per chain: CLRUSD_<chainId>, ESA_VAULT_<chainId>, the *_84532
addresses the credit services read, SEND_CLAIM_ESCROW_ADDRESS_<chainId> and the savings relayer's
SAVINGS_*_<chainId>. Where they come from is Deploying the contracts.
Builds and deploys
- Vercel always builds an app's production deployment. A preview builds only for a pull request, or on
dev, whose previews are deployed like production; and even then it is skipped when nothing that app builds from changed since its last deployment, or when the commit message says[skip preview](scripts/vercel-ignore.sh, called from each app'svercel.json). - Railway rebuilds the API only when a path in
apps/api/railway.json'swatchPatternschanged: the API, the packages it links, and its Docker and Railway files. - The browser apps bake their variables in at build time (
VITE_*). Changing one means a new build, and anything with that prefix is public (Setting up).
Cross-origin
CORS_ORIGIN lists the origins allowed to call the API. STRICT_CORS=true refuses anything not
listed; without it the API allows every origin. ALLOW_VERCEL_PREVIEWS=true also admits Vercel
preview hosts. The public /v1 API has its own CORS, per shop.
The installed merchant app (Capacitor) calls the API from capacitor://localhost on iOS and
https://localhost on Android. With STRICT_CORS=true, add both to CORS_ORIGIN
(Setting up → The installed merchant app).
Providers
Each provider's sandbox and live sides, the variable that chooses between them, and what else it needs.
BRIDGE_ENVLITHIC_ENV and LITHIC_ALLOW_PRODUCTIONPLAID_ENVRAMP_SANDBOXBridge
BRIDGE_API_KEY is the live key. BRIDGE_ENV=sandbox swaps the sandbox in at start-up, before
anything reads Bridge's settings (bridgeEnv.ts):
BRIDGE_SANDBOX_API_KEY becomes the key, the base URL becomes Bridge's sandbox, and
BRIDGE_SANDBOX_WEBHOOK_PUBLIC_KEY, when set, replaces the webhook keys. With BRIDGE_ENV=sandbox
and no sandbox key, Bridge is left unconfigured rather than pointed at live.
It also needs BRIDGE_WEBHOOK_PUBLIC_KEY (below), and BRIDGE_ONBOARDING_REDIRECT_URI for where
hosted KYC returns. Send's fiat payouts use Bridge when SEND_PAYOUT_PROVIDER is bridge_only or
stripe_bridge_jit; it is mock until then, and the SEND_BRIDGE_* variables shape those
transfers.
Bank withdrawals are POST /api/withdraw/:wallet
(withdraw.ts): USDC on Base is off-ramped by Bridge to a
bank account the member linked with Plaid, after a step-up. rail is ach (standard) or
ach_same_day, which charges Clear's fee through Bridge's developer_fee_percent:
BRIDGE_INSTANT_WITHDRAW_FEE_PERCENT, 1 when unset (not in env.example). It needs Bridge and
Plaid both configured, and a Bridge customer for the member's verified email; otherwise it answers
503 with notConfigured. The response carries a
Bridge deposit address: the client must send the USDC there, or the transfer stays
awaiting_funds. No member-app screen reaches this route
(Developers → Known limitations).
Lithic
Sandbox is the default, and production takes two variables: LITHIC_ENV=production and
LITHIC_ALLOW_PRODUCTION=true. Naming production without the second refuses to talk to Lithic at
all. Nothing Lithic runs until LITHIC_API_KEY is set. Issuing a card also needs
LITHIC_CARD_PRODUCT_ID. Settling card spend on chain signs with CARD_SETTLER_PRIVATE_KEY; which
wallet holds which role is in On-chain authority.
The integration in full: Lithic.
Plaid
PLAID_ENV=production uses Plaid's production; anything else uses its sandbox. It needs
PLAID_CLIENT_ID and PLAID_SECRET for that environment, PLAID_REDIRECT_URI (also allowed in
Plaid's dashboard) for OAuth banks, and PLAID_TOKEN_MASTER_KEY or PLAID_TOKEN_KEYRING_JSON:
linked banks' tokens are stored encrypted, and without a key they cannot be stored.
Stripe
One key, STRIPE_SECRET_KEY, and its prefix is the mode: sk_live_ or rk_live_ is live,
anything else is test. Everything Stripe follows it:
STRIPE_PUBLISHABLE_KEYis used only when its mode matches.- A Connect endpoint receives both live and test events; an event in the other mode is recorded and not acted on.
- A Pay with Clear session in test mode is never offered a card on a live key.
Membership billing also needs STRIPE_YEARLY_MEMBERSHIP_PRICE_ID and
STRIPE_LIFETIME_MEMBERSHIP_PRICE_ID, from the same Stripe account and mode as the key, and the
registry it writes to: MEMBERSHIP_REGISTRY_ADDRESS, MEMBERSHIP_RPC_URL, MEMBERSHIP_CHAIN_ID.
Connect onboarding returns to MERCHANT_APP_URL. Terminal readers have no variables of their own.
SHOP_STRIPE_SECRET_KEY is a different account: the website shop's, where hardware deposits are
taken. The merchant app reads reservations from it and refunds there. Unset, the hardware-orders
routes answer 503 not_configured and the merchant app says hardware orders aren't connected yet.
Coinbase Onramp and Onramper
RAMP_PROVIDER picks one: coinbase when unset, or onramper. Bank deposits are not here; they
are Bridge.
- Coinbase:
CDP_ONRAMP_API_KEY_IDandCDP_ONRAMP_API_KEY_SECRET(falling back to the Send relayer's CDP keys, thenCDP_API_KEY_*).RAMP_SANDBOX=truemarks every order as a sandbox order: it always succeeds and no card is charged. Leave it unset on a live deployment. - Onramper:
ONRAMPER_API_KEY, whose prefix is its mode (pk_test_orpk_prod_).
The relayers
The relayers submit gasless Send and savings transactions.
SEND_RELAYER_MODE: local_key (default), managed_webhook or cdp_server_walletSEND_RELAYER_PRIVATE_KEY; an outside signer at SEND_RELAYER_MANAGED_SIGNER_URL; or a CDP server wallet from SEND_CDP_*SAVINGS_RELAYER_MODE, else Send's: local_key or cdp_server_wallet (default)SAVINGS_RELAYER_PRIVATE_KEY, or a CDP server wallet from SAVINGS_CDP_*A CDP wallet signs on the network it is told (SEND_CDP_NETWORK_<chainId>,
SAVINGS_CDP_NETWORK_<chainId>, or the unsuffixed fallback). A network that does not match the
chain, such as base for chain 84532, stops the transaction
(cdpNetworkGuard.ts).
Two Send defaults suit development only:
SEND_RELAYER_ALLOW_SIMULATION:trueunless set. When the relayer is misconfigured, it returns a simulated transaction hash instead of failing. Set it tofalselive.SEND_RELAYER_REQUIRE_RECEIPT_CONFIRMATION:trueunless set. Leave it on.
Privy
PRIVY_APP_ID (or VITE_PRIVY_APP_ID) and PRIVY_APP_SECRET verify sign-ins; the apps'
VITE_PRIVY_APP_ID must name the same app. There is no test switch in the code: an environment is
whichever Privy app these name, and each app's allowed origins are set in Privy's dashboard.
Merchant organisation wallets also need PRIVY_AUTHORIZATION_PRIVATE_KEY (its public half,
PRIVY_AUTHORIZATION_PUBLIC_KEY, is registered as a signer on each), and gas sponsorship switched
on in Privy's dashboard for the chain in use. Without the private key, a merchant withdrawal is
recorded as a request and says so.
Email and texts
- Email goes by Resend whenever
RESEND_API_KEYis set, fromRESEND_FROMon a domain verified in Resend. The Command Center's invites use it too. - Texts follow
SEND_NOTIFICATION_PROVIDER:mock(the default, logs and sends nothing),twilio, orgeneric_webhook(posts toSEND_NOTIFICATION_WEBHOOK_URL). Twilio needsSEND_TWILIO_ACCOUNT_SID,SEND_TWILIO_AUTH_TOKEN, and a messaging service or a from number. Receipts, Send claims and Clear Terminal's table-ready and order-ready texts all go this way. - Push needs
VAPID_PUBLIC_KEY,VAPID_PRIVATE_KEYandVAPID_SUBJECT, with the member app'sVITE_VAPID_PUBLIC_KEYthe same public key; without them push is skipped.
Analytics
The member and merchant apps load Umami from VITE_UMAMI_SRC and VITE_UMAMI_WEBSITE_ID, and only
on their live hostnames. The API reads visits back for the Command Center with UMAMI_API_URL,
UMAMI_USERNAME, UMAMI_PASSWORD and the site ids (UMAMI_WEBSITE_ID, UMAMI_MEMBER_WEBSITE_ID,
UMAMI_MERCHANT_WEBSITE_ID).
Off until set
Some features stay inert until a variable names them, so a deployment can go live without them:
CLEAR_FEE_COLLECTION_ADDRESS (collecting Clear's fee from a shop's wallet; until set, fees are
billed and wait as due), PROTOCOL_EVENT_LISTENER and PORTFOLIO_SNAPSHOTS
(Deploying the server). Also:
VERCEL_TOKEN and VERCEL_REWARDS_PROJECT_ID, with VERCEL_TEAM_ID when the project is a team's (not in env.example)pending and never added to Vercel or verified (vercel.ts)SHOP_STRIPE_SECRET_KEYAUTO_TERM_LIMITS (not in env.example)84532) only. on runs it on any chain; any other value turns it off everywhere (termLimitService.ts). The rule's figures are proposals not yet signed off (Governance → Parameters)Webhooks
Every webhook route is public and checks a signature over the raw request body. The API keeps
the raw bytes of every JSON request for this. Give each provider https://<api-host> followed by
the path.
/api/webhooks/bridgeX-Webhook-Signature: RSA-SHA256 over the timestamp and body, against Bridge's public key; older than 5 minutes is refused (SEND_BRIDGE_WEBHOOK_MAX_AGE_SECONDS)BRIDGE_WEBHOOK_PUBLIC_KEY; BRIDGE_WEBHOOK_PUBLIC_KEYS for rotation/api/webhooks/lithicwebhook-id, webhook-timestamp, webhook-signature)LITHIC_WEBHOOK_SECRET/api/webhooks/lithic/auth-streamLITHIC_ASA_SECRET, else LITHIC_WEBHOOK_SECRET/api/stripe/webhooks/membershipStripe-SignatureSTRIPE_MEMBERSHIP_WEBHOOK_SECRET, else STRIPE_WEBHOOK_SECRET/api/stripe/webhooks/connectStripe-SignatureSTRIPE_CONNECT_WEBHOOK_SECRET/api/webhooks/coinbase-rampX-Hook0-Signature: HMAC-SHA256 over the timestamp and body; older than 5 minutes is refusedCDP_RAMP_WEBHOOK_SECRET/api/webhooks/onramperONRAMPER_WEBHOOK_SECRET, ONRAMPER_WEBHOOK_SECRET_PROD: either matches/api/send/webhooks/bridge-transferX-Send-Bridge-Webhook-SecretSEND_BRIDGE_WEBHOOK_PUBLIC_KEY(S), or SEND_BRIDGE_WEBHOOK_SECRET/api/send/webhooks/payoutgeneric_webhook modeX-Send-Webhook-Secret equals the secretSEND_PAYOUT_WEBHOOK_SECRET/api/command/webhooks/plaidPlaid-Verification: an ES256 JWT, signed with Plaid's key, issued within 5 minutes, carrying the body's SHA-256What a route does when its secret is not set differs, and three accept unsigned requests. Set every secret on a live deployment.
bridge-transferSUSPECTED_FRAUD)payoutThe Command Center's Plaid webhook is registered with Plaid from COMMAND_PLAID_WEBHOOK_URL, and
only for the sandbox link it makes (The Command Center's live switches).
The API has no route for Twilio's delivery callbacks: SEND_TWILIO_STATUS_CALLBACK_URL is passed to
Twilio as it is.
Webhooks Clear sends
Pay with Clear sends shops' own servers signed events (Clear-Signature), retried for 3 days. That
is a shop's integration, documented for developers: Webhooks.