Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions apps/mobile/.env.example
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions apps/mobile/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();
Expand Down
41 changes: 38 additions & 3 deletions apps/mobile/security/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const configs: Record<Environment, EnvConfig> = {
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',
Expand All @@ -28,8 +29,42 @@ const configs: Record<Environment, EnvConfig> = {

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'),
);
}
};
4 changes: 3 additions & 1 deletion apps/mobile/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
141 changes: 141 additions & 0 deletions docs/ENVIRONMENT.md
Original file line number Diff line number Diff line change
@@ -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
```
Loading
Loading