Navan Multi-Environment Setup
Overview
Navan does not offer a sandbox or staging API — every call hits production data with real corporate bookings and expense records. This creates risk for development and testing: a bug in a sync script could modify live itineraries, and CI pipelines cannot safely run integration tests. This skill implements environment isolation using separate OAuth apps, environment variable validation, a local development proxy, and a CI mock server.
Prerequisites
- Navan admin access to create multiple OAuth apps (Admin > Travel admin > Settings > Integrations)
- Node.js 18+ for proxy and mock server
- Understanding of OAuth 2.0 client credentials flow (see
navan-install-auth) .envmanagement tooling (dotenv, direnv, or cloud secret manager)
Instructions
Step 1: Create Per-Environment OAuth Apps
Create separate API credentials in the Navan admin dashboard for each environment. This provides natural isolation — the dev app can have read-only scopes while production gets full access.
# .env.development — read-only scoped OAuth app
NAVAN_ENV=development
NAVAN_CLIENT_ID=dev-client-id-xxxxx
NAVAN_CLIENT_SECRET=dev-client-secret-xxxxx
NAVAN_API_BASE=https://api.navan.com/v1
NAVAN_READ_ONLY=true
# .env.staging — read + limited write, separate audit trail
NAVAN_ENV=staging
NAVAN_CLIENT_ID=stg-client-id-xxxxx
NAVAN_CLIENT_SECRET=stg-client-secret-xxxxx
NAVAN_API_BASE=https://api.navan.com/v1
NAVAN_READ_ONLY=false
# .env.production — full access, rotation-managed
NAVAN_ENV=production
NAVAN_CLIENT_ID=prod-client-id-xxxxx
NAVAN_CLIENT_SECRET=prod-client-secret-xxxxx
NAVAN_API_BASE=https://api.navan.com/v1
NAVAN_READ_ONLY=false
Step 2: Build an Environment-Aware Client
import { config } from 'dotenv';
interface NavanConfig {
env: string;
clientId: string;
clientSecret: string;
apiBase: string;
readOnly: boolean;
}
function loadConfig(): NavanConfig {
const envFile = `.env.${process.env.NODE_ENV || 'development'}`;
config({ path: envFile });
const required = ['NAVAN_CLIENT_ID', 'NAVAN_CLIENT_SECRET', 'NAVAN_API_BASE'];
for (const key of required) {
if (!process.env[key]) {
throw new Error(`Missing ${key} in ${envFile}`);
}
}
return {
env: process.env.NAVAN_ENV || 'development',
clientId: process.env.NAVAN_CLIENT_ID!,
clientSecret: process.env.NAVAN_CLIENT_SECRET!,
apiBase: process.env.NAVAN_API_BASE!,
readOnly: process.env.NAVAN_READ_ONLY === 'true'
};
}
class NavanClient {
private config: NavanConfig;
private accessToken: string | null = null;
constructor() {
this.config = loadConfig();
console.log(`Navan client initialized [${this.config.env}] readOnly=${this.config.readOnly}`);
}
async request(method: string, path: string, body?: object): Promise<any> {
// Block writes in read-only environments
if (this.config.readOnly && method !== 'GET') {
throw new Error(`Write operations blocked in ${this.config.env} (read-only mode)`);
}
if (!this.accessToken) {
this.accessToken = await this.authenticate();
}
const response = await fetch(`${this.config.apiBase}${path}`, {
method,
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
},
body: body ? JSON.stringify(body) : undefined
});
if (!response.ok) {
throw new Error(`Navan API error: HTTP ${response.status} on ${method} ${path}`);
}
return response.json();
}
private async authenticate(): Promise<string> {
const res = await fetch('https://api.navan.com/ta-auth/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.config.clientId,
client_secret: this.config.clientSecret
})
});
const { access_token } = await res.json();
return access_token;
}
}
Step 3: Create a Local Development Proxy
import express from 'express';
// Proxy that logs all requests and optionally blocks writes
const proxy = express();
proxy.use(express.json());
proxy.all('/navan/*', async (req, res) => {
const navanPath = req.path.replace('/navan', '');
const method = req.method;
// Log every request for debugging
console.log(`[PROXY] ${method} ${navanPath}`);
if (req.body && Object.keys(req.body).length > 0) {
console.log(`[PROXY] Body:`, JSON.stringify(req.body, null, 2));
}
// In dev mode, block mutating operations
if (process.env.NAVAN_READ_ONLY === 'true' && method !== 'GET') {
console.log(`[PROXY] BLOCKED: ${method} ${navanPath} (read-only mode)`);
return res.status(403).json({
error: 'Write operation blocked in development mode',
method, path: navanPath
});
}
// Forward to real Navan API
try {
const response = await fetch(`https://api.navan.com/v1${navanPath}`, {
method,
headers: {
'Authorization': req.headers.authorization as string,
'Content-Type': 'application/json'
},
body: ['POST', 'PUT', 'PATCH'].includes(method)
? JSON.stringify(req.body) : undefined
});
const data = await response.json();
console.log(`[PROXY] Response: ${response.status}`);
res.status(response.status).json(data);
} catch (err: any) {
console.error(`[PROXY] Error:`, err.message);
res.status(502).json({ error: 'Proxy error', message: err.message });
}
});
proxy.listen(4000, () => console.log('Navan dev proxy on http://localhost:4000'));
Step 4: Build a CI Mock Server
import express from 'express';
const mock = express();
mock.use(express.json());
// Mock data store
const mockData = {
users: [
{ id: 'user-001', email: 'traveler@company.com', role: 'traveler', department: 'engineering' }
],
trips: [
{ id: 'trip-001', traveler_id: 'user-001', status: 'confirmed', total: 450.00 }
],
expenses: [
{ id: 'exp-001', submitter_id: 'user-001', amount: 125.50, status: 'submitted' }
]
};
// OAuth token endpoint
mock.post('/ta-auth/oauth/token', (req, res) => {
res.json({ access_token: 'mock-token-ci', expires_in: 3600, token_type: 'Bearer' });
});
// Users
mock.get('/v1/users', (req, res) => {
res.json({ data: mockData.users, total: mockData.users.length, has_more: false });
});
// Trips
mock.get('/v1/trips', (req, res) => {
res.json({ data: mockData.trips, total: mockData.trips.length, has_more: false });
});
// Expenses
mock.get('/v1/expenses', (req, res) => {
res.json({ data: mockData.expenses, total: mockData.expenses.length, has_more: false });
});
// Catch-all for unimplemented endpoints
mock.all('*', (req, res) => {
console.log(`[MOCK] Unhandled: ${req.method} ${req.path}`);
res.status(501).json({ error: 'Not implemented in mock', path: req.path });
});
const port = process.env.MOCK_PORT || 4001;
mock.listen(port, () => console.log(`Navan mock server on http://localhost:${port}`));
Step 5: Wire Mock Server into CI
# .github/workflows/test.yml
- name: Start Navan mock server
run: |
node navan-mock-server.js &
sleep 2
env:
MOCK_PORT: 4001
- name: Run integration tests
run: npm test
env:
NAVAN_API_BASE: http://localhost:4001/v1
NAVAN_CLIENT_ID: ci-test-client
NAVAN_CLIENT_SECRET: ci-test-secret
NODE_ENV: test
Output
A complete environment isolation strategy for Navan integrations: separate OAuth apps per environment with scoped permissions, an environment-aware client with write protection, a local dev proxy for request logging and mutation blocking, and a CI-ready mock server that eliminates production API dependencies from automated tests.
Error Handling
| Error | Code | Solution |
|-------|------|----------|
| Missing env vars | N/A | Config loader throws on startup; check the correct .env.<environment> file exists |
| Write blocked in read-only | 403 | Expected in dev mode; switch to staging/prod for write operations |
| Mock endpoint not found | 501 | Add the endpoint to mock server; check test expectations match mock data |
| Proxy connection refused | 502 | Ensure the proxy server is running; check port availability |
| Wrong environment loaded | N/A | Verify NODE_ENV matches the intended .env.<environment> file |
Examples
Validate environment configuration:
# Check which environment would load
NODE_ENV=staging node -e "
require('dotenv').config({ path: '.env.staging' });
console.log('ENV:', process.env.NAVAN_ENV);
console.log('READ_ONLY:', process.env.NAVAN_READ_ONLY);
console.log('CLIENT_ID:', process.env.NAVAN_CLIENT_ID?.slice(0, 8) + '...');
"
Run tests against mock server locally:
# Terminal 1: Start mock
MOCK_PORT=4001 node navan-mock-server.js
# Terminal 2: Run tests
NAVAN_API_BASE=http://localhost:4001/v1 npm test
Resources
- Navan Help Center — API credential creation and management
- Navan Integrations — Available integration patterns and partners
- Navan Security — Data handling and environment security policies
- dotenv Documentation — Environment variable management for Node.js
Next Steps
After setting up environments, see navan-security-basics for credential rotation across all environments, or navan-ci-integration for building the full CI/CD pipeline with Navan API tests.