Skip to content

Production environment variables

Guide for configuring Sagio in production. Covers what to set, where to set it, and how it differs from local dev.

For auth architecture context, see auth-migration-handoff.md.


Where variables live

Sagio is a monorepo with four deployable surfaces. Each uses a different config mechanism:

Surface Host Runtime config Build-time config
API Cloudflare Workers (api.sagio.io) wrangler secret put wrangler.jsonc vars (non-secret)
Dashboard Cloudflare Pages (merchant.sagio.io) Pages env vars (VITE_*) at build
Wallet Cloudflare Pages (app.sagio.io) Pages env vars (VITE_*) at build
Landing Cloudflare Pages (sagio.io) None required
Terminal Android APK local.properties (build-time) Gradle / signing config

Rules:

  • Workers do not read apps/api/.env at runtime. Use wrangler secret put (secrets) or Wrangler [vars] (non-secrets).
  • Frontends bake in VITE_* values at vite build. Changing a Pages env var requires a rebuild/redeploy.
  • Never commit secrets. Keep a local .env or .env.local as your source of truth and push values to Cloudflare with the commands below.
  • Generate production secrets separately from dev. Do not copy local .env wholesale into prod.

Production deploy checklist

  1. Set API Worker secrets (auth, DB, integrations).
  2. Set NODE_ENV=production in Wrangler vars.
  3. Run DB migrations against the production Neon database (migrations do not auto-run in prod).
  4. Register Google OAuth redirect URI: https://api.sagio.io/api/auth/callback/google.
  5. Set VITE_API_BASE_URL on Dashboard and Wallet Pages projects, then redeploy.
  6. Smoke-test Google login on dashboard and wallet.
  7. Verify GET https://api.sagio.io/api/auth/jwks returns keys after first login.

Setting API secrets (Cloudflare Workers)

From apps/api:

cd apps/api
wrangler login   # Sagio org account

# Interactive — paste value when prompted
wrangler secret put BETTER_AUTH_SECRET

# Or pipe from a local file (no trailing newline)
printf '%s' 'your-secret-value' | wrangler secret put BETTER_AUTH_SECRET

Worker environments (see wrangler.jsonc):

Target Flag Worker name
Production (none) sagio-api-prod
Dev/staging --env=sagio-api-dev sagio-api-dev

Example for dev worker:

printf '%s' 'your-value' | wrangler secret put BETTER_AUTH_SECRET --env=sagio-api-dev

Secrets apply immediately; no redeploy required. Changing BETTER_AUTH_SECRET after users have logged in invalidates existing JWKS keys — see JWKS troubleshooting.

Do not rely on the Cloudflare Dashboard for non-secret Worker vars (e.g. BETTER_AUTH_URL). Any value set only in the dashboard is replaced on the next wrangler deploy from CI. Put durable public config in apps/api/wrangler.jsonc under [vars].

Helper script (Fasstap only today):

./src/scripts/deployFasstapSecrets.sh .env.local          # prod
./src/scripts/deployFasstapSecrets.sh .env.local sagio-api-dev  # dev worker

API (apps/api) — Cloudflare Workers

Core / runtime

Variable Required How to set Production value Notes
NODE_ENV Yes Wrangler [vars] production Enables encrypted JWKS, Neon serverless driver, skips auto-migrations.
DATABASE_URL Yes wrangler secret put postgresql://…@…/…?sslmode=require Neon connection string. Only DB var needed at runtime (DB_* vars are local-only).
PORT No Wrangler var 8787 Ignored on Workers; relevant for local bun run dev only.

Authentication (Better Auth)

Variable Required How to set Production value Notes
BETTER_AUTH_SECRET Yes Secret Random 32+ chars openssl rand -base64 32. Must be set before first prod login. Separate from dev.
BETTER_AUTH_URL Yes Wrangler [vars] (recommended) or secret https://api.sagio.io API origin only — no /api suffix. Must live in wrangler.jsonc so CI deploys do not wipe it. Dashboard-only env vars are removed on every wrangler deploy.
GOOGLE_CLIENT_ID Yes Secret Web OAuth client ID Same client as dev is OK if both redirect URIs are registered in Google Cloud.
GOOGLE_CLIENT_SECRET Yes Secret Web OAuth client secret
JWT_SECRET Yes Secret Random 32+ chars Legacy HS256 tokens (merchant-app fallback). Use a different value than BETTER_AUTH_SECRET.
ALLOWED_ORIGINS Recommended Secret or var Comma-separated origins Optional override. When set, https://merchant.sagio.io, https://app.sagio.io, and https://sagio.io are always merged in — you do not need to repeat them.
CAPACITOR_ALLOWED_ORIGINS Required for native wallet Wrangler var https://localhost,capacitor://localhost Exact Android/iOS WebView origins used for CORS and Better Auth origin validation. Production does not trust localhost origins unless this is explicitly set; never use a wildcard.

Important (Cloudflare Workers): Auth and CORS config must be read at request time via buildAuthConfig() / getAllowedOrigins(), not from the static config object imported at module load. Worker secrets exist in c.env and are injected per-request; a module-load snapshot sees empty GOOGLE_CLIENT_ID even when wrangler secret put succeeded. Deploy the latest API code that uses runtime config resolution.

Google Cloud Console (Web OAuth client):

  • Authorized redirect URI: https://api.sagio.io/api/auth/callback/google
  • Authorized JavaScript origins: https://app.sagio.io, https://merchant.sagio.io, https://sagio.io, etc.

Cross-subdomain cookies: Dashboard on merchant.sagio.io (or app.sagio.io) calls api.sagio.io for /api/auth/token. The Better Auth session cookie must be shared across .sagio.io. This may require a crossSubDomainCookies config in apps/api/src/auth/betterAuth.ts — validate after deploy.

Blockchain (Base)

Variable Required How to set Production value Notes
BASE_SEPOLIA_RPC_URL Recommended Secret Alchemy/Infura Base Sepolia URL Preferred RPC for testnet.
BASE_RPC_URL Fallback Secret Base RPC URL Used only if BASE_SEPOLIA_RPC_URL is unset and URL looks like Sepolia.
BASE_ACCOUNT_ADDRESS Optional Secret 0x… Account abstraction / paymaster related.
BASE_PAYMASTER_ADDRESS Optional Secret 0x…

For mainnet, point RPC URLs at Base mainnet and update token contract addresses in frontends.

Circle Programmable Wallets

Variable Required How to set Production value Notes
CIRCLE_API_KEY For treasury Secret Circle API key
CIRCLE_ENTITY_SECRET For treasury Secret 32-byte hex Register once with Circle via setup-circle-wallet-set script.
CIRCLE_TREASURY_WALLET_ID For treasury Secret UUID Developer-controlled SCA wallet.
CIRCLE_MERCHANT_WALLET_SET_ID For merchants Secret UUID Wallet set for per-merchant settlement wallets.
CIRCLE_BLOCKCHAIN Yes Secret or var BASE-SEPOLIA or BASE Use BASE for mainnet. Hyphens, not underscores.
CIRCLE_BASE_URL No Secret or var https://api.circle.com/v1/w3s Default is Circle production API.

Fasstap / Worldline (Tap-to-pay terminal)

Variable Required How to set Production value Notes
FASSTAP_ACCESS_KEY Yes (terminal) Secret From Soft Space Served to Android app via /api/merchant-app/fasspay-config.
FASSTAP_SECRET_KEY Yes (terminal) Secret From Soft Space
FASSTAP_UNIQUE_ID Yes (terminal) Secret Soft Space MUID
FASSTAP_DEVELOPER_ID Yes (terminal) Secret Soft Space SSO/dev ID
FASSTAP_SSO_ID For Worldline API Secret Soft Space SSO ID Backend-only; Worldline REST calls.
FASSTAP_ENVIRONMENT Yes Secret or var PROD Use UAT for sandbox. Selects default attestation/keyloading hosts.
FASSTAP_ATTESTATION_HOST Override Secret Prod host from Soft Space Default baked in for UAT/PROD.
FASSTAP_KEYLOADING_HOST Override Secret Prod host from Soft Space
FASSTAP_ATTESTATION_CERT_PINNING Override Secret sha256/…
FASSTAP_KEYLOADING_CERT_PINNING Override Secret sha256/…
FASSTAP_KEYLOADING_CA_CERT Override Secret PEM (single line, \n)
FASSTAP_GPLAY_PROJECT_NUMBER Override Secret Play Integrity project # Defaults to Soft Space UAT value.
WORLDLINE_CALLBACK_SECRET Recommended Secret Random string Validates inbound Worldline webhooks. Required in production if callbacks are enabled.

See 003-fasstap-configuration.md for full Fasstap setup.

AI / RAG

Variable Required How to set Production value Notes
GEMINI_API_KEY For Gemini features Secret Google AI key Insights, embeddings fallback.
GEMINI_BASE_URL No Secret or var https://generativelanguage.googleapis.com/v1beta
XAI_API_KEY For chat/LLM Secret xAI key Used by llmStream.ts and health check.
XAI_BASE_URL No Secret or var https://api.x.ai/v1
AI_MODEL No Secret or var e.g. grok-4-1-fast-non-reasoning
EMBEDDING_MODEL No Secret or var text-embedding-004
CHAINGPT_API_KEY Optional Secret ChainGPT key
DEEPGRAM_API_KEY For voice agent Secret Deepgram key Wallet voice feature proxies via API.
DEEPGRAM_AGENT_ENDPOINT No Secret or var wss://agent.deepgram.com/v1/agent/converse
DEEPGRAM_STT_MODEL No Var nova-3
DEEPGRAM_TTS_MODEL No Var aura-2-thalia-en
DEEPGRAM_LLM_PROVIDER No Var open_ai
DEEPGRAM_LLM_MODEL No Var gpt-4o-mini

Cloudflare Vectorize (RAG)

Variable Required How to set Production value Notes
CLOUDFLARE_ACCOUNT_ID For Vectorize Secret CF account ID Or VECTORIZE_ACCOUNT_ID.
CLOUDFLARE_API_TOKEN For Vectorize Secret Token with Vectorize RW Or VECTORIZE_API_TOKEN.
VECTORIZE_INDEX No Secret or var sagio-merchant Or VECTOR_INDEX_NAME.

Legacy alternate auth (email + global API key) also checked in vectorizeService.ts: CLOUDFLARE_API_EMAIL, CLOUDFLARE_API_KEY.

ENS (subname provisioning)

Variable Required How to set Production value Notes
ENS_RPC_URL For ENS mint Secret Ethereum RPC (Sepolia or mainnet)
ENS_PARENT_NAME No Secret or var sagio.eth
ENS_CHAIN No Secret or var sepolia or mainnet
ENS_OWNER_PRIVATE_KEY For mint Secret 0x… Highly sensitive. Signs subname txs.
ENS_REGISTRY_ADDRESS Override Secret Contract address
ENS_DEFAULT_RESOLVER Override Secret Resolver address
ENS_WAIT_CONFIRMATIONS No Var 1 (default) Set 0 to skip waiting.
ENS_SET_ADDR No Var 1 (default) Set 0 to skip.
ENS_TRANSFER_SUBNAME No Var 1 (default) Set 0 to skip.

Hackathon / NBR Reader Rewards

Variable Required How to set Production value Notes
HACKATHON_DNZD_TOKEN_ADDRESS For dNZD drops Secret or var 0x… Base Sepolia token contract.
HACKATHON_DNZD_AMOUNT No Secret or var 10 Amount per signup.
HACKATHON_DNZD_DECIMALS No Secret or var 6 Override; else read on-chain.
HACKATHON_ADMIN_EMAILS For admin console Secret or var a@…,b@… Comma-separated allowlist for hackathon routes.
NBR_SETTLEMENT_MODE Optional Secret or var onchain or mock Reader Rewards settlement behavior.
HACKATHON_SIGNIN_TEST_URL Load test only Secret URL Not for production.

Market data / misc

Variable Required How to set Production value Notes
CMC_API_KEY For prices Secret CoinMarketCap key
CHAT_DEBUG No Var 0 Set 1 only for debugging.
API_PUBLIC_URL Fallback Secret or var https://api.sagio.io Fallback for BETTER_AUTH_URL.
PUBLIC_API_BASE_URL Fallback Secret or var https://api.sagio.io/api Used in merchant-app deep links.

Legacy / health-check (Privy — being removed)

These are still referenced by /api/health but Privy is deprecated in favor of Better Auth:

Variable Status Notes
VITE_PRIVY_APP_ID Legacy Health check marks degraded if missing. Safe to leave unset; update health check when Privy is fully removed.
PRIVY_APP_SECRET Legacy Same.
PRIVY_WALLET_ID Legacy Hackathon on-chain settlement (if used).
PRIVY_WALLET_AUTHORIZATION_PRIVATE_KEY Legacy

Local-only (do not set on Workers)

These appear in apps/api/.env.example for local tooling only:

  • DB_USER, DB_PASSWORD, DB_HOST, DB_DATABASE, DB_PORT — use DATABASE_URL in production instead.
  • GOOGLE_REDIRECT_URI — Better Auth derives redirect from BETTER_AUTH_URL; legacy utils/auth.ts only.

Dashboard (apps/dashboard) — Cloudflare Pages

Set in Cloudflare Pages → sagio-merchant → Settings → Environment variables (Production), then trigger a new deployment.

Variable Required Production value Notes
VITE_API_BASE_URL Yes https://api.sagio.io/api Must include /api suffix.
VITE_BASE_SEPOLIA_RPC_URL Recommended Alchemy Base Sepolia URL Used for on-chain reads in dashboard.
VITE_NZDD_ADDRESS Recommended 0x… NZDD contract on Base Sepolia/mainnet.
VITE_ENS_PARENT_DOMAIN No sagio.eth Default if unset.
VITE_MAINNET_RPC For ENS reads Ethereum mainnet RPC Dashboard ENS resolution.
VITE_USD_TO_NZD_RATE No 1.65 Display conversion fallback.

Deploy:

cd apps/dashboard
VITE_API_BASE_URL=https://api.sagio.io/api bun run build
bun run deploy   # wrangler pages deploy → sagio-merchant

Or rely on Pages CI with env vars configured in the dashboard UI.


Wallet (apps/wallet) — Cloudflare Pages

Set in Cloudflare Pages → sagio-app → Settings → Environment variables (Production).

Variable Required Production value Notes
VITE_API_BASE_URL Yes https://api.sagio.io/api Must include /api suffix.
VITE_BASE_SEPOLIA_RPC_URL Recommended Alchemy Base Sepolia URL Falls back to public RPC with a console warning.
VITE_DNZD_ADDRESS Recommended 0x… dNZD token in wallet registry.
VITE_NZDD_ADDRESS Recommended 0x… NZDD token in wallet registry.
VITE_ENS_CHAIN No sepolia or mainnet
VITE_ENS_RPC For ENS Ethereum RPC URL
VITE_ENS_PARENT_DOMAIN No sagio.eth
VITE_MAINNET_RPC Fallback ENS Ethereum mainnet RPC
VITE_USD_TO_NZD_RATE No 1.65
VITE_OAUTH_REDIRECT_URL Capacitor https://app.sagio.io/oauth/callback Required for native Google OAuth deep links.
VITE_FACILITATOR_BASE_URL Optional Facilitator service URL x402 / payment facilitator.
VITE_LOG_ENDPOINT Optional HTTPS log ingest URL Remote error logging.
VITE_LOG_TO_CONSOLE No false

Wallet — Privy (legacy, migration in progress)

Better Auth replaces Privy for login. These remain for wallet signing until Circle migration is complete:

Variable Status Notes
VITE_PRIVY_APP_ID Legacy Still read by app-providers.tsx.
VITE_BASE_PAYMASTER_POLICY_ID Optional Gas sponsorship via Privy paymaster.
VITE_PRIVY_PAYMASTER_POLICY_ID Optional Alias for paymaster policy.
VITE_SMART_WALLET_PAYMASTER_CONTEXT_JSON Optional Full paymaster context JSON.

Deploy:

cd apps/wallet
VITE_API_BASE_URL=https://api.sagio.io/api bun run build
bun run deploy   # wrangler pages deploy → sagio-app

Landing (apps/landing)

No required environment variables for production. Static marketing site.

cd apps/landing
bun run deploy:cf

Android terminal (apps/terminal)

Build-time config in apps/terminal/local.properties (not committed):

Property Required Production value Notes
GOOGLE_WEB_CLIENT_ID Yes Same as API GOOGLE_CLIENT_ID Web OAuth client; used as Credential Manager serverClientId.
SAGIO_API_BASE_URL Yes https://api.sagio.io/api/ Trailing slash optional; must include /api.

Also create an Android OAuth client in Google Cloud:

The legacy API-key path (SAGIO_API_KEY / SAGIO_API_SECRET) is removed; terminal auth is Better Auth JWT only.


Dev vs production — key differences

Concern Local dev Production
Config file apps/api/.env wrangler secret put + Wrangler vars
BETTER_AUTH_URL http://localhost:8787 https://api.sagio.io
VITE_API_BASE_URL http://localhost:8787/api https://api.sagio.io/api
JWKS private keys Stored unencrypted (NODE_ENV=development) Stored encrypted (NODE_ENV=production)
DB migrations Auto-run on API start Run manually against prod DB
Google redirect http://localhost:8787/api/auth/callback/google https://api.sagio.io/api/auth/callback/google
Fasstap FASSTAP_ENVIRONMENT=UAT FASSTAP_ENVIRONMENT=PROD
Secrets Can share one Neon DB for solo dev Separate secrets and preferably separate DB

JWKS / Better Auth troubleshooting

TypeError: JWK must be an object on login

The jwks table has key material that does not match the current encryption mode or secret. Common causes:

  • Dev DB row was created with encryption on, but local dev reads keys unencrypted.
  • BETTER_AUTH_SECRET was changed after keys were created.

Fix (dev or prod):

cd apps/api
bun run scripts/reset-better-auth-jwks.ts   # deletes all rows in jwks
# restart API, sign in again — new keys are created automatically

Or run DELETE FROM jwks; directly against the affected database.

Rotating BETTER_AUTH_SECRET

  1. Plan for all users to re-authenticate.
  2. Clear the jwks table (script above).
  3. Set the new secret via wrangler secret put BETTER_AUTH_SECRET.
  4. Restart / redeploy is not required for secrets, but clear any cached auth instance if testing locally.

First production login

Set BETTER_AUTH_SECRET before the first user signs in. The first login creates the JWKS row encrypted with that secret. Do not copy the dev jwks table to production.


Quick reference — minimum prod secrets

API Worker (must-have for auth + DB):

cd apps/api
wrangler secret put DATABASE_URL
wrangler secret put BETTER_AUTH_SECRET
# BETTER_AUTH_URL is in wrangler.jsonc [vars] — do not use dashboard-only env vars
wrangler secret put GOOGLE_CLIENT_ID
wrangler secret put GOOGLE_CLIENT_SECRET
wrangler secret put JWT_SECRET

Pages (must-have per frontend):

VITE_API_BASE_URL=https://api.sagio.io/api

Terminal (local.properties):

GOOGLE_WEB_CLIENT_ID=<same as GOOGLE_CLIENT_ID>
SAGIO_API_BASE_URL=https://api.sagio.io/api/