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/.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.
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:
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— 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. - 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):
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)