Environment Variables
Complete reference for every environment variable in Trovella -- local defaults, CI values, production sources, and which package reads each one.
Trovella uses environment variables for all runtime configuration. This page documents every variable, where it is read, and how its value differs across environments.
Env File Layout
There are two .env files in development, and one on the production VM:
| File | Purpose | Read By |
|---|---|---|
apps/web/.env | Primary app configuration | Next.js dev server, all @repo/* packages at runtime |
.env (root) | Database scripts only | pnpm db:migrate, pnpm db:seed, pnpm db:studio |
/opt/trovella/.env (VM) | Production runtime | All containers via env_file in docker-compose.prod.yml |
The root .env exists because database scripts run outside the Next.js process and need DATABASE_URL. Both files should have the same DATABASE_URL value in development.
Variable Reference
Database
| Variable | Local Default | CI | Production Source |
|---|---|---|---|
DATABASE_URL | postgresql://trovella:trovella_dev@localhost:5433/trovella | Same (ephemeral PG service) | Secret Manager trovella-database-url (rewritten to cloud-sql-proxy:5432) |
Read by @repo/db in packages/db/src/client.ts via process.env["DATABASE_URL"]. The production value is rewritten by sync-secrets-vm.sh to route through the Cloud SQL Auth Proxy container instead of the Cloud SQL public IP. See VM Secret Sync for the rewrite logic.
Authentication
| Variable | Local Default | CI | Production Source |
|---|---|---|---|
BETTER_AUTH_SECRET | dev-secret-change-in-production | ci-test-secret-at-least-32-characters-long | Secret Manager trovella-better-auth-secret |
BETTER_AUTH_URL | http://localhost:3000 | http://localhost:3000 | Secret Manager trovella-better-auth-url |
NEXT_PUBLIC_BETTER_AUTH_URL | http://localhost:3000 | Not set (not needed for CI) | Static: https://trovella.ai (build arg + sync script) |
GOOGLE_CLIENT_ID | your-google-client-id | ci-placeholder | Secret Manager trovella-google-oauth-client-id |
GOOGLE_CLIENT_SECRET | your-google-client-secret | ci-placeholder | Secret Manager trovella-google-oauth-client-secret |
Read by @repo/auth in packages/auth/src/server.ts. The BETTER_AUTH_SECRET must be at least 32 characters. NEXT_PUBLIC_BETTER_AUTH_URL is a build-time variable -- Next.js inlines it during next build, so it must be set as a Docker build arg (not a runtime env var).
AI Services
| Variable | Local Default | CI | Production Source |
|---|---|---|---|
ANTHROPIC_API_KEY | Empty (set manually) | ci-placeholder | Secret Manager trovella-anthropic-api-key |
GOOGLE_AI_API_KEY | Empty (set manually) | ci-placeholder | Secret Manager trovella-google-ai-api-key |
Read by @repo/ai in packages/ai/src/client.ts (Anthropic) and packages/ai/src/embedding.ts (Google Gemini). Both clients are lazy singletons that throw on first use if the key is missing. CI uses placeholder values because AI tests are mocked.
Cache
| Variable | Local Default | CI | Production Source |
|---|---|---|---|
REDIS_URL | redis://localhost:6379 | redis://localhost:6379 (service container) | Secret Manager trovella-upstash-redis-url |
UPSTASH_REDIS_TOKEN | Not set locally | Not set | Secret Manager trovella-upstash-redis-token |
Read by @repo/cache in packages/cache/src/client.ts. Local development uses a plain Redis container. Production uses Upstash Redis, which requires both a URL and a separate token. The REDIS_URL for Upstash includes the TLS endpoint.
Search
| Variable | Local Default | CI | Production Source |
|---|---|---|---|
TYPESENSE_API_KEY | trovella_dev_key | ci-test-key | Secret Manager trovella-typesense-api-key |
TYPESENSE_URL | http://localhost:8108 | http://localhost:8108 (service container) | Static: http://typesense:8108 (Docker network) |
Read by @repo/search in packages/search/src/client.ts. In production, the Typesense container runs on the Docker network, so the URL uses the service name typesense as the hostname.
Error Tracking
| Variable | Local Default | CI | Production Source |
|---|---|---|---|
SENTRY_DSN | Empty | Not set | Secret Manager trovella-sentry-dsn |
NEXT_PUBLIC_SENTRY_DSN | Empty | Not set | Build arg from Secret Manager (baked into image) |
SENTRY_AUTH_TOKEN | Empty | GitHub Secret | GitHub Secret (build-time only) |
SENTRY_DSN configures server-side error reporting in apps/web/sentry.server.config.ts. NEXT_PUBLIC_SENTRY_DSN is inlined at build time for client-side reporting. SENTRY_AUTH_TOKEN is the only GitHub Secret -- it authenticates source map uploads to Sentry during the Docker build and is never sent to the VM.
Background Jobs
| Variable | Local Default | CI | Production Source |
|---|---|---|---|
INNGEST_DEV | 1 | Not set | Not set (production mode) |
INNGEST_EVENT_KEY | Empty | Not set | Secret Manager trovella-inngest-event-key |
INNGEST_SIGNING_KEY | Empty | Not set | Secret Manager trovella-inngest-signing-key |
INNGEST_BASE_URL | http://localhost:8288 | Not set | Static: http://inngest:8288 |
INNGEST_DEV=1 enables Inngest dev mode locally (no auth required). In production, the event key and signing key authenticate communication between the Next.js app and the self-hosted Inngest server.
| Variable | Local Default | CI | Production Source |
|---|---|---|---|
SMTP_HOST | localhost | Not set | Not yet configured |
SMTP_PORT | 1025 | Not set | Not yet configured |
SMTP_FROM | dev@trovella.ai | Not set | Not yet configured |
RESEND_API_KEY | Not set | Not set | Secret Manager trovella-resend-api-key |
Email is deferred. Locally, Mailpit captures all outbound email on port 1025 with a web UI at port 8025.
Logging
| Variable | Local Default | CI | Production Source |
|---|---|---|---|
NODE_ENV | development (implicit) | Not set | Static: production |
LOG_LEVEL | Not set (defaults to debug in dev, info in prod) | Not set | Not set (uses default) |
Read by @repo/logger in packages/logger/src/logger.ts. When NODE_ENV=production, the logger outputs structured JSON for Cloud Logging. In development, it uses pino-pretty with colorized output.
Runtime Configuration (Production Only)
These variables are set by sync-secrets-vm.sh but are not secrets:
| Variable | Value | Purpose |
|---|---|---|
NODE_ENV | production | Enables production behaviors across all packages |
HOSTNAME | 0.0.0.0 | Binds Next.js to all interfaces inside the container |
PORT | 3000 | Next.js listen port |
NEXT_PUBLIC_BETTER_AUTH_URL | https://trovella.ai | Auth callback URL (also set as build arg) |
TYPESENSE_URL | http://typesense:8108 | Docker network address for Typesense |
INNGEST_BASE_URL | http://inngest:8288 | Docker network address for Inngest |
CLOUD_SQL_CONNECTION_NAME | trovella-prod:us-central1:trovella-prod | Used by the Cloud SQL Auth Proxy container |
Package Boundaries
Each package reads only the env vars relevant to its service. The mapping is enforced by convention (there is no centralized env validation schema):
| Package | Variables |
|---|---|
@repo/db | DATABASE_URL |
@repo/auth | BETTER_AUTH_SECRET, BETTER_AUTH_URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET |
@repo/ai | ANTHROPIC_API_KEY, GOOGLE_AI_API_KEY |
@repo/cache | REDIS_URL |
@repo/search | TYPESENSE_API_KEY, TYPESENSE_URL |
@repo/logger | NODE_ENV, LOG_LEVEL |
@repo/mcp | INNGEST_BASE_URL, INNGEST_DEV |
apps/web | SENTRY_DSN, NEXT_PUBLIC_* vars |
All packages access env vars using bracket notation (process.env["VAR_NAME"]) rather than dot notation, following the TypeScript strict indexing convention.
Build-Time vs Runtime Variables
Next.js distinguishes between build-time and runtime environment variables:
| Prefix | When Resolved | How Set in Production |
|---|---|---|
NEXT_PUBLIC_* | Build time (inlined into client JS bundles) | Docker ARG in the builder stage |
| No prefix | Runtime (read from process.env on each request) | env_file in docker-compose.prod.yml |
The Dockerfile declares build args for public variables:
ARG NEXT_PUBLIC_BETTER_AUTH_URL=https://trovella.ai
ARG NEXT_PUBLIC_SENTRY_DSN
ARG SENTRY_AUTH_TOKEN
Changing a NEXT_PUBLIC_* value requires rebuilding the Docker image. Changing a runtime variable only requires restarting the container.