diff --git a/apps/mobile/.env.example b/apps/mobile/.env.example new file mode 100644 index 00000000..cc493f58 --- /dev/null +++ b/apps/mobile/.env.example @@ -0,0 +1,19 @@ +# ───────────────────────────────────────────────────────────── +# Vaultix Mobile – Environment Variables (Issue #557) +# Copy this file to `.env` and adjust values for your setup. +# ───────────────────────────────────────────────────────────── + +# The environment to run. One of: dev | testnet | production +EXPO_PUBLIC_APP_ENV=dev + +# Backend API base URL (without trailing slash). +# - dev: your local NestJS backend +# - testnet: the staging API +# - production: the live API +EXPO_PUBLIC_API_URL=http://localhost:3000 + +# Soroban RPC endpoint URL. +# - dev: local soroban-rpc (default port 8000, NOT 8545 which is EVM) +# - testnet: testnet Soroban RPC +# - production: mainnet Soroban RPC +EXPO_PUBLIC_RPC_URL=http://127.0.0.1:8000 diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index 8ad9532b..e8381e0d 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -3,6 +3,7 @@ import { StatusBar } from 'expo-status-bar'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { ToastProvider } from '../components/Toast'; import { hydrateSession } from '../services/session'; +import { validateEnv } from '../security/env'; import { AppState, AppStateStatus } from 'react-native'; import { useBiometricLock } from '../hooks/useBiometricLock'; @@ -11,6 +12,10 @@ import { MobileLockScreen } from '../components/MobileLockScreen'; import { UpdatePromptModal } from '../components/UpdatePromptModal'; import { useEffect, useRef, useState } from 'react'; +// #557 – validate environment configuration at app start. +// Surfaces a visible console warning in development when config is missing. +validateEnv(); + export default function RootLayout() { const { isEnabled, isUnlocked, authenticate, lock, disableBiometric } = useBiometricLock(); const { needsUpdate, forceUpdate, latestVersion, updateUrl, isLoading } = useAppVersion(); diff --git a/apps/mobile/security/env.ts b/apps/mobile/security/env.ts index ba0337c9..0a37e2ec 100644 --- a/apps/mobile/security/env.ts +++ b/apps/mobile/security/env.ts @@ -12,7 +12,8 @@ const configs: Record = { dev: { environment: 'dev', apiUrl: process.env.EXPO_PUBLIC_API_URL || 'http://localhost:3000', - rpcUrl: process.env.EXPO_PUBLIC_RPC_URL || 'http://127.0.0.1:8545', + // Soroban RPC runs on port 8000 (soroban-rpc standalone) — not 8545 (EVM). + rpcUrl: process.env.EXPO_PUBLIC_RPC_URL || 'http://127.0.0.1:8000', }, testnet: { environment: 'testnet', @@ -28,8 +29,42 @@ const configs: Record = { export const envConfig = configs[ENV]; +/** + * Validates that required environment variables are set. + * In development, surfaces a visible console warning when config is missing + * or still pointing at a localhost default (which may indicate the developer + * hasn't configured their local backend/rpc yet). + * + * Issue #557: Called at app start from app/_layout.tsx. + */ export const validateEnv = () => { - if (!envConfig.apiUrl || !envConfig.rpcUrl) { - console.warn('Missing required environment variables for', ENV); + const warnings: string[] = []; + + if (!envConfig.apiUrl) { + warnings.push('EXPO_PUBLIC_API_URL is not set'); + } + + if (!envConfig.rpcUrl) { + warnings.push('EXPO_PUBLIC_RPC_URL is not set'); + } + + if (ENV === 'dev') { + if (envConfig.apiUrl === 'http://localhost:3000') { + warnings.push( + 'EXPO_PUBLIC_API_URL is using the default localhost URL — set it explicitly in .env if your backend runs elsewhere', + ); + } + if (envConfig.rpcUrl === 'http://127.0.0.1:8000') { + warnings.push( + 'EXPO_PUBLIC_RPC_URL is using the default local Soroban RPC URL — set it explicitly in .env if your RPC runs elsewhere', + ); + } + } + + if (warnings.length > 0) { + console.warn( + `[env] Environment configuration warnings for "${ENV}":\n` + + warnings.map((w) => ` • ${w}`).join('\n'), + ); } }; diff --git a/apps/mobile/services/api.ts b/apps/mobile/services/api.ts index 3fc90171..af95bac3 100644 --- a/apps/mobile/services/api.ts +++ b/apps/mobile/services/api.ts @@ -9,8 +9,10 @@ import { import { withRetry } from '../utils/retry'; import { NotificationsResponse } from '../types/notification'; import { getAccessToken, getSecureAccessToken } from './session'; +import { envConfig } from '../security/env'; -const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL ?? 'http://localhost:3000'; +// Single source of truth: envConfig.apiUrl from security/env.ts (Issue #557) +const API_BASE_URL = envConfig.apiUrl; /** * The Nest auth module is URI-versioned (`app.enableVersioning`), so its routes diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md new file mode 100644 index 00000000..60db527b --- /dev/null +++ b/docs/ENVIRONMENT.md @@ -0,0 +1,141 @@ +# Environment Variables + +This document lists every environment variable used across the Vaultix monorepo, grouped by owning service. + +> **Issue #579:** Configuration was previously scattered and undocumented. Contributors discovered required +> variables by hitting runtime errors. This file is the single canonical reference. + +--- + +## Conflict & Duplicate Register + +The following variables were identified as conflicting or duplicated. The **canonical** name is listed first. + +| Conflicting variable | Canonical variable | Service | Resolution | +|---|---|---|---| +| `EXPO_PUBLIC_API_BASE_URL` | `EXPO_PUBLIC_API_URL` | Mobile | **Removed** in #557 — `services/api.ts` now imports from `security/env.ts` | +| `NEXT_PUBLIC_API_BASE_URL` | `NEXT_PUBLIC_API_URL` | Frontend | `escrow-api.ts` still reads `NEXT_PUBLIC_API_BASE_URL` for a URL-stripping helper; **should be migrated to `NEXT_PUBLIC_API_URL`** | +| `API_BASE_URL` | `PORT` / `NEXT_PUBLIC_API_URL` | Backend (email config) | `API_BASE_URL` is used only to build an email verification link; consider migrating to `FRONTEND_URL` | + +--- + +## Backend (`apps/backend`) + +| Variable | Required | Secret | Default | Purpose | +|---|---|---|---|---| +| `PORT` | No | No | `3000` | HTTP listen port for the NestJS API | +| `NODE_ENV` | No | No | — | Runtime environment (`development`, `production`, `test`) | +| `DATABASE_PATH` | No | No | `./data/vaultix.db` | SQLite database file path | +| `JWT_SECRET` | **Yes** | **Yes** | — | Secret used to sign JWT auth tokens | +| `STELLAR_NETWORK` | No | No | `testnet` | Stellar network (`testnet` or `mainnet`) | +| `STELLAR_NETWORK_PASSPHRASE` | No | No | Derived from `STELLAR_NETWORK` | Stellar network passphrase | +| `STELLAR_RPC_URL` | No | No | `https://soroban-testnet.stellar.org` | Soroban RPC endpoint | +| `STELLAR_CONTRACT_ID` | **Yes** | No | — | Deployed escrow contract ID on Soroban | +| `STELLAR_TIMEOUT` | No | No | `60000` | RPC call timeout in ms | +| `STELLAR_MAX_RETRIES` | No | No | `3` | Max RPC retry attempts | +| `STELLAR_RETRY_DELAY` | No | No | `1000` | Base retry delay in ms | +| `HORIZON_URL` | No | No | Derived from `STELLAR_NETWORK` | Stellar Horizon server URL | +| `WALLET_SECRET` | **Yes** | **Yes** | — | Secret key for the backend escrow wallet | +| `CORS_ORIGINS` | No | No | — | Comma-separated list of allowed CORS origins | +| `FRONTEND_URL` | No | No | `http://localhost:3001` | Frontend URL, used for WebSocket origin checks | +| `API_BASE_URL` | No | No | `http://localhost:3000` | Backend URL, used for email verification link construction | +| `REQUEST_LOG_SLOW_THRESHOLD_MS` | No | No | `5000` | Slow-request logging threshold in ms | +| `IPFS_PROVIDER` | No | No | `pinata` | IPFS storage provider (`pinata` or `local`) | +| `IPFS_GATEWAY_URL` | No | No | `https://gateway.pinata.cloud/ipfs/` | IPFS gateway URL | +| `IPFS_LOCAL_NODE_URL` | No | No | `http://localhost:5001` | Local IPFS node URL | +| `IPFS_MAX_RETRIES` | No | No | `1` | Max IPFS upload retries | +| `PINATA_API_KEY` | No | **Yes** | — | Pinata API key | +| `PINATA_SECRET_API_KEY` | No | **Yes** | — | Pinata secret API key | +| `PINATA_JWT` | No | **Yes** | — | Pinata JWT token (alternative to API key + secret) | +| `SMTP_HOST` | No | No | — | SMTP server host | +| `SMTP_PORT` | No | No | `587` | SMTP server port | +| `SMTP_USER` | No | **Yes** | — | SMTP username | +| `SMTP_PASS` | No | **Yes** | — | SMTP password | +| `EMAIL_FROM` | No | No | `no-reply@vaultix.local` | From address for outgoing emails | +| `EMAIL_MAX_ATTEMPTS` | No | No | `5` | Max email send retry attempts | +| `EMAIL_RETRY_BASE_DELAY_MS` | No | No | `60000` | Base delay for email retries in ms | +| `EMAIL_VERIFICATION_BASE_URL` | No | No | `API_BASE_URL + /auth/profile/verify-email` | Base URL for email verification links | +| `WEBHOOK_MAX_ATTEMPTS` | No | No | `6` | Max webhook delivery attempts | +| `WEBHOOK_RETRY_SCHEDULE_MS` | No | No | — | Comma-separated retry delays in ms | +| `WEBHOOK_REQUEST_TIMEOUT_MS` | No | No | `30000` | Webhook request timeout in ms | +| `WEBHOOK_ALERT_FAILURE_RATE_THRESHOLD` | No | No | `25` | Failure rate % that triggers an alert | +| `WEBHOOK_ALERT_MIN_DELIVERIES` | No | No | `10` | Min deliveries before alerting | +| `WEBHOOK_ALERT_WINDOW_MINUTES` | No | No | `60` | Alert window in minutes | +| `VITE_API_URL` | No | No | `http://localhost:3001` | Used by backend's test client (`src/clients/client.ts`) | + +--- + +## Frontend (`apps/frontend` — Next.js) + +| Variable | Required | Secret | Default | Purpose | +|---|---|---|---|---| +| `NEXT_PUBLIC_API_URL` | **Yes** | No | `http://localhost:3000` | Backend API base URL | +| `NEXT_PUBLIC_API_BASE_URL` | No | No | — | **Deprecated/conflicting** — used only in `escrow-api.ts` for URL stripping. Should be migrated to `NEXT_PUBLIC_API_URL`. | +| `NEXT_PUBLIC_STELLAR_NETWORK` | No | No | `testnet` | Stellar network for wallet connections | +| `NEXT_PUBLIC_WS_URL` | No | No | `http://localhost:3000` | WebSocket URL for real-time updates | +| `CI` | No | No | — | Set by CI runners; controls Playwright retry behaviour | + +--- + +## Mobile (`apps/mobile` — Expo / React Native) + +| Variable | Required | Secret | Default | Purpose | +|---|---|---|---|---| +| `EXPO_PUBLIC_APP_ENV` | No | No | `dev` | Environment selector (`dev`, `testnet`, `production`) | +| `EXPO_PUBLIC_API_URL` | **Yes** | No | Per-env default | Backend API base URL (single source of truth in `security/env.ts`) | +| `EXPO_PUBLIC_RPC_URL` | **Yes** | No | Per-env default | Soroban RPC endpoint URL | +| `EXPO_PUBLIC_AUTH_PATH_PREFIX` | No | No | `/v1/auth` | Auth route prefix (Nest versioning) | +| `EXPO_PUBLIC_WEB_BASE_URL` | No | No | `https://vaultix.app` | Web app base URL for share links | +| `EXPO_PUBLIC_API_BASE_URL` | — | — | — | **Removed** in #557 — was conflicting with `EXPO_PUBLIC_API_URL` | + +--- + +## Onchain Deployment (`apps/onchain`) + +No environment variables are read directly by the onchain deployment code. Contract addresses and network +configuration are injected via the backend's `STELLAR_CONTRACT_ID`, `STELLAR_RPC_URL`, and `STELLAR_NETWORK` +variables during deployment scripts. + +--- + +## Cross-Service Variables (Must Agree) + +These variables must be consistent across services to avoid drift: + +| Variable group | Backend | Frontend | Mobile | Notes | +|---|---|---|---|---| +| **API URL** | `PORT` (listed on) | `NEXT_PUBLIC_API_URL` (points to) | `EXPO_PUBLIC_API_URL` (points to) | All three must resolve to the same backend instance | +| **Stellar Network** | `STELLAR_NETWORK` | `NEXT_PUBLIC_STELLAR_NETWORK` | (derived from `EXPO_PUBLIC_APP_ENV`) | All services must target the same Stellar network | +| **Soroban RPC** | `STELLAR_RPC_URL` | — | `EXPO_PUBLIC_RPC_URL` | Mobile and backend must use the same RPC endpoint | +| **Contract ID** | `STELLAR_CONTRACT_ID` | — | — | Only backend needs this, but the deployed contract must match the network | + +--- + +## Quick Start + +### Backend +```bash +# .env +PORT=3000 +DATABASE_PATH=./data/vaultix.db +JWT_SECRET=your-secret-here +STELLAR_NETWORK=testnet +STELLAR_CONTRACT_ID=your-contract-id +WALLET_SECRET=your-wallet-secret +``` + +### Frontend +```bash +# .env.local +NEXT_PUBLIC_API_URL=http://localhost:3000 +NEXT_PUBLIC_STELLAR_NETWORK=testnet +NEXT_PUBLIC_WS_URL=http://localhost:3000 +``` + +### Mobile +```bash +# .env +EXPO_PUBLIC_APP_ENV=dev +EXPO_PUBLIC_API_URL=http://localhost:3000 +EXPO_PUBLIC_RPC_URL=http://127.0.0.1:8000 +```