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.prod.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/.envat runtime. Usewrangler secret put(secrets) or Wrangler[vars](non-secrets). - Frontends bake in
VITE_*values atvite build. Changing a Pages env var requires a rebuild/redeploy. - Never commit secrets. Keep a local
.envor.env.localas your source of truth and push values to Cloudflare with the commands below. - Generate production secrets separately from dev. Do not copy local
.envwholesale into prod.
Production deploy checklist¶
- Set API Worker secrets (auth, DB, integrations).
- Set
NODE_ENV=productionin Wrangler vars. - Run DB migrations against the production Neon database (migrations do not auto-run in prod).
- Register Google OAuth redirect URI:
https://api.sagio.io/api/auth/callback/google. - Set
VITE_API_BASE_URLon Dashboard and Wallet Pages projects, then redeploy. - Smoke-test Google login on dashboard and wallet.
- Verify
GET https://api.sagio.io/api/auth/jwksreturns keys after first login. - Verify
bun run --cwd apps/api verify:prodconfirms thatapi.sagio.iois attached tosagio-api-prod, reports the production Worker identity, and can initiate Google OAuth.
Deployment isolation invariants¶
apps/api/wrangler.prod.jsoncis the only configuration allowed to claim theapi.sagio.ioCustom Domain.apps/api/wrangler.dev.jsonchas an explicit empty route list. Never add the production hostname to it as a temporary testing shortcut.- Production deploys run only from
master, through the GitHubproductionenvironment. Directwrangler deploycommands must always pass an explicit config file. - CI verifies live Cloudflare domain ownership before touching the production database and rechecks domain ownership, Worker identity, health, Google OAuth initiation, and JWKS after deployment.
/api/healthreportsdeployment.service,deployment.environment, and Cloudflare version metadata. A request forapi.sagio.iois degraded unless it is served bysagio-api-prodin the production environment..github/workflows/production-api-smoke.ymlrepeats the public identity and auth checks every ten minutes and alerts the deployment log channel on failure.
Cloudflare dashboard edits are emergency operations, not durable configuration. After an emergency change, run bun run --cwd apps/api verify:prod and reconcile the repository configuration immediately.
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 --config=wrangler.prod.jsonc
# Or pipe from a local file (no trailing newline)
printf '%s' 'your-secret-value' | wrangler secret put BETTER_AUTH_SECRET --config=wrangler.prod.jsonc
Worker targets use separate configuration files:
| Target | Config | Worker name | Public production domain |
|---|---|---|---|
| Production | wrangler.prod.jsonc |
sagio-api-prod |
api.sagio.io |
| Dev/staging | wrangler.dev.jsonc |
sagio-api-dev |
None |
Example for dev worker:
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 or domain ownership. wrangler.prod.jsonc is the production source of truth, and CI verifies the live Custom Domain mapping before migrations and after deployment. The dev config has no route and must never claim api.sagio.io.
Helper script (Fasstap only today):
./src/scripts/deployFasstapSecrets.sh .env.local # prod
./src/scripts/deployFasstapSecrets.sh .env.local 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.prod.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— useDATABASE_URLin production instead.GOOGLE_REDIRECT_URI— Better Auth derives redirect fromBETTER_AUTH_URL; legacyutils/auth.tsonly.
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.
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:
- Package name:
io.sagio.merchant - SHA-1: signing key of the release APK (see sagio-kms-apk-signing.md)
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_SECRETwas 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¶
- Plan for all users to re-authenticate.
- Clear the
jwkstable (script above). - Set the new secret via
wrangler secret put BETTER_AUTH_SECRET --config=wrangler.prod.jsonc. - 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 --config=wrangler.prod.jsonc
wrangler secret put BETTER_AUTH_SECRET --config=wrangler.prod.jsonc
# BETTER_AUTH_URL is in wrangler.prod.jsonc [vars] — do not use dashboard-only env vars
wrangler secret put GOOGLE_CLIENT_ID --config=wrangler.prod.jsonc
wrangler secret put GOOGLE_CLIENT_SECRET --config=wrangler.prod.jsonc
wrangler secret put JWT_SECRET --config=wrangler.prod.jsonc
Pages (must-have per frontend):
Terminal (local.properties):
Related docs¶
- auth-migration-handoff.md — Better Auth architecture and smoke tests
- 003-fasstap-configuration.md — Fasstap / terminal env
- merchant-terminal-launch.md — Terminal prod launch checklist
apps/api/.env.example— Local dev template (not used at Workers runtime)