Skip to main content

Configuration Management

The Ever Works platform uses a layered configuration system that combines environment variables, NestJS ConfigModule, typed config objects, and runtime config accessors. This guide covers how configuration flows from .env files through to application code.

Configuration Architecture

.env file
|
+--v--+
| dotenv | Loads vars into process.env
+--+--+
|
+--v----------------+ +--v------------------+
| NestJS ConfigModule| | Typed config objects |
| (registerAs) | | (packages/agent/src/ |
+--+----------------+ | config/index.ts) |
| +--+------------------+
| |
+--v--+ +--v--+
| ConfigService.get() | | config.database.getType() |
| (injected via DI) | | config.trigger.isEnabled() |
+------+ +------+

Environment Variable Loading

Environment variables are loaded at the very start of the bootstrap process:

// apps/api/src/main.ts
import { configDotenv } from 'dotenv';

async function bootstrap() {
configDotenv({ path: path.resolve(process.cwd(), '.env') });

initSentry();
initPostHog();

const app = await NestFactory.create(ApiModule);
// ...
}

Typed Configuration Objects

API Constants

The API application defines its constants in apps/api/src/config/constants.ts:

export const config = {
auth: {
// Fail-closed at boot: AUTH_SECRET must be set. Never fall back to a
// hardcoded default — a fixed fallback turns "missing env var in
// production" into "predictable encryption/signing key in production".
secret: () => {
const secret = process.env.AUTH_SECRET;
if (!secret) {
throw new Error('AUTH_SECRET environment variable is required');
}
return secret;
}
}
};

export const jwtConstants = {
accessTokenExpiration: (): any => {
const expiration = process.env.JWT_ACCESS_TOKEN_EXPIRATION;
return expiration === 'never' ? undefined : expiration || '7d';
},
refreshTokenExpiration: () => {
const days = process.env.JWT_REFRESH_TOKEN_EXPIRATION_DAYS;
return days === 'never' ? -1 : parseInt(days || '14', 10);
},
isTokenExpirationDisabled: () => {
return process.env.JWT_DISABLE_EXPIRATION === 'true';
}
};

export const authConstants = {
bcryptSaltRounds: 10,
refreshTokenLength: 32,
refreshTokenCleanupDays: 30
};

Agent Package Config

The core agent package has its own typed config in packages/agent/src/config/index.ts:

export const config = {
getEnvironment() {
return process.env.NODE_ENV;
},

trigger: {
isEnabled() {
return process.env.TRIGGER_ENABLED === 'true';
},
getSecretKey() {
return process.env.TRIGGER_SECRET_KEY;
},
getApiUrl() {
return process.env.TRIGGER_API_URL || 'https://api.trigger.dev';
},
shouldUseTrigger() {
return this.isEnabled() && Boolean(this.getInternalSecret());
}
},

database: {
getType() {
return process.env.DATABASE_TYPE || 'better-sqlite3';
},
isSqlite() {
return this.getType()?.includes('sqlite');
},
getUrl() {
return process.env.DATABASE_URL;
},
autoMigrate() {
return process.env.DATABASE_AUTOMIGRATE !== 'false';
},
loggingEnabled() {
return process.env.DATABASE_LOGGING === 'true';
},
sslMode() {
return process.env.DATABASE_SSL_MODE === 'true';
}
// ... more accessors
},

sentry: {
getDsn() {
return process.env.SENTRY_DSN;
}
},

posthog: {
getApiKey() {
return process.env.POSTHOG_API_KEY;
},
getHost() {
return process.env.POSTHOG_HOST;
}
},

subscriptions: {
isEnabled() {
return process.env.SUBSCRIPTIONS_ENABLED === 'true';
},
getDefaultPlanCode() {
return process.env.SUBSCRIPTIONS_DEFAULT_PLAN || 'free';
}
}
};

ConfigModule with registerAs

The database configuration uses NestJS registerAs for DI-based configuration:

// packages/agent/src/database/database.config.ts
import { registerAs } from '@nestjs/config';

export const databaseConfig = registerAs('database', (): DatabaseConfig => {
const dbType = config.database.getType();

const baseConfig = {
entities: ENTITIES,
synchronize: config.database.autoMigrate(),
logging: config.database.loggingEnabled()
};

if (dbType === 'postgres') {
return {
...baseConfig,
type: 'postgres',
host: config.database.getHost() || 'localhost',
port: parseInt(config.database.getPort() || '5432'),
username: config.database.getUsername() || 'postgres',
password: config.database.getPassword() || '',
database: config.database.getDatabaseName() || 'ever_works'
};
}

// ... other backends
});

Used via ConfigService in module factories:

TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => {
return configService.get('database');
},
inject: [ConfigService],
}),

Complete Environment Variable Reference

Core Application

VariableDefaultDescription
NODE_ENVdevelopmentRuntime environment
PORT3100API server port
WEB_URLhttp://localhost:3000Frontend URL
ALLOWED_ORIGINShttp://localhost:3000CORS allowed origins (comma-sep)
HTTP_DEBUGfalseEnable request/response logging

Authentication

VariableDefaultDescription
JWT_SECRETfallbackJWT signing secret
JWT_ACCESS_TOKEN_EXPIRATION7dAccess token TTL (never to disable)
JWT_REFRESH_TOKEN_EXPIRATION_DAYS14Refresh token TTL in days
JWT_DISABLE_EXPIRATIONfalseDisable all token expiration

Database

VariableDefaultDescription
DATABASE_TYPEbetter-sqlite3better-sqlite3, postgres, mysql
DATABASE_URL--Full connection URL (overrides host)
DATABASE_HOSTlocalhostDatabase host
DATABASE_PORT5432 / 3306Database port
DATABASE_USERNAMEpostgresDatabase user
DATABASE_PASSWORD--Database password
DATABASE_NAMEever_worksDatabase name
DATABASE_PATH--SQLite file path
DATABASE_IN_MEMORYfalseUse in-memory SQLite
DATABASE_AUTOMIGRATEtrueAuto-sync schema (disable in prod)
DATABASE_LOGGINGfalseEnable SQL query logging
DATABASE_SSL_MODEfalseEnable SSL for PostgreSQL
DATABASE_CA_CERT--CA certificate for SSL

OAuth Providers

VariableDefaultDescription
GH_CLIENT_ID--GitHub OAuth app client ID
GH_CLIENT_SECRET--GitHub OAuth app client secret
GH_CALLBACK_URLautoGitHub OAuth callback URL
GOOGLE_CLIENT_ID--Google OAuth client ID
GOOGLE_CLIENT_SECRET--Google OAuth client secret
GOOGLE_CALLBACK_URLautoGoogle OAuth callback URL

Monitoring

VariableDefaultDescription
SENTRY_DSN--Sentry project DSN
POSTHOG_API_KEY--PostHog project API key
POSTHOG_HOSThttps://app.posthog.comPostHog instance URL

Background Tasks

VariableDefaultDescription
TRIGGER_ENABLEDfalseEnable Trigger.dev
TRIGGER_SECRET_KEY--Trigger.dev API key
TRIGGER_API_URLhttps://api.trigger.devTrigger.dev API URL
TRIGGER_INTERNAL_API_URL--Internal API base URL
TRIGGER_INTERNAL_SECRET--Secret for internal API calls

Per-Environment Configuration

Use the DatabaseConfigurations factory for environment-specific setups:

// Development
DatabaseConfigurations.apiDevelopment();

// Production
DatabaseConfigurations.postgres({ url: process.env.DATABASE_URL });

// Testing
DatabaseConfigurations.test(); // Always in-memory SQLite

Secrets Management

Development

Use .env files (never committed to git):

# .env
JWT_SECRET=my-dev-secret
GH_CLIENT_SECRET=github-dev-secret
DATABASE_PASSWORD=local-db-password

Production

Use platform-native secrets:

# Kubernetes
kubectl create secret generic api-secrets \
--from-literal=JWT_SECRET=$JWT_SECRET \
--from-literal=DATABASE_PASSWORD=$DB_PASSWORD

# Docker Compose
services:
api:
env_file: .env.production

Best Practices

  1. Never hardcode secrets -- Always use environment variables. The JWT default fallback is only for development.

  2. Use typed accessors -- Prefer config.database.getType() over raw process.env.DATABASE_TYPE for type safety and default values.

  3. Validate at startup -- Critical configuration should be validated in onModuleInit or onApplicationBootstrap.

  4. Separate concerns -- API-specific config stays in apps/api/src/config/, shared config goes in packages/agent/src/config/.

  5. Document all variables -- Every new environment variable should be added to the reference table above.

Troubleshooting

Config values not loading

Ensure configDotenv() is called before NestFactory.create(). The .env file path is resolved from process.cwd().

Database not connecting

Check that DATABASE_TYPE matches one of the supported values: better-sqlite3, sqlite, postgres, mysql, mariadb.

JWT secret warning

If you see "using default secret" in logs, JWT_SECRET is not set. This is acceptable in development but must be set in production.