Skip to main content
This guide walks through setting up a complete Express server with NAVAI backend routes, including CORS configuration and environment variables.

Installation

1

Install dependencies

npm install @navai/voice-backend express cors dotenv
2

Create environment file

Create a .env file with your configuration:
OPENAI_API_KEY=sk-...
OPENAI_REALTIME_MODEL=gpt-realtime
OPENAI_REALTIME_VOICE=marin
OPENAI_REALTIME_INSTRUCTIONS=You are a helpful assistant.
OPENAI_REALTIME_LANGUAGE=Spanish
OPENAI_REALTIME_VOICE_ACCENT=neutral Latin American Spanish
OPENAI_REALTIME_VOICE_TONE=friendly and professional
OPENAI_REALTIME_CLIENT_SECRET_TTL=600
NAVAI_FUNCTIONS_FOLDERS=src/ai/...
NAVAI_CORS_ORIGIN=http://localhost:5173
NAVAI_ALLOW_FRONTEND_API_KEY=false
PORT=3000
3

Create server file

Create server.ts with the Express setup:
import "dotenv/config";
import express from "express";
import cors from "cors";
import { registerNavaiExpressRoutes } from "@navai/voice-backend";

const app = express();
app.use(express.json());

// Configure CORS
const corsOrigin = process.env.NAVAI_CORS_ORIGIN?.split(",").map((s) => s.trim()) ?? "*";
app.use(cors({ origin: corsOrigin }));

// Health check endpoint
app.get("/health", (_req, res) => {
  res.json({ ok: true });
});

// Register NAVAI routes
registerNavaiExpressRoutes(app);

const port = Number(process.env.PORT ?? "3000");
app.listen(port, () => {
  console.log(`API on http://localhost:${port}`);
});

Environment variables

The registerNavaiExpressRoutes function automatically reads configuration from environment variables using getNavaiVoiceBackendOptionsFromEnv().

OpenAI configuration

OPENAI_API_KEY
string
required
Your OpenAI API key. Keep this secret and never expose it to the frontend.
OPENAI_REALTIME_MODEL
string
default:"gpt-realtime"
The OpenAI Realtime model to use for voice sessions.
OPENAI_REALTIME_VOICE
string
default:"marin"
Default voice for the AI agent. Options include marin, alloy, echo, fable, onyx, nova, shimmer.
OPENAI_REALTIME_INSTRUCTIONS
string
default:"You are a helpful assistant."
Base instructions for the AI session. These define the agent’s behavior and personality.

Language and voice customization

OPENAI_REALTIME_LANGUAGE
string
Response language (e.g., “Spanish”, “French”). This is automatically injected into instructions as: “Always reply in .”
OPENAI_REALTIME_VOICE_ACCENT
string
Desired voice accent (e.g., “neutral Latin American Spanish”). Injected as: “Use a accent while speaking.”
OPENAI_REALTIME_VOICE_TONE
string
Desired voice tone (e.g., “friendly and professional”). Injected as: “Use a tone while speaking.”

Security configuration

OPENAI_REALTIME_CLIENT_SECRET_TTL
number
default:"600"
Client secret lifetime in seconds. Must be between 10 and 7200 (2 hours).
NAVAI_ALLOW_FRONTEND_API_KEY
boolean
default:"false"
Allow clients to pass their own API key in requests. Set to true only for development or when you want users to provide their own keys.Security note: When OPENAI_API_KEY is set on the backend, this defaults to false. When the backend key is missing, it defaults to true as a fallback.

Functions configuration

NAVAI_FUNCTIONS_FOLDERS
string
default:"src/ai/functions-modules"
Comma-separated paths to scan for backend functions. Supports:
  • Folder: src/ai/functions-modules
  • Recursive: src/ai/functions-modules/...
  • Wildcard: src/features/*/voice-functions
  • File: src/ai/functions-modules/secret.ts
NAVAI_FUNCTIONS_BASE_DIR
string
default:"process.cwd()"
Base directory for resolving function paths. Defaults to current working directory.

CORS configuration

NAVAI_CORS_ORIGIN
string
default:"*"
Comma-separated list of allowed origins for CORS. Example: http://localhost:5173,https://app.example.com

Custom configuration

You can override environment variables by passing options directly to registerNavaiExpressRoutes:
import { registerNavaiExpressRoutes } from "@navai/voice-backend";

registerNavaiExpressRoutes(app, {
  backendOptions: {
    openaiApiKey: process.env.OPENAI_API_KEY,
    defaultModel: "gpt-realtime",
    defaultVoice: "marin",
    defaultInstructions: "You are a voice navigation assistant.",
    clientSecretTtlSeconds: 300,
    allowApiKeyFromRequest: false
  }
});

CORS setup

For production environments, configure CORS to only allow your frontend domains:
import cors from "cors";

const allowedOrigins = [
  "https://app.example.com",
  "https://mobile.example.com"
];

app.use(cors({
  origin: allowedOrigins,
  credentials: true
}));
For development, you can allow all origins:
app.use(cors({ origin: "*" }));
Never use origin: "*" in production. Always whitelist specific domains to prevent unauthorized access.

API key policy

The backend enforces a strict API key policy:
  1. Backend key always wins: If openaiApiKey is configured on the server, it is always used regardless of what the client sends
  2. Request key as fallback: If backend key is missing, the client can provide apiKey in the request body (only when allowApiKeyFromRequest is true)
  3. Security first: When backend key exists, request keys are rejected unless explicitly allowed with NAVAI_ALLOW_FRONTEND_API_KEY=true
This is implemented in the resolveApiKey function (index.ts:85-103):
function resolveApiKey(opts: NavaiVoiceBackendOptions, req?: CreateClientSecretRequest): string {
  // Server key always wins when configured; request key is only a fallback.
  const backendApiKey = opts.openaiApiKey?.trim();
  if (backendApiKey) {
    return backendApiKey;
  }

  const requestApiKey = req?.apiKey?.trim();
  if (requestApiKey) {
    if (!opts.allowApiKeyFromRequest) {
      throw new Error(
        "Passing apiKey from request is disabled. Set allowApiKeyFromRequest=true to enable it."
      );
    }
    return requestApiKey;
  }

  throw new Error("Missing API key. Configure openaiApiKey or send apiKey in request.");
}

Registered routes

When you call registerNavaiExpressRoutes(app), these endpoints are registered:

POST /navai/realtime/client-secret

Generates ephemeral client secrets for OpenAI Realtime API. Request body:
{
  "model": "gpt-realtime",
  "voice": "marin",
  "instructions": "You are a helpful assistant.",
  "language": "Spanish",
  "voiceAccent": "neutral Latin American Spanish",
  "voiceTone": "friendly and professional",
  "apiKey": "sk-..."
}
Response:
{
  "value": "ek_...",
  "expires_at": 1730000000
}

GET /navai/functions

Lists all available backend functions discovered by the runtime. Response:
{
  "items": [
    {
      "name": "get_weather",
      "description": "Call exported function getWeather.",
      "source": "src/ai/functions-modules/weather.ts#getWeather"
    }
  ],
  "warnings": []
}

POST /navai/functions/execute

Executes a backend function by name. Request body:
{
  "function_name": "get_weather",
  "payload": {
    "args": ["San Francisco"]
  }
}
Success response:
{
  "ok": true,
  "function_name": "get_weather",
  "source": "src/ai/functions-modules/weather.ts#getWeather",
  "result": { "temperature": 72, "conditions": "sunny" }
}
Error response:
{
  "error": "Unknown or disallowed function.",
  "available_functions": ["get_weather", "send_email"]
}

Production recommendations

Follow these security best practices for production deployments:
  • Keep OPENAI_API_KEY only on the server, never expose it to frontend
  • Set NAVAI_ALLOW_FRONTEND_API_KEY=false in production
  • Whitelist specific CORS origins, never use *
  • Monitor and surface warnings from runtime and function registry
  • Restart backend when function files change to reload the registry
  • Use HTTPS in production with valid SSL certificates
  • Implement rate limiting on client secret endpoint
  • Add authentication/authorization before NAVAI routes if needed

Next steps

Client secrets

Learn how the client secret generation works and how to configure TTL

Build docs developers (and LLMs) love