configit

a general typescript configuration service


Project maintained by Kibibit Hosted on GitHub Pages — Theme by mattgraham

@kibibit/configit

Build Tests All Contributors

A general typescript configuration service


Unless forced to create a new service, this service will return the first created service

Usage

Create a new class to define your configuration. The class should extend the Config class from this repo

import { IsNumber, IsString } from 'class-validator';

import { BaseConfig, Configuration, ConfigVariable } from '@kibibit/configit';

@Configuration()
export class ProjectConfig extends BaseConfig {
  @ConfigVariable('Server port')
  @IsNumber()
  PORT: number;

  @ConfigVariable([
    'This is the slack API to talk and report to channel "hello"'
  ])
  @IsString()
  SLACK_API_KEY: string;
}

}

Then, in your code, initialize the config service when you bootstrap your application

import express from 'express';
import { ConfigService } from '@kibibit/configit';
import { ProjectConfig } from './project-config.model';

export const configService = new ConfigService<ProjectConfig>(ProjectConfig);
const app = express();

app.get( '/', ( req, res ) => {
  res.send( 'Hello world!' );
} );

app.listen(configService.config.PORT, () => {
  console.log(
    `server started at http://localhost:${ configService.config.PORT }`
  );
});

You can extend the configuration to add your own customization and functions!

import { chain } from 'lodash';

import { ConfigService, IConfigServiceOptions } from '@kibibit/configit';
import { WinstonLogger } from '@kibibit/nestjs-winston';

import { ExtProjectConfig } from './ext-project-config.model';
import { initializeWinston } from './winston.config';

export class ExtConfigService extends ConfigService<ExtProjectConfig> {
  public logger: WinstonLogger;
  constructor(passedConfig?: Partial<ExtProjectConfig>, options: IConfigServiceOptions = {}) {
    super(ExtProjectConfig, passedConfig, options);

    initializeWinston(this.appRoot);
    this.logger = new WinstonLogger('');
  }

  getSlackApiObject() {
    const slackApiObject = chain(this.toPlainObject())
      .pickBy((value, key) => key.startsWith('SLACK_'))
      .mapKeys((value, key) => key.replace(/^SLACK_/i, ''))
      .mapKeys((value, key) => key.toLowerCase())
      .value();

    return slackApiObject;
  }
}

export const configService = new ExtConfigService() as ExtConfigService;

Vault Integration

Configit supports HashiCorp Vault integration for dynamic secrets management with automatic TTL-based refresh. This enables secure, centralized secret management with automatic rotation for database credentials, API keys, and other sensitive configuration values.

Overview

Vault integration provides:

Prerequisites

  1. HashiCorp Vault running and accessible
    • Development: http://127.0.0.1:8200
    • Production: HTTPS endpoint (required)
  2. Authentication Method configured:
    • Token: Simple token-based auth (dev only)
    • AppRole: Recommended for production
    • GCP IAM: For GCP workloads
    • AWS IAM: For AWS workloads
  3. Vault Secrets configured:
    • KV secrets: secret/data/myapp/api_key
    • Database credentials: database/creds/my-role
    • Other engines as needed

Installation

Vault integration is included in @kibibit/configit - no additional packages required.

Configuration with Vault

Configure Vault integration by passing vault options to ConfigService:

import { ConfigService, IVaultConfigOptions } from '@kibibit/configit';

const vaultOptions: IVaultConfigOptions = {
  endpoint: process.env.VAULT_ADDR || 'http://127.0.0.1:8200',
  auth: {
    method: 'token',
    config: {
      token: process.env.VAULT_TOKEN
    }
  },
  tls: {
    enabled: true,
    verifyCertificate: true
  },
  refreshBuffer: 300, // Refresh 5 minutes before expiry (default)
  fallback: {
    required: true, // Fail fast if Vault unavailable
    useCacheOnFailure: true,
    maxCacheAge: 3600000 // 1 hour
  }
};

const configService = new ConfigService(ProjectConfig, undefined, {
  vault: vaultOptions
});

// Initialize Vault (async - call before accessing config)
await configService.initializeVault();

Authentication Methods

Token Authentication (Development):

auth: {
  method: 'token',
  config: {
    token: process.env.VAULT_TOKEN
  }
}

AppRole Authentication (Production):

auth: {
  method: 'approle',
  config: {
    roleId: process.env.VAULT_ROLE_ID,
    secretId: process.env.VAULT_SECRET_ID, // From secure source
    mountPath: 'approle' // Optional, defaults to 'approle'
  }
}

GCP IAM Authentication:

auth: {
  method: 'gcp',
  config: {
    role: 'my-vault-role',
    serviceAccountKeyFile: '/path/to/key.json', // Optional, uses ADC if not provided
    serviceAccountEmail: 'my-service@project.iam.gserviceaccount.com', // Optional
    jwtExpiration: 900 // Optional, default 15 minutes
  }
}

AWS IAM Authentication:

auth: {
  method: 'aws',
  config: {
    role: 'my-vault-role'
    // Uses instance profile or environment credentials automatically
  }
}

Multiple Auth Methods (Fallback Chain):

auth: {
  methods: [
    {
      type: 'gcp',
      config: { role: 'my-role' }
    },
    {
      type: 'approle',
      config: {
        roleId: process.env.VAULT_ROLE_ID,
        secretId: process.env.VAULT_SECRET_ID
      }
    }
  ]
}

TLS Configuration

tls: {
  enabled: true, // Required (cannot be disabled)
  verifyCertificate: true, // Verify server certificate
  certificateFingerprint: 'sha256:...', // Optional: pin certificate
  caCert: '-----BEGIN CERTIFICATE-----\n...', // Optional: custom CA
  minVersion: 'TLSv1.2' // Optional: minimum TLS version
}

Refresh Buffer

The refresh buffer determines when secrets are refreshed before expiration:

refreshBuffer: 300 // Refresh 300 seconds (5 minutes) before TTL expires

Default: min(10% of TTL, 300 seconds) - whichever is smaller.

Vault Decorators

Use composable decorators to mark configuration properties as Vault secrets. Decorators work alongside @ConfigVariable and class-validator decorators.

@VaultPath(path) - Required

Specifies the Vault path for a property. Any property with @VaultPath is treated as a Vault secret.

@VaultPath('secret/data/myapp/api_key')
API_KEY: string;

@VaultEngine(type) - Optional

Specifies the Vault secrets engine type. Auto-detected from path if not provided.

@VaultEngine('database') // 'kv-v1', 'kv-v2', 'database', 'aws', 'gcp', etc.
DATABASE_PASSWORD: string;

Supported engines:

@VaultKey(key) - Optional

Specifies the key name within the secret. Only used for KV v1/v2 engines. Defaults to property name in kebab-case.

@VaultKey('api_key') // If key name differs from property name
API_KEY: string;

@VaultRefreshBuffer(seconds) - Optional

Override default refresh buffer for a specific secret.

@VaultRefreshBuffer(600) // Refresh 10 minutes before expiry
DATABASE_PASSWORD: string;

@VaultOptional() - Optional

Mark secret as optional - allows fallback to environment variable if Vault unavailable.

@VaultOptional()
FEATURE_FLAG?: boolean;

Usage Examples

Basic Example with Token Auth

import { BaseConfig, Configuration, ConfigVariable, VaultPath, VaultKey } from '@kibibit/configit';
import { IsString } from 'class-validator';

@Configuration()
export class AppConfig extends BaseConfig {
  @VaultPath('secret/data/myapp/api_key')
  @VaultKey('api_key')
  @ConfigVariable('API key for external service')
  @IsString()
  API_KEY: string;

  @ConfigVariable('Server port')
  @IsNumber()
  PORT: number;
}

// Initialize with Vault
const configService = new ConfigService(AppConfig, undefined, {
  vault: {
    endpoint: process.env.VAULT_ADDR || 'http://127.0.0.1:8200',
    auth: {
      method: 'token',
      config: {
        token: process.env.VAULT_TOKEN
      }
    }
  }
});

await configService.initializeVault();
console.log(configService.config.API_KEY); // Loaded from Vault

GCP IAM Authentication

const configService = new ConfigService(AppConfig, undefined, {
  vault: {
    endpoint: 'https://vault.example.com',
    auth: {
      method: 'gcp',
      config: {
        role: 'my-vault-role'
        // Uses Application Default Credentials (ADC)
      }
    },
    tls: {
      enabled: true,
      verifyCertificate: true
    }
  }
});

await configService.initializeVault();

Dynamic Database Credentials

import { BaseConfig, Configuration, ConfigVariable, VaultPath, VaultEngine, VaultKey } from '@kibibit/configit';
import { IsString } from 'class-validator';

@Configuration()
export class DatabaseConfig extends BaseConfig {
  @VaultPath('database/creds/my-role')
  @VaultEngine('database')
  @VaultKey('username')
  @ConfigVariable('Database username')
  @IsString()
  DATABASE_USERNAME: string;

  @VaultPath('database/creds/my-role')
  @VaultEngine('database')
  @VaultKey('password')
  @ConfigVariable('Database password')
  @IsString()
  DATABASE_PASSWORD: string;
}

Behavior:

Optional Secret with Fallback

@Configuration()
export class OptionalConfig extends BaseConfig {
  @VaultPath('secret/data/myapp/feature_flag')
  @VaultKey('enabled')
  @VaultOptional()
  @ConfigVariable('Optional feature flag')
  @IsOptional()
  @IsBoolean()
  FEATURE_FLAG_ENABLED?: boolean;
}

If Vault is unavailable, falls back to FEATURE_FLAG_ENABLED environment variable.

Mixed Vault and Non-Vault Properties

@Configuration()
export class MixedConfig extends BaseConfig {
  // From Vault
  @VaultPath('database/creds/my-role')
  @VaultEngine('database')
  @VaultKey('password')
  @ConfigVariable('Database password')
  @IsString()
  DATABASE_PASSWORD: string;

  // From environment variable (no Vault decorators)
  @ConfigVariable('Database host')
  @IsString()
  DATABASE_HOST: string;

  // From Vault
  @VaultPath('secret/data/myapp/api_key')
  @ConfigVariable('API key')
  @IsString()
  API_KEY: string;
}

Source Hierarchy

Configit uses the following source priority (highest to lowest):

  1. Vault (if configured) - Highest priority
  2. CLI arguments (--key=value)
  3. Environment variables (KEY=value)
  4. Config files (.env.development.json, etc.) - Lowest priority

Vault secrets are injected into nconf.overrides() which has the highest priority in the nconf hierarchy.

Health Monitoring

Monitor Vault connection status and secret refresh schedule:

const health = configService.getVaultHealth();

if (health) {
  console.log('Connected:', health.connected);
  console.log('Authenticated:', health.authenticated);
  console.log('Cached secrets:', health.cacheSize);
  console.log('Scheduled refreshes:', health.refreshQueueSize);
  console.log('Last refresh:', new Date(health.lastRefreshTime));
  console.log('Recent errors:', health.errors);
}

Health Status Fields:

Example: NestJS Integration

See examples/vault-nestjs-sequelize/ for a complete example showing:

// In NestJS onModuleInit
export class ConfigModule implements OnModuleInit {
  async onModuleInit() {
    await configService.initializeVault();
  }
}

Troubleshooting

Vault Connection Failed:

Secrets Not Refreshing:

Secret Not Found:

For more examples and advanced usage, see the examples/ folder.

Features

Examples

See the examples folder for a variety of usage examples

Contributors ✨

Thanks goes to these wonderful people (emoji key):

Neil Kalman
Neil Kalman

πŸ’» πŸ“– 🎨 🚧 πŸš‡ ⚠️
Nitzan Madar
Nitzan Madar

πŸ’»
Dafna Assaf
Dafna Assaf

πŸ’»

This project follows the all-contributors specification. Contributions of any kind are welcome!

Logo made by Good Ware from www.flaticon.com


Stay in touch