Configuration
Putnami uses YAML files for configuration combined with typed config definitions for validation. This provides both flexibility and type safety.
Configuration files
File hierarchy
Configuration is loaded from multiple sources in order of precedence (highest wins):
- Environment variables via
Env()descriptors (highest precedence) CONFIG_DATAenvironment variable (inline YAML string).gen/conf/.env.{env}.yaml- Generated config files (created by build tools)conf/.env.{env}.yaml- Source config files in your projectconfInitvalues - Programmatic defaults from plugin constructorsDefault()values in the schema (lowest precedence)
Basic structure
# .env.yaml
putnami:
port: 3000
publicFolder: public
static:
compress: 1024
cacheMaxAge: 86400
react:
scanFolder: app
ssrTimeout: 5000
database:
host: localhost
port: 5432
database: myapp
logger:
level: infoLocal overrides
# .env.local.yaml (gitignored)
database:
password: my-local-password
oauth:
clientSecret: my-dev-secretEnvironment-specific
# .env.production.yaml
putnami:
port: 8080
database:
host: prod-db.example.com
ssl: true
logger:
level: warnTyped configuration
Defining a config
import { Config, useConfig, Default, Optional } from '@putnami/runtime';
export const HttpConfig = Config('http', {
port: Default(Number, 3000),
hostname: Default(String, '0.0.0.0'),
});
// Usage
const config = useConfig(HttpConfig);
console.log(config.port); // 3000Config path
The first argument to Config() is the dot-notation path in YAML:
const FeatureConfig = Config('myapp.feature', {
enabled: Default(Boolean, false),
maxItems: Default(Number, 100),
});This loads from:
myapp:
feature:
enabled: true
maxItems: 100Schema primitives
import {
Config,
useConfig,
Default,
Optional,
ArrayOf,
OneOf,
Min,
Max,
MinLength,
MaxLength,
Email,
Url,
Uuid,
Int,
Sensitive,
Env,
Resolve,
} from '@putnami/runtime';
const AppConfig = Config('app', {
name: MinLength(1),
port: Default(Min(1), 3000),
debug: Default(Boolean, false),
environment: Default(OneOf('development', 'staging', 'production'), 'development'),
adminEmail: Optional(Email),
apiEndpoint: Optional(Url),
});Nested configuration
Use plain objects for nested structures:
const ServicesConfig = Config('services', {
database: {
host: Default(String, 'localhost'),
port: Default(Number, 5432),
database: String,
user: String,
password: Sensitive(String),
},
cache: {
host: Default(String, 'localhost'),
port: Default(Number, 6379),
},
});services:
database:
host: db.example.com
port: 5432
database: myapp
user: postgres
password: secret
cache:
host: cache.example.com
port: 6379Type annotations
Use InferConfig to extract TypeScript types from config definitions:
import type { InferConfig } from '@putnami/runtime';
type AppCfg = InferConfig<typeof AppConfig>;Environment variables
Direct access
import { getEnv } from '@putnami/runtime';
const env = getEnv(); // 'local', 'test', or 'production'Binding fields to env vars
Use Env() to bind a config field to an environment variable:
const ServerConfig = Config('server', {
port: Env('PORT', Number),
host: Default(String, '0.0.0.0'),
});Secret resolution
Use Resolve() for async secret resolution (e.g., from a secret manager):
const SecretConfig = Config('secrets', {
apiKey: Resolve(async () => {
return await fetchFromSecretManager('API_KEY');
}, String),
dbPassword: Resolve(async () => {
return await fetchFromSecretManager('DB_PASSWORD');
}, Sensitive(String)),
});
// At bootstrap, resolve all async values
await resolveConfigs(SecretConfig);
// Then use synchronously
const secrets = useConfig(SecretConfig);In YAML files
Reference environment variables using ${VAR_NAME}:
database:
password: ${DATABASE_PASSWORD}
oauth:
clientSecret: ${OAUTH_CLIENT_SECRET}
api:
key: ${API_KEY:default-key} # With default valueUsing configuration
In services
import { useConfig } from '@putnami/runtime';
import { AppConfig } from './config/app.config';
export class MyService {
private config = useConfig(AppConfig);
doSomething() {
if (this.config.debug) {
console.log('Debug mode enabled');
}
}
}In route handlers
import { endpoint } from '@putnami/application';
import { useConfig } from '@putnami/runtime';
import { FeatureConfig } from '../config/feature.config';
export default endpoint(() => {
const config = useConfig(FeatureConfig);
return {
feature: config.name,
enabled: config.enabled,
};
});In plugins
import type { Plugin } from '@putnami/application';
import { useConfig } from '@putnami/runtime';
import { MyPluginConfig } from './config';
export function myPlugin(): Plugin {
const config = useConfig(MyPluginConfig);
return {
async warmup() {
console.log(`Plugin initialized with ${config.setting}`);
},
};
}Framework configuration
Putnami application
Shared settings for the HTTP server, static files, and React SSR:
putnami:
port: 3000
publicFolder: public
skipLoading: false
static: # Static files plugin
compress: 1024
cacheMaxAge: 86400
prefix: '/assets'
react: # React SSR plugin
scanFolder: app
autoScan: true
ssrTimeout: 5000Database
database:
host: localhost
port: 5432
database: myapp
user: postgres
password: secret
ssl: false
queryProfiling: falseSessions
session:
store: 'cookie'
cookieName: 'session'
cookieSecret: 'your-32-character-secret-key!!!'
cookieSalt: 'your-16-char-salt'
ttl: 604800
secure: true
sameSite: 'lax'OAuth
oauth:
clientId: 'your-client-id'
clientSecret: 'your-client-secret'
authorizeUri: 'https://auth.example.com/authorize'
tokenUri: 'https://auth.example.com/token'
scopes:
- 'openid'
- 'profile'Logging
logging:
level: 'info' # 'debug', 'info', 'warn', 'error'Configuration patterns
Feature flags
const FeatureFlags = Config('features', {
newDashboard: Default(Boolean, false),
betaFeatures: Default(Boolean, false),
maintenanceMode: Default(Boolean, false),
});features:
newDashboard: true
betaFeatures: false
maintenanceMode: falseAPI clients
const StripeConfig = Config('integrations.stripe', {
apiKey: Sensitive(String),
webhookSecret: Sensitive(String),
mode: Default(OneOf('test', 'live'), 'test'),
});integrations:
stripe:
apiKey: ${STRIPE_API_KEY}
webhookSecret: ${STRIPE_WEBHOOK_SECRET}
mode: testMulti-tenant configuration
const TenantConfig = Config('tenants', {
defaultTenant: Default(String, 'default'),
multiTenantEnabled: Default(Boolean, false),
});Config with DI
When running inside a DI container (via Application), useConfig() automatically delegates to ConfigService, which provides instance-scoped caching and per-field origin tracking.
Config tokens
Create typed DI tokens for config definitions using configToken():
import { configToken, provideConfig } from '@putnami/runtime';
import { DatabaseConfig } from './database.config';
// In endpoint handlers:
endpoint()
.inject({ db: configToken(DatabaseConfig) })
.handle(async ({ db }, ctx) => {
console.log(db.host); // typed as string
});Config tokens that have been created via configToken() are auto-registered by Application at container build time.
Debugging config origins
ConfigService can show where each field value comes from:
const configService = ctx.get(ConfigService);
const desc = configService.describe(DatabaseConfig);
for (const origin of desc.origins) {
console.log(`${origin.field}: ${origin.value} (from ${origin.source})`);
}
// host: prod.db (from CONFIG_DATA)
// port: 5432 (from default)
// password: *** (from env:DB_PASSWORD)Custom config sources
Provide alternative config backends for testing or production:
import type { ConfigSource } from '@putnami/runtime';
const vaultSource: ConfigSource = {
name: 'vault',
priority: 90,
load: () => fetchVaultSecrets(),
};Testing with configuration
Override in tests
Use confInit to provide programmatic defaults. These are overridden by YAML files, so in tests you may combine them with resetConfigLoader() and CONFIG_DATA:
import { resetConfigLoader } from '@putnami/runtime';
beforeEach(() => {
// Reset config cache between tests
resetConfigLoader();
});
test('uses confInit as programmatic default', () => {
const config = useConfig(DatabaseConfig, {
confInit: { host: 'test-db' },
});
expect(config.host).toBe('test-db');
});Test configuration file
Create .env.test.yaml for test-specific values:
database:
host: localhost
database: myapp_test
logging:
level: error