Jose Madrid Salsa uses environment variables for all secrets, API keys, and runtime configuration. Variables prefixed with NEXT_PUBLIC_ are exposed to the browser; all others are server-only.
Never commit .env files to version control. Use .env.example as a reference template and configure real values in Vercel or your local .env.local file.
A sign-in button appears only when both of its provider's variables are set, so each Vercel project
(storefront and fundraising) shows only the providers it has keys for. Each site's domain also needs
its callback, such as https://fundraising.josemadridsalsa.com/api/auth/callback/google, listed as
an authorized redirect URI with the provider.
64-character hex string (32 bytes) for AES-256-GCM encryption of service keys. Generate with node -e "console.log(require('crypto').randomBytes(32).toString('hex'))".
ENCRYPTION_KEY
Yes
Base64-encoded key for SMTP password encryption. Generate with node -e "console.log(require('crypto').randomBytes(64).toString('base64'))".
Bearer token Vercel Cron sends to /api/cron/*. Outside production a missing value is allowed; in production a missing value makes every cron route refuse, rather than leaving them world-callable. See Scheduled Jobs.
Default sender address. Defaults to orders@josemadridsalsa.com.
BUSINESS_POSTAL_ADDRESS
No
Physical mailing address printed in marketing-email footers, as CAN-SPAM requires. Defaults to the Zanesville address.
FUNDRAISING_REPLY_TO
No
Reply-to address on fundraiser re-signup invitations. Defaults to the owner's address.
NEXT_PUBLIC_FROM_EMAIL
No
Sender for queued bulk email. Falls back to FROM_EMAIL.
EMAIL_CHUNK_BUDGET_MS
No
Time budget per email-queue run, in ms. Defaults to 45000.
EMAIL_SEND_DELAY_MS
No
Delay between queued sends, in ms (keeps under Resend's rate limit). Defaults to 550.
EMAIL_IMAGE_BASE_URL
No
Absolute base URL for email header and footer images. Email clients cannot resolve relative paths, so every image needs a full URL. Defaults to the email-templates folder of the josemadridsalsa-blob Vercel Blob store, which npm run email:upload-images --workspace @jose-madrid/storefront -- --apply fills from public/email-templates. Leave it unset unless the images move to another host. No trailing slash.
UploadThing v7 token (dashboard → API Keys → V7). The v6 UPLOADTHING_SECRET / UPLOADTHING_APP_ID pair is no longer read.
BLOB_READ_WRITE_TOKEN
For image hosting
Read/write token for the josemadridsalsa-blob Vercel Blob store. Powers the Developer Console file explorer (/admin/developer/files), the product image uploader, the images:sync / images:migrate scripts, and email:upload-images. Get it from Vercel → Storage → josemadridsalsa-blob. Without it, every blob upload path is disabled and returns 503.
The Client ID and Secret are optional here — the preferred place to enter them is
Admin → Settings → Integrations, which stores the secret encrypted in the database. These
variables exist as a fallback and are read only when no admin-entered credentials are present.
QUICKBOOKS_ENVIRONMENT has no admin-panel equivalent and must be set to reach real company
data: it defaults to sandbox, and the Connect button on the integrations page connects to
whichever environment it names. Left unset in production, the flow completes successfully against
an Intuit sandbox company and syncs nothing real.
Variable
Required
Description
QUICKBOOKS_ENVIRONMENT
To reach live books
sandbox (default) or production. Selects both the Intuit API host and which stored credential set is used.
The redirect URI is derived from NEXTAUTH_URL as {NEXTAUTH_URL}/api/integrations/quickbooks/callback
and must be registered verbatim in the Intuit app. Sandbox and production keys are different pairs,
and credentials are stored per environment — production keys saved while the app is in sandbox mode
are filed under sandbox and will not work.
BigCommerce is the system of record for catalog, carts, checkout and orders; this app is a
headless storefront on top of it. Jose Madrid runs two BigCommerce stores, and each needs its
own store-level API account (BigCommerce admin → Settings → API → Store-level API accounts).
Product ids are per store, so the two sets must never be swapped. A store with any of its four
variables unset is treated as not configured.
Variable
Required
Description
BIGCOMMERCE_STORE_HASH
For the storefront
Main store (josemadridsalsa.com) hash — the part after /stores/ in the API path.
BIGCOMMERCE_ACCESS_TOKEN
For the storefront
Main store API account access token.
BIGCOMMERCE_CLIENT_ID
For the storefront
Main store API account client ID.
BIGCOMMERCE_CLIENT_SECRET
For the storefront
Main store API account client secret; also signs customer-login tokens.
BIGCOMMERCE_FUNDRAISING_STORE_HASH
For fundraising orders
Fundraising store (josemadridsalsafundraising.com) hash.
Origin of the fundraising app (apps/fundraising): its canonicals, sitemap and robots, and where the main site sends its old fundraiser paths. Set it in both Vercel projects. Defaults to https://fundraising.josemadridsalsa.com.
NEXT_PUBLIC_COMMERCE_BACKEND
At cutover
Set to bigcommerce to switch the storefront over: product prices and stock come from BigCommerce, and retail carts check out in BigCommerce's hosted checkout. Unset, the site prices and checks out as before. Fundraiser carts keep this site's checkout either way. Build-time: redeploy after changing it.
BIGCOMMERCE_CHANNEL_ID
No
BigCommerce channel carts are created in. Defaults to 1, the existing storefront, whose checkout is served on josemadridsalsa.com — correct while that domain still points at BigCommerce. Once the domain points at this site, set it to the headless channel whose checkout domain is a subdomain.
BIGCOMMERCE_ORDERS_TO_QUICKBOOKS
No
true lets orders copied from either BigCommerce store (importSource = bigcommerce or bigcommerce-fundraising) flow into the hourly QuickBooks sync as sales receipts. Off by default: if online revenue already reaches QuickBooks another way, such as the Stripe and PayPal deposits, syncing these too would count every retail sale twice. Decide with whoever keeps the books before turning it on.
BIGCOMMERCE_STOREFRONT_URL
At domain cutover
Where the old BigCommerce storefront lives once josemadridsalsa.com points here, e.g. https://shop.josemadridsalsa.com. When set, legacy login.php, account.php, giftcertificates.php and wishlist.php links go there (temporary redirects, query string kept) so customers reach their BigCommerce account; unset, they go to this site's pages. Build-time. See the cutover runbook.
BIGCOMMERCE_WEBHOOK_SECRET
For live catalog updates
Shared secret BigCommerce sends in a header on every webhook; /api/webhooks/bigcommerce refuses calls without it (and refuses everything when it is unset). Any long random string. Register the hooks after deploying with npm run bigcommerce:webhooks --workspace @jose-madrid/storefront -- https://<site-origin> (add --store fundraising for the fundraising store), run with production's values; re-run it to revive hooks BigCommerce disabled after an outage.
The API accounts need modify on Products, Carts, Checkouts, Customers, Channel settings and
Sites & routes, and read-only on Orders and Information & settings. The main store's account
also needs Themes: modify (and Content: modify is useful) for the checkout-theme tooling
(npm run bigcommerce:theme). A 403 You don't have a required scope on every call means the account was saved with no scopes; a 401 means the token
or store hash is wrong.
Legacy aliases for the SHIPPING_ORIGIN_* variables above, still read as a fallback. Prefer SHIPPING_ORIGIN_*
ORDER_SMS_ALERTS_ENABLED
No
Set to "true" to show the text-alert opt-in on /order-tracking. Leave unset until an SMS provider is wired up, or customers will sign up for texts that never arrive
Set the warehouse address
Rate quoting and label purchase both read the warehouse address through
lib/shipping/origin.ts, which takes Settings → Shipping first and these variables second.
Until one of the two is complete, no real carrier rate can be fetched — checkout falls back to
flat-rate estimates and logs why.
They used to be configured separately: quoting read SHIPPING_ORIGIN_* and defaulted to a
placeholder address in San Francisco when unset, while label purchase read SHIP_FROM_*. A
half-configured deployment therefore quoted customers from one address and shipped from another,
with nothing in the response to indicate it.
The Gmail triage reuses the Google OAuth client and the Anthropic key; it adds no variables
of its own. The mailbox grant itself is stored encrypted in the database, not in the
environment — connect it at /admin/inbox/settings.
Variable
Purpose
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET
The OAuth client the Gmail grant is made against. Needs gmail.modify and gmail.send on its consent screen, and /api/admin/inbox/google/callback as a redirect URI.
ANTHROPIC_API_KEY
Classifies each message and writes the reply. Without it every email escalates to a human rather than going unread.
ANTHROPIC_MODEL
Optional override; defaults to claude-opus-5.
CRON_SECRET
Guards /api/cron/inbox-triage, as it does every scheduled route.
Shared secret for internal service-to-service API calls
ADMIN_SECRET_TOKEN
No
An admin_token cookie equal to this value is treated as an admin session (lib/admin-auth.ts). Leave unset in production.
IMPORT_SECRET
No
Required by /api/import-locations-secret; unset disables that route
APP_URL
No
Base URL for fundraiser approval, share-shield and donation links. Falls back to the request origin.
SALSADOCS_GITHUB_TOKEN
For docs publishing
GitHub token the Developer Console uses to publish to the salsadocs repo
SALSADOCS_GITHUB_REPO
No
Defaults to jordolang/salsadocs
SALSADOCS_GITHUB_BRANCH
No
Defaults to main
SALSADOCS_CONTENT_DIR
No
Defaults to content/docs
NEXT_PUBLIC_SITE_URL
Production
The site's public origin, e.g. https://www.josemadridsalsa.com (the default). Canonical links, the sitemap, structured data, feeds, email links and the domain printed on invoices and packing slips are all built from it (lib/site-url.ts). Build-time: redeploy after changing it.
NEXT_PUBLIC_BASE_URL
No
Public-facing base URL used by some email links and product feeds. Falls back to NEXT_PUBLIC_SITE_URL.
NEXT_PUBLIC_APP_URL
No
Base URL for share links and order-rule emails. Falls back to NEXTAUTH_URL, then NEXT_PUBLIC_SITE_URL.
Moving the site to another domain: set NEXT_PUBLIC_SITE_URL, NEXT_PUBLIC_BASE_URL,
NEXT_PUBLIC_APP_URL and NEXTAUTH_URL to the new origin in Vercel (production), update
Admin → SEO → Site URL (SeoConfiguration.siteUrl), and redeploy. No code change is needed.
HMAC secret for POST /api/fundraiser/sale (lib/fundraiser-auth.ts)
ARENA_SHARE_SECRET
For share shields
Signs arena share nonces. At least 32 characters; share links fail without it.
ARENA_GAME_ORIGINS
No
Extra Battle Arena 3D game origins (comma separated, e.g. a custom domain) allowed to receive game sign-in tokens and call the game API. The published game is always allowed.
Set these in apps/agent/.env.local, not the storefront's.
Variable
Required
Description
AI_GATEWAY_API_KEY
One of these
Vercel AI Gateway key used to reach the agent's model
VERCEL_OIDC_TOKEN
One of these
Pulled automatically by eve link; the alternative to an AI Gateway key
STOREFRONT_API_URL
No
Base URL the agent's read-only tools call. Defaults to https://www.josemadridsalsa.com. Point it at http://localhost:3000 to run against a local storefront.
Read from the environment of the Hermes cron job that runs the agent. All are optional.
Variable
Description
ANTHROPIC_API_KEY
Calls Claude directly. Without it the agent uses the Hermes Anthropic provider.
EMAIL_AGENT_MODEL
Model used to classify each message. Defaults to claude-sonnet-4-5.
EMAIL_AGENT_TIMEOUT_MS
Time limit for one message's model call. Defaults to 120000; an invalid value also falls back to it. A message that runs over is listed for the owner and retried at the next review.
EMAIL_AGENT_QUERY
Gmail search for the inbox review. Defaults to the last 7 days, excluding Promotions and Social.
EMAIL_AGENT_MAX
Most new messages analyzed per review. Defaults to 25.
EMAIL_AGENT_SEARCH_MAX
How many matching inbox messages are searched to find new ones, since processed mail stays in the inbox. Defaults to 500.
EMAIL_AGENT_STATE_PATH
Where processed message IDs are kept. Defaults to email-agent/state.json under the Hermes home.
HERMES_HOME
Hermes profile directory holding the Google OAuth token. Defaults to ~/.hermes.
PYTHON_BIN
Python with the Google API client installed, used for Gmail calls. Defaults to python.
Build-time variables for the Expo app, not the storefront. Set them in the shell or in an EAS
build profile (apps/fundraiser-app/eas.json).
Variable
Required
Description
SQUARE_APPLICATION_ID
No
Square application id built into the app for card payments. Defaults to the shop's production app id (set on the Square plugin in app.json); set a sandbox app id for a sandbox build. Not a secret.
EXPO_PUBLIC_API_URL
No
Storefront the app talks to. Defaults to https://www.josemadridsalsa.com (extra.apiUrl in app.json). Point a development build at a local storefront on your network, e.g. http://192.168.1.20:3000.