Environment Variables
Every verified environment variable, grouped by category. “Required” means the backend refuses to start without it (the Joi validator names the missing variable). Values below are safe placeholders — replace them with the values you generate/obtain yourself.
Creating Your Environment Files#
# from the repository root
cp backend/.env.example backend/.env
# frontend (create manually if no example ships)
echo NEXT_PUBLIC_API_URL=http://localhost:5000/api/v1 > frontend/.env.localbackend/.env.example is the canonical starting point — it documents every line the backend recognizes. frontend/.env.local needs only ONE variable for local development. Never rename these files (.env and .env.local are auto-loaded by dotenv and Next.js).
Backend Variables#
| Variable | Description | Where it comes from | File | Example |
|---|---|---|---|---|
NODE_ENVOptional | development | production | test. In production: dev OTP exposure disabled, cookies get secure+SameSite=strict, email dev fallbacks off | — | backend/.env | development |
PORTOptional | HTTP port of the Express server | — | backend/.env | 5000 |
API_PREFIXOptional | URL prefix all API routes mount under (also defines the health endpoint path) | — | backend/.env | /api/v1 |
| Variable | Description | Where it comes from | File | Example |
|---|---|---|---|---|
MONGO_URIRequired | MongoDB connection string — local or Atlas (see Database Setup) | — | backend/.env | your_mongodb_connection_string |
| Variable | Description | Where it comes from | File | Example |
|---|---|---|---|---|
JWT_SECRETRequired | Signs access-token JWTs. Generate with randomBytes(32).toString('hex') | — | backend/.env | your_random_32_character_secret |
JWT_EXPIRES_INOptional | Access-token lifetime (cookie maxAge matches 7 days by default) | — | backend/.env | 7d |
JWT_REFRESH_SECRETRequired | Signs refresh tokens; MUST differ from JWT_SECRET | — | backend/.env | your_other_random_32_char_secret |
JWT_REFRESH_EXPIRES_INOptional | Refresh-token lifetime; stored hashed in the tokens collection and rotated | — | backend/.env | 30d |
| Variable | Description | Where it comes from | File | Example |
|---|---|---|---|---|
COOKIE_SECRETRequired | Used by cookie-parser to validate cookie integrity | — | backend/.env | your_random_32_character_secret |
| Variable | Description | Where it comes from | File | Example |
|---|---|---|---|---|
RESEND_API_KEYRequired | Resend API key used by the HTTP email API (no SMTP ports) | — | backend/.env | re_your_api_key |
RESEND_FROMRequired | "From" header; must use a domain verified in Resend | — | backend/.env | "SaaS Starter Kit <noreply@yourdomain.com>" |
RESEND_REPLY_TOOptional | Optional Reply-To address for outgoing mail | — | backend/.env | support@yourdomain.com |
EMAIL_FROMOptional | Legacy fallback for RESEND_FROM (kept for existing deployments) | — | backend/.env | "SaaS Starter Kit <noreply@yourdomain.com>" |
EMAIL_MAX_RETRIESOptional | Attempts for transient Resend failures (429 / 5xx / network). Default 3 | — | backend/.env | 3 |
EMAIL_RETRY_DELAY_MSOptional | Base backoff delay in milliseconds. Default 1000 | — | backend/.env | 1000 |
| Variable | Description | Where it comes from | File | Example |
|---|---|---|---|---|
FRONTEND_URLRequired | Backend-side: the single allowed CORS origin (must match the browser origin exactly) + where OAuth/email links redirect | — | backend/.env | http://localhost:3000 |
NEXT_PUBLIC_API_URLRequired | Frontend-side base URL INCLUDING the /api/v1 prefix — Rewrites concatenate :path* onto it, and the Google button builds its URL from it | — | frontend/.env.local | http://localhost:5000/api/v1 |
| Variable | Description | Where it comes from | File | Example |
|---|---|---|---|---|
GOOGLE_CLIENT_IDRequired | OAuth Client ID from Google Cloud Console | — | backend/.env | your_google_client_id |
GOOGLE_CLIENT_SECRETRequired | OAuth Client Secret from Google Cloud Console | — | backend/.env | your_google_client_secret |
GOOGLE_CALLBACK_URLRequired | The exact Authorized redirect URI registered in Google Console (backend callback path) | — | backend/.env | http://localhost:5000/api/v1/auth/google/callback |
| Variable | Description | Where it comes from | File | Example |
|---|---|---|---|---|
RATE_LIMIT_WINDOW_MS / RATE_LIMIT_MAX_REQUESTSOptional | Global limiter on all /api/v1/* — max requests per IP per window | — | backend/.env | 900000 / 100 |
RATE_LIMIT_FORGOT_WINDOW_MS / _MAX_REQUESTSOptional | Password-reset OTP code requests: 3 per email per 15 min (read directly in authRoutes.js, keyed by IP+email) | — | backend/.env | 900000 / 3 |
RATE_LIMIT_OTP_WINDOW_MS / _MAX_REQUESTSOptional | OTP verify/reset attempts: 5 per email per 15 min (keyed by IP+email) | — | backend/.env | 900000 / 5 |
| Variable | Description | Where it comes from | File | Example |
|---|---|---|---|---|
UPLOAD_PATH / MAX_FILE_SIZEOptional | Upload folder and max upload size (5 MB default) — reserved: there are no upload endpoints in the current API yet | — | backend/.env | ./uploads / 5242880 |
| Variable | Description | Where it comes from | File | Example |
|---|---|---|---|---|
STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET / STRIPE_PUBLISHABLE_KEYOptional | Reserved for the FUTURE billing feature. No billing code exists yet — leaving these out is fine | — | backend/.env | sk_test_your_key (future) |
| Variable | Description | Where it comes from | File | Example |
|---|---|---|---|---|
ADMIN_EMAIL / ADMIN_PASSWORD / ADMIN_NAMEOptional | Read by backend/seed-admin.js (NOT Joi-validated) — creates or promotes the admin account | — | backend/.env | you@yourdomain.com / <your strong password> / Admin |
Common Configuration Mistakes#
These are the mistakes the project itself is designed to surface as loud startup errors — each one crashes with a Joi message naming the variable, rather than failing silently at runtime.
- Reusing secrets from another project — secrets must be unique per install and rotated in production.
- Short secrets — JWT_* and COOKIE_SECRET must be 32+ characters or Joi rejects them.
- Trailing slash in FRONTEND_URL — CORS compares the browser origin EXACTLY (no trailing slash).
- NEXT_PUBLIC_API_URL without the /api/v1 prefix — the proxy strips the prefix, so the backend then 404s and the Google button fails.
- Editing .env and expecting nodemon to reload — nodemon only watches JS; restart manually (Ctrl+C → pnpm dev).
- Unquoted values with spaces — RESEND_FROM must keep its quotes INSIDE the value.
Keeping Secrets Safe#
Documentation and support tickets must never show real values. If you ever suspect a secret leaked (screenshots, error recordings, public repositories), rotate it immediately — generate a new one and update .env everywhere.