A zero-dependency, type-safe environment variable management library for TypeScript applications. envoyage allows you to define environment configurations with multiple resolution strategies while maintaining full type safety across different deployment environments.
envoyage is completely agnostic about how your variables are loaded, it solely provides a safe way to declare your environment structure based on your specific requirements.
📚 View Full Documentation →
Complete guides, API reference, and examples are available in our documentation site.
npm install envoyage
import { createEnvironmentRegistry, defineType } from 'envoyage'
// Define your environments
const envReg = createEnvironmentRegistry()
.addEnv(
"local",
defineType<{ env: Record<string, string> }>(),
(env) => env
.addResolution("hardcoded", defineType<string>(), (data) => data.payload)
.addResolution("from-env", defineType<string | undefined>(), (data) =>
data.envData.env[data.payload ?? data.variableName])
)
.addEnv(
"production",
defineType<{ secrets: Record<string, string> }>(),
(env) => env
.addResolution("hardcoded", defineType<string>(), (data) => data.payload)
.addResolution("from-secrets", defineType<string | undefined>(), (data) =>
data.envData.secrets[data.payload ?? data.variableName])
)
// Define your variables
const varReg = envReg.createVariableRegistry()
.addVar("DATABASE_URL", (v) => v
.for("local", "from-env")
.for("production", "from-secrets"))
.addVar("IS_PRODUCTION", (v) => v
.for("local", "hardcoded", "false")
.for("production", "hardcoded", "true"))
// Create a resolver for a specific environment
const localResolver = varReg.createResolver(
"local",
{ env: { DATABASE_URL: "localhost:5432/myapp" } }
)
console.log(localResolver.get("DATABASE_URL")) // "localhost:5432/myapp"
console.log(localResolver.get("IS_PRODUCTION")) // "false"
An EnvironmentRegistry
manages multiple environments, each with their own data structure and resolution methods:
const envReg = createEnvironmentRegistry()
.addEnv("local", defineType<LocalEnvData>(), configureLocalEnv)
.addEnv("production", defineType<ProdEnvData>(), configureProdEnv)
Each environment defines:
.addEnv(
"workflows",
defineType<{ githubSecrets: Record<string, string> }>(),
(env) => env
.addResolution("hardcoded", defineType<string>(), (data) => data.payload)
.addResolution("from-github-secrets", defineType<string | undefined>(), (data) =>
data.envData.githubSecrets[data.payload ?? data.variableName])
.addResolution("from-aws-secrets", defineType<undefined>(), async (data) => {
// Async resolution example
const secret = await fetchFromAWS(data.variableName)
return secret
})
)
A VariableRegistry
defines environment variables and how they should be resolved in each environment:
const varReg = envReg.createVariableRegistry()
.addVar("API_KEY", (v) => v
.for("local", "from-env")
.for("production", "from-aws-secrets"))
.addVar("APP_NAME", (v) => v
.for("local", "hardcoded", "MyApp-Dev")
.for("production", "hardcoded", "MyApp"))
For runtime-provided values:
const varReg = envReg.createVariableRegistry()
.addVar("DOCUMENT_BUCKET", (v) => v
.dynamicFor("local", "bucketName")
.for("production", "from-secrets"))
// Provide dynamic data when creating the resolver
const resolver = varReg.createResolver(
"local",
envData,
{ bucketName: "my-local-bucket" }
)
Create environment-specific resolvers to access variable values:
const resolver = varReg.createResolver("production", {
secrets: { API_KEY: "secret-value" }
})
// Get a single variable value
const apiKey = resolver.get("API_KEY") // Type-safe access
// Get all accessible variables
const allValues = await resolver.getAll()
// { API_KEY: "secret-value", APP_NAME: "MyApp", ... }
// Get variables that use a specific resolution in another environment
const hardcodedInLocal = resolver.getAllFor("local", "hardcoded")
// Gets values from production for variables that use "hardcoded" in local
envoyage automatically handles async resolutions:
// If the resolution is async, the return type becomes Promise<string>
const apiKey = await resolver.get("API_KEY")
Combine multiple variable registries for modular configuration:
const authVarReg = envReg.createVariableRegistry()
.addVar("AUTH_SECRET", (v) => v.for("local", "from-env"))
const dbVarReg = envReg.createVariableRegistry()
.addVar("DATABASE_URL", (v) => v.for("local", "from-env"))
const globalVarReg = envReg.createVariableRegistry()
.mergeWith(authVarReg)
.mergeWith(dbVarReg)
.addVar("APP_VERSION", (v) => v.for("local", "hardcoded", "1.0.0"))
The listVariables
method enables writing validation scripts to ensure all required environment variables are properly configured:
// Get all variables that should be in your .env file
const envVars = varReg.listVariables("local", "from-env")
// Example validation script
const validateEnvFile = function () {
const missingVars = envVars.filter(name => !process.env[name])
if (missingVars.length > 0)
throw new Error(
`Missing required environment variables in .env: ${missingVars.join(", ")}`
)
}
For runtime environment selection while maintaining type safety:
const resolver = varReg.createDynamicResolver({
local: [{ env: process.env }],
production: [{ secrets: await getSecrets() }]
}, () => process.env.NODE_ENV === "production" ? "production" : "local")
// Only variables defined in ALL environments are accessible
const value = resolver.get("SHARED_VARIABLE")
envoyage provides complete type safety across all aspects of environment configuration - from environment names and resolution methods to variable definitions and return types. The TypeScript compiler ensures your environment configuration is valid at compile time.
This project is licensed under the MIT License.