Environments
Convex deployments, env files, and deploy keys for dev, staging, and production.
The three environments
| Env | Convex deployment | Convex cloud URL | HTTP URL |
|---|---|---|---|
| Production | precious-rat-335 | appsapi.theraw.com.my | appsbackend.theraw.com.my |
| Staging | agile-corgi-641 | agile-corgi-641.convex.cloud | agile-corgi-641.convex.site |
| Development | agreeable-canary-738 (personal dev) | agreeable-canary-738.convex.cloud | agreeable-canary-738.convex.site |
Staging and production are both prod-type deployments in the Convex project tipeer:theraw. Staging did not exist at migration time — it was added 2026-08-25 across backend, dashboard, and Vercel.
The dashboard maps to environments through Vercel: production builds from main (prod backend), previews from staging (staging backend). Mobile selects its backend by APP_ENV (development/staging/production).
Deploying the backend
cd packages/backend
bun run deploy:staging # uses CONVEX_DEPLOY_KEY from .env.staging.local
bun run deploy:prod # bare `convex deploy` — targets productionThe deploy key is the redirect mechanism: --env-file alone does not redirect convex deploy, only CONVEX_DEPLOY_KEY does. The staging key lives in packages/backend/.env.staging.local (gitignored).
To copy env vars between deployments: bun run env:sync-staging runs scripts/sync-convex-env.sh, which copies every var from dev to staging, skips JWKS (each deployment signs with its own key), and hard-refuses if the target is production.
Env files by package
All secret-bearing files are gitignored; only apps/dashboard/.env.example is tracked.
packages/backend
.env.local— localconvex devconfig:CONVEX_DEPLOYMENT,CONVEX_URL,CONVEX_SITE_URL,BETTER_AUTH_SECRET,SITE_URL, plus integrations (iSMSISMS_USERNAMEandISMS_PASSWORD,RESEND_*,SHOPIFY_*,R2_*, scanner secrets)..env.staging.local— stagingCONVEX_DEPLOYMENT+CONVEX_DEPLOY_KEY..env.production.local— productionCONVEX_DEPLOYMENT.
Runtime env vars on each deployment are set with bunx convex env set. Required: BETTER_AUTH_SECRET, SITE_URL (must be the .convex.site endpoint, not the marketing domain — it drives OAuth redirects), JWKS, plus the integration credentials (Shopify, R2, Resend, iSMS, Google/Apple). ALLOW_TEST_PHONE_OTP exists for non-prod only — see Runbooks.
apps/dashboard
.env— base defaults (NEXT_PUBLIC_SITE_URL, Sentry disabled flag, Shopify store handle)..env.development/.env.staging/.env.production—NEXT_PUBLIC_CONVEX_URL+NEXT_PUBLIC_CONVEX_SITE_URLper environment..env.local— personal overrides (e.g. pointing local dev at the personal dev deployment)..env.sentry-build-plugin— the Sentry org token. The build plugin loads it, but only a Vercel build (Production or Preview, where Vercel holds the same token) creates a release or uploads source maps.
Vercel specifics: the project's root directory is apps/dashboard, region sin1, and vercel.json pins installCommand: "npx -y bun@1.4.0 install --frozen-lockfile" because Vercel picks Bun from the lockfile format and its container lags.
apps/mobile
One file per APP_ENV: .env.development, .env.staging, .env.production — all gitignored, loaded by env.js (zod-validated; see Mobile Environment). Same Convex deployment/URL trio, plus app identity, Shopify storefront token, Google client IDs, Sentry DSN, and MMKV obfuscation key.
The most dangerous edge
One Shopify store is shared by every environment. SHOPIFY_ADMIN_ENDPOINT and its token are identical across dev, staging, and production, so every write path (voucher codes, webhook subscriptions, catalog syncs) targets the live store.
SHOPIFY_WRITES_ALLOWED is the guard. adminRequestWithMetadata in convex/shopify/shopifyClient.ts throws SHOPIFY_WRITES_DISABLED before it sends any Admin API mutation unless the deployment sets the variable to 1, and mutations that would only queue a Shopify write check shopifyWritesAllowed() first (convex/helpers/shopifyWrite.ts). The helper's comment names prod and staging as the deployments that set it. On any deployment that does, treat every Shopify write as production-impacting. Storefront cart calls are not gated.