| name | code-conventions |
| description | Use when creating or editing TypeScript code in the orange-lab repo — stacks, components (`stacks/<module>/components/`, `components/`), or the `@orangelab/pulumi` library (`packages/pulumi/src/`). Covers code style, component patterns, Pulumi config conventions, and secrets handling. Not needed for markdown/documentation-only changes. |
Code Conventions
Code Style
- TypeScript with strict type checking.
npm test runs tsc + eslint; eslint enforces no-any, ?? over ||, optional chaining, and _ prefix for unused variables — don't restate these in code comments or reviews
- Prefer self-documenting code over comments; descriptive variable names and clear function signatures
- Keep code concise — omit default values unless useful to communicate intent
- Pass dependencies through args (providers, config, metadata) rather than accessing globals (except
config.ts)
- camelCase for variables, functions, methods; PascalCase for classes, interfaces, type aliases
- Group imports external packages first, then internal modules; sort alphabetically within each group
- Use
assert for validations that should never fail
- Prefer
variable?: Type over variable: Type | undefined
- Keep methods around 20 lines or less; inline simple operations (single
.map(), .filter()) rather than extracting private methods; orchestration methods coordinating multiple steps may be longer
- Prefer standard Kubernetes conventions unless there's a specific reason to deviate (e.g. node-role labels use
true value to match k3s control-plane and master roles)
Components
- Follow the established pattern for new modules and components
- Use constructor parameter properties with access modifiers (e.g.,
constructor(private readonly args: Args))
- Prefer composition over inheritance for component relationships
- Only export fields when needed for external use; keep sensitive values like auth keys internal when not required
- Share provider instances across components (pass via args) rather than creating duplicates
Initialization Order
When a value from Application (like this.app.debug) is needed during chained initialization, create the Application instance first: this.app = new Application(...);, then continue with chained methods: this.app.addStorage().addConfigVolume(...).
Configuration
- Pulumi stack files: Never modify or read
Pulumi.*.yaml files directly — they may contain secrets. Use pulumi config set/get (--secret for sensitive values).
- Use
config.require() instead of config.get() when a default value exists in Pulumi.yaml; don't use optional accessors (?., ??) for such settings. This prevents typos and simplifies processing logic
- Feature-gated validation: When a feature is conditionally enabled (e.g.,
smtp/enabled), validate its required settings only when enabled. Pulumi.yaml can then carry defaults for optional features without forcing users to set them
- Conditional requirements: When a feature is enabled, all its related settings should typically be required — the
enabled flag acts as the conditional gate
- Minimal config generation: Only generate config files (ConfigMaps/Secrets) when a feature requiring them is enabled
- Derive don't duplicate: Values that can be derived from existing resources (like URLs from
HttpEndpointInfo) should be derived, not manually configured
- Naming: Settings that map to container environment variables use the exact env var name (e.g.
DNS_SERVER_FORWARDERS instead of forwarders)
Secrets
- Use
envSecret field in ContainerSpec for sensitive data instead of command args — the Application class automatically creates Kubernetes Secrets and configures envFrom
- Environment variable names should be UPPERCASE
- Never expose passwords or sensitive data in command line arguments
Auto-generated Secrets
When a component generates secrets/tokens automatically (e.g., encryption keys), provide instructions in the component docs for:
- Retrieving the secret from stack outputs after first deployment
- Saving it to config with
--secret flag for backup restoration
Example: n8n, vaultwarden