Multi-Environment Guide
Documentation for Multi-Environment Guide
Scope: Local development → Staging → Production Stack: Cloudflare Pages (frontend) + Cloudflare Workers (backend services)
1. Overview
NNO uses three environments. Each has its own service URLs, Cloudflare resource bindings, and DNS hostnames.
| Environment | Purpose | Frontend | Backend |
|---|---|---|---|
| Local | Day-to-day development | Vite dev server (localhost:5174) | Wrangler dev (localhost:8787) |
| Staging | Pre-production validation | Cloudflare Pages preview | Workers with [env.stg] config |
| Production | Live platform | Cloudflare Pages production | Workers default config |
2. Environment File Structure
apps/console/
├── .env # Local development — gitignored
├── .env.example # Template committed to repo
├── .env.stg # Staging — committed (VITE_* public vars only)
└── .env.prod # Production — committed (VITE_* public vars only)
services/<name>/
└── .dev.vars # Cloudflare Workers local secrets — gitignoredWhat is committed vs ignored
The .gitignore pattern is:
.env # ignored — may contain local secrets
.env*.local # ignored
.env.* # ignored by default, then...
!.env.stg # ...but .env.stg is explicitly re-included
!.env.prod # ...and .env.prod is explicitly re-included
.dev.vars # ignored — Cloudflare Workers local secretsWhy .env.stg and .env.prod are safe to commit: they contain only VITE_* variables. Vite bakes these into the JavaScript bundle at build time — they are already public. Secrets (API keys, tokens) must never go in these files; they belong in .dev.vars locally or in Cloudflare's secret store for deployed environments.
Variable naming conventions
| Prefix | Used in | Notes |
|---|---|---|
VITE_* | Frontend (apps/console) | Public; baked into JS bundle |
SERVICE_NAME, ENVIRONMENT, etc. | Workers (wrangler.toml [vars]) | Non-sensitive worker config |
| Secrets (no prefix convention) | Workers secrets store / .dev.vars | Never in committed files |
3. Running Commands with Environment Files
Frontend (apps/console)
# Local development — uses .env
pnpm --filter console dev
# Build for staging — uses .env.stg
pnpm --filter console build --mode stg
# Build for production — uses .env.prod
pnpm --filter console build --mode prodThe workspace root also exposes shortcuts defined in package.json:
pnpm with-env <command> # Loads .env (local)
pnpm with-env-stg <command> # Loads .env.stg (staging)Backend Workers
Wrangler picks up the environment via --env:
# Local dev (reads .dev.vars for secrets, wrangler.toml defaults)
cd services/gateway
NODE_ENV=development pnpm dev
# Deploy to staging
pnpm --filter @neutrino-io/gateway deploy --env stg
# Deploy to production (no --env flag = default = production)
pnpm --filter @neutrino-io/gateway deploy4. Local Development
Prerequisites
- Copy the example env file and fill in local values:
cp apps/console/.env.example apps/console/.env - Create
.dev.varsin each service you need locally (see Secrets Management).
Starting services
# Frontend only (Vite dev server at http://localhost:5174)
cd apps/console && pnpm dev
# A backend Worker (Wrangler local runtime at http://localhost:8787)
cd services/iam && NODE_ENV=development pnpm dev
# All apps via Turborepo
pnpm devLocal service URLs
The console's .env proxies auth through Vite to avoid cross-origin cookie issues:
VITE_AUTH_API_URL=/api/auth # Vite proxy → http://localhost:8787
VITE_API_URL=http://localhost:8787/apiThe Vite proxy is configured in apps/console/vite.config.ts and makes all requests appear same-origin, so SameSite=lax cookies work without extra CORS configuration.
Auth cookie domain
Locally, auth cookies are scoped to localhost. No special DNS configuration is needed.
5. Staging Environment
DNS
Staging services live under *.stg.nno.app:
| Service | Staging URL |
|---|---|
| Console (UI) | https://console.app.stg.nno.app |
| Gateway | https://gateway.svc.stg.nno.app |
| IAM | https://iam.svc.stg.nno.app |
| Registry | https://registry.svc.stg.nno.app |
| Billing | https://billing.svc.stg.nno.app |
Public alias: https://api.stg.nno.app → gateway.svc.stg.nno.app
Console Pages Projects
Each environment uses a separate CF Pages project for full isolation. CF Pages custom domains always route to the production deployment of a project, so a single project cannot serve both environments on custom domains.
| Environment | Pages Project | Custom Domain |
|---|---|---|
| Production | nno-k3m9p2xw7q-console | console.app.nno.app, console.nno.app |
| Staging | nno-k3m9p2xw7q-console-stg | console.app.stg.nno.app |
The deploy workflow (deploy.yml) routes to the correct project per branch:
develop→ builds with.env.stg→ deploys to-stgprojectmain→ builds with.env.prod→ deploys to prod project
Cloudflare Pages preview URLs
CF Pages preview URLs (*.pages.dev) are added to the staging CORS allowlist so that PR branch deploys can call staging Workers:
# services/gateway/wrangler.toml [env.stg]
CORS_ORIGINS = "https://console.app.stg.nno.app,\
https://nno-k3m9p2xw7q-console.pages.dev,\
https://develop.nno-k3m9p2xw7q-console.pages.dev"Wrangler config for staging
Each service defines [env.stg] in its wrangler.toml. Key differences from production:
- Worker
namegets a-stgsuffix (e.g.nno-k3m9p2xw7q-gateway-stg) - Service bindings point to
-stgvariants of upstream workers - KV namespace IDs use staging namespaces
ENVIRONMENT = "stg"is set in[env.stg.vars]
Console env file for staging
apps/console/.env.stg is used by vite build --mode stg and by Cloudflare Pages when building from the staging/preview branch:
VITE_APP_ENV=stg
VITE_AUTH_API_URL=https://iam.svc.stg.nno.app
VITE_GATEWAY_URL=https://gateway.svc.stg.nno.app
VITE_BILLING_API_URL=https://billing.svc.stg.nno.app6. Production Environment
DNS
Production services live directly under *.nno.app:
| Service | Production URL |
|---|---|
| Console (UI) | https://console.app.nno.app |
| Gateway | https://gateway.svc.nno.app |
| IAM | https://iam.svc.nno.app |
| Registry | https://registry.svc.nno.app |
| Billing | https://billing.svc.nno.app |
Public alias: https://api.nno.app → gateway.svc.nno.app
CORS
Production Workers accept a single CORS origin per service — no *.pages.dev wildcards:
# services/gateway/wrangler.toml (production default)
CORS_ORIGINS = "https://console.app.nno.app"Deployment checklist
Before deploying to production:
- Secrets configured via
wrangler secret putor Cloudflare dashboard (see §7) - D1 migrations applied:
pnpm --filter <service> with-env wrangler d1 migrations apply <db-name> - KV namespace IDs in
wrangler.tomlmatch production namespaces - DNS records verified in Cloudflare dashboard
-
wrangler.toml[[routes]]custom domain entries are correct -
apps/console/.env.prodhas correct production service URLs
7. Secrets Management
Local development
Create a .dev.vars file in each service directory. Wrangler reads it automatically when running locally:
# services/gateway/.dev.vars (gitignored)
AUTH_API_KEY=dev-secret-here
NNO_INTERNAL_API_KEY=dev-internal-secret
SENTRY_DSN=https://[email protected]/...Do not commit .dev.vars. It is in .gitignore.
Staging and production
Use Wrangler's secret management or the Cloudflare dashboard:
# Set a secret for staging
wrangler secret put AUTH_API_KEY --env stg
# Set a secret for production
wrangler secret put AUTH_API_KEY
# List secrets (names only, values are never shown)
wrangler secret list --env stgCommon secrets by service
| Secret | Services | Notes |
|---|---|---|
AUTH_API_KEY | gateway, iam | Inbound request auth |
NNO_INTERNAL_API_KEY | gateway, registry, billing | Service-to-service auth (outbound) |
CF_API_TOKEN | provisioning | Cloudflare API access |
STRIPE_SECRET_KEY | billing | Stripe payments |
STRIPE_WEBHOOK_SECRET | billing | Stripe webhook verification |
SENTRY_DSN | all services | Error reporting |
SENTRY_RELEASE | all services | Git SHA or semver tag |
Note: Service-to-service communication within the same Cloudflare account uses Service Bindings — no URL or secret needed for those calls. The secrets above are only needed for external services or for inbound authentication from untrusted callers.
8. DNS by Environment
Full reference in DNS Naming doc.
| Environment | NNO Core Pattern | Example |
|---|---|---|
| Production | <name>.<type>.nno.app | gateway.svc.nno.app |
| Staging | <name>.<type>.stg.nno.app | gateway.svc.stg.nno.app |
| Local | localhost:<port> | localhost:8787 |
For client platform services (tenant apps built on NNO):
| Environment | Client Platform Pattern |
|---|---|
| Production | <name>.<type>.<stack>.<pid>.nno.app |
| Staging | <name>.<type>.stg.<stack>.<pid>.nno.app |
| Local | localhost:<port> |
Type suffix convention: .app = user-facing frontend (Pages), .svc = backend API (Workers).
Quick Reference
# --- LOCAL ---
cp apps/console/.env.example apps/console/.env # one-time setup
cd apps/console && pnpm dev # frontend at :5174
cd services/iam && NODE_ENV=development pnpm dev # IAM at :8787
# --- STAGING ---
pnpm with-env-stg pnpm --filter console build --mode stg
wrangler deploy --env stg # (run from service directory)
# --- PRODUCTION ---
pnpm --filter console build --mode prod
wrangler deploy # (no --env = production)
# --- SECRETS ---
wrangler secret put AUTH_API_KEY --env stg
wrangler secret put AUTH_API_KEY
# --- DB MIGRATIONS ---
pnpm --filter @neutrino-io/iam with-env wrangler d1 migrations apply nno-iam-db
pnpm --filter @neutrino-io/registry with-env wrangler d1 migrations apply nno-registry-db