NNO Docs
Guides

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.

EnvironmentPurposeFrontendBackend
LocalDay-to-day developmentVite dev server (localhost:5174)Wrangler dev (localhost:8787)
StagingPre-production validationCloudflare Pages previewWorkers with [env.stg] config
ProductionLive platformCloudflare Pages productionWorkers 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 — gitignored

What 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 secrets

Why .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

PrefixUsed inNotes
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.varsNever 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 prod

The 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 deploy

4. Local Development

Prerequisites

  1. Copy the example env file and fill in local values:
    cp apps/console/.env.example apps/console/.env
  2. Create .dev.vars in 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 dev

Local 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/api

The 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.

Locally, auth cookies are scoped to localhost. No special DNS configuration is needed.


5. Staging Environment

DNS

Staging services live under *.stg.nno.app:

ServiceStaging URL
Console (UI)https://console.app.stg.nno.app
Gatewayhttps://gateway.svc.stg.nno.app
IAMhttps://iam.svc.stg.nno.app
Registryhttps://registry.svc.stg.nno.app
Billinghttps://billing.svc.stg.nno.app

Public alias: https://api.stg.nno.appgateway.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.

EnvironmentPages ProjectCustom Domain
Productionnno-k3m9p2xw7q-consoleconsole.app.nno.app, console.nno.app
Stagingnno-k3m9p2xw7q-console-stgconsole.app.stg.nno.app

The deploy workflow (deploy.yml) routes to the correct project per branch:

  • develop → builds with .env.stg → deploys to -stg project
  • main → 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 name gets a -stg suffix (e.g. nno-k3m9p2xw7q-gateway-stg)
  • Service bindings point to -stg variants 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.app

6. Production Environment

DNS

Production services live directly under *.nno.app:

ServiceProduction URL
Console (UI)https://console.app.nno.app
Gatewayhttps://gateway.svc.nno.app
IAMhttps://iam.svc.nno.app
Registryhttps://registry.svc.nno.app
Billinghttps://billing.svc.nno.app

Public alias: https://api.nno.appgateway.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 put or Cloudflare dashboard (see §7)
  • D1 migrations applied: pnpm --filter <service> with-env wrangler d1 migrations apply <db-name>
  • KV namespace IDs in wrangler.toml match production namespaces
  • DNS records verified in Cloudflare dashboard
  • wrangler.toml [[routes]] custom domain entries are correct
  • apps/console/.env.prod has 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 stg

Common secrets by service

SecretServicesNotes
AUTH_API_KEYgateway, iamInbound request auth
NNO_INTERNAL_API_KEYgateway, registry, billingService-to-service auth (outbound)
CF_API_TOKENprovisioningCloudflare API access
STRIPE_SECRET_KEYbillingStripe payments
STRIPE_WEBHOOK_SECRETbillingStripe webhook verification
SENTRY_DSNall servicesError reporting
SENTRY_RELEASEall servicesGit 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.

EnvironmentNNO Core PatternExample
Production<name>.<type>.nno.appgateway.svc.nno.app
Staging<name>.<type>.stg.nno.appgateway.svc.stg.nno.app
Locallocalhost:<port>localhost:8787

For client platform services (tenant apps built on NNO):

EnvironmentClient Platform Pattern
Production<name>.<type>.<stack>.<pid>.nno.app
Staging<name>.<type>.stg.<stack>.<pid>.nno.app
Locallocalhost:<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

On this page