Config Standards Skill
Standards and patterns for SDD configuration management.
Principles
- Single source of truth - All config lives in
components/config/ - Environment layering -
envs/default/→envs/{env}/merge order - Minimal env vars - Only
SDD_CONFIG_PATHallowed for servers - Secrets as references - Config contains K8s Secret names, not values
- Fail fast - Validate config at startup, crash on invalid
- Environment agnosticism - Components never know which environment they're in
Directory Structure
components/config/
├── package.json # Workspace package for type imports
├── tsconfig.json # TypeScript config
├── envs/
│ ├── default/
│ │ └── config.yaml # Base configuration (always merged first)
│ ├── local/
│ │ └── config.yaml # Local development overrides
│ ├── staging/
│ │ └── config.yaml # Staging overrides (add as needed)
│ └── production/
│ └── config.yaml # Production overrides (add as needed)
├── schemas/
│ └── config.schema.json # JSON Schema for validation
└── types/
├── index.ts # Re-exports all config types
└── {component}.ts # Per-component type definitions
Naming Conventions
- Config sections match component names
- Use kebab-case for all names
- Secret references end with
Secretsuffix (e.g.,passwordSecret)
# Section names match component names
task-service: # -> components/servers/task-service/
port: 3000
task-dashboard: # -> components/webapps/task-dashboard/
apiBaseUrl: /api
taskdb: # -> components/databases/taskdb/
host: localhost
Config Structure
# envs/default/config.yaml
global: {} # Reserved for future cross-cutting concerns
# Each component gets a section matching its directory name
server-task-service:
port: 3000
probesPort: 9090
logLevel: info
database:
host: db.internal
port: 5432
name: taskdb
user: app
passwordSecret: task-service-db-credentials
pool: 10
webapp-task-dashboard:
apiBaseUrl: /api
features:
darkMode: true
Environment Agnosticism
Components are environment-agnostic. Server, frontend, database, contract components never know which environment they're running in. They receive config values and use them.
Only two places know about environments:
components/config/- hasenvs/local/,envs/production/, etc.components/helm-*/- hasvalues-local.yaml,values-production.yaml, etc.
A server component:
- ✅ Reads
config.portand listens on it - ✅ Reads
config.database.hostand connects to it - ❌ Never checks "am I in production?"
- ❌ Never has
if (env === 'local')logic
Type Definitions
Types are defined in components/config/types/:
// types/server.ts
export type ServerConfig = Readonly<{
port?: number;
probesPort?: number;
logLevel?: 'debug' | 'info' | 'warn' | 'error';
database?: Readonly<{
host?: string;
port?: number;
name?: string;
user?: string;
passwordSecret?: string;
pool?: number;
}>;
}>;
Components import types via workspace package:
// In server component
import type { ServerConfig } from '@my-project/config/types';
Schema Management
- Main schema (
schemas/config.schema.json) validates all config - Start minimal, extend as config evolves
- Schema is validated at generate time AND at runtime
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"global": { "type": "object" },
"server-task-service": {
"type": "object",
"properties": {
"port": { "type": "number" },
"logLevel": { "enum": ["debug", "info", "warn", "error"] }
}
}
}
}
Local Development Workflow
- Add config property to
components/config/envs/default/config.yaml - Add override (if needed) to
components/config/envs/local/config.yaml - Update schema in
components/config/schemas/config.schema.json - Update types in
components/config/types/{component}.ts - Generate merged config:
/sdd I want to generate config - Start server:
SDD_CONFIG_PATH=./local-config.yaml npm start
Secret Handling
Config contains secret references (K8s Secret names), not values:
# In config YAML
server-task-service:
database:
host: db.production.internal
passwordSecret: "task-service-db-credentials" # K8s Secret name
At deploy time, Helm maps these references to actual secrets:
# In helm-task-service/templates/deployment.yaml
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.config.database.passwordSecret }}
key: password
The application reads DB_PASSWORD from environment, not from config YAML.
Gitignore
Generated config files should be gitignored:
# Generated config files in server components (never commit)
components/server-*/local-config.yaml
components/server-*/*.schema.json
All envs/ directories ARE committed - they contain references only.
NODE_ENV Handling
NODE_ENV is an infrastructure exception, not application config. It exists because third-party libraries check it for optimizations.
Application code NEVER reads NODE_ENV. It's injected by Helm for library behavior only.
| Environment | NODE_ENV | Set By |
|---|---|---|
| Local dev | Not set | (libraries default to development) |
| K8s staging | development | Helm values |
| K8s production | production | Helm values |
Validation Rules
- Required fields - Schema can mark fields as required
- Type checking - Schema validates types (string, number, object)
- Enum values - Schema constrains to allowed values
- Pattern matching - Schema can validate string patterns
Validation happens:
- At
/sdd I want to generate configtime (CLI) - At server startup (runtime)
Input / Output
This skill defines no input parameters or structured output.