A type-safe environment variable management system for Swift applications.
- Type-safe environment access: Access environment variables with type conversion support for Int, Bool, URL, and String
- Multiple file format support: Supports both JSON and standard KEY=VALUE (.env) file formats
- Environment-aware loading: Automatically loads base configuration and environment-specific overrides
- Layered configuration: Clear precedence order from defaults → base files → environment files → process environment
- Required keys validation: Specify and validate required environment variables at runtime
- Dependencies integration: Built-in support for the Dependencies package for clean dependency injection
- Test support: Includes test helpers and mock values for testing
- Error handling: Comprehensive error handling with custom error types
- Logging integration: Built-in logging support using Swift's Logger
To use environment variables with Dependencies, conform to DependencyKey
:
extension EnvironmentVariables: @retroactive DependencyKey {
public static var liveValue: Self {
#if DEBUG
let environment = "development"
#else
let environment: String? = nil
#endif
return try! EnvironmentVariables.live(
environmentConfiguration: .projectRoot(
URL.projectRoot,
environment: environment
)
)
}
}
// Helper for finding the project root
extension URL {
public static var projectRoot: URL {
.init(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
}
}
This setup:
- Uses
@retroactive
to conform toDependencyKey
- Automatically loads
.env
+.env.development
in DEBUG builds - Loads only
.env
+ process environment in production - Supports environment-specific configuration overrides
Access environment variables using the @Dependency
property wrapper:
import Dependencies
struct MyFeature {
@Dependency(\.envVars) var env
func configure() throws {
guard let apiKey = env["API_KEY"] else {
throw ConfigError.missingApiKey
}
// Use apiKey...
}
}
import EnvironmentVariables
// Initialize with environment-aware configuration
let env = try EnvironmentVariables.live(
environmentConfiguration: .projectRoot(
URL(fileURLWithPath: "/path/to/project"),
environment: "development"
),
requiredKeys: ["APP_SECRET", "DATABASE_URL"]
)
// Access values with type safety
let port: Int? = env.int("PORT")
let isDevelopment: Bool? = env.bool("DEVELOPMENT")
let databaseUrl: URL? = env.url("DATABASE_URL")
let apiKey: String? = env["API_KEY"]
The package supports multiple configuration strategies with clear precedence:
Create environment files in your project root:
.env
(Base configuration):
# Base configuration for all environments
APP_NAME=My Application
DEBUG=false
DATABASE_HOST=localhost
DATABASE_PORT=5432
CACHE_TTL=3600
.env.development
(Development overrides):
# Development-specific overrides
DEBUG=true
DATABASE_NAME=myapp_dev
CACHE_TTL=60
DEV_TOOLS_ENABLED=true
.env.production
(Production overrides):
# Production-specific overrides
DEBUG=false
DATABASE_NAME=myapp_prod
CACHE_TTL=7200
ENABLE_METRICS=true
let env = try EnvironmentVariables.live(
environmentConfiguration: .projectRoot(
URL(fileURLWithPath: "/path/to/project"),
environment: "development" // Loads .env + .env.development
)
)
Precedence Order (lowest to highest):
- Default values (empty by default)
- Base
.env
file - Environment-specific file (e.g.,
.env.development
) - Process environment variables
For simpler use cases, load a single environment file:
let env = try EnvironmentVariables.live(
environmentConfiguration: .singleFile(
URL(fileURLWithPath: ".env.development")
)
)
Both KEY=VALUE (.env) and JSON formats are supported:
KEY=VALUE format:
API_KEY=my-secret-key
DEBUG=true
DATABASE_URL=postgresql://user:pass@localhost:5432/db
JSON format:
{
"API_KEY": "my-secret-key",
"DEBUG": "true",
"DATABASE_URL": "postgresql://user:pass@localhost:5432/db"
}
The EnvironmentConfiguration
enum provides three loading strategies:
Load only from process environment variables:
let env = try EnvironmentVariables.live(
environmentConfiguration: .none
)
Load from a single environment file:
let env = try EnvironmentVariables.live(
environmentConfiguration: .singleFile(
URL(fileURLWithPath: "/path/to/.env.development")
)
)
Load base configuration with optional environment-specific overrides:
// Load only .env
let env = try EnvironmentVariables.live(
environmentConfiguration: .projectRoot(
URL(fileURLWithPath: "/path/to/project"),
environment: nil
)
)
// Load .env + .env.staging
let env = try EnvironmentVariables.live(
environmentConfiguration: .projectRoot(
URL(fileURLWithPath: "/path/to/project"),
environment: "staging"
)
)
While dictionary-style access (env["KEY"]
) is always available, you can add strongly-typed property access by extending EnvironmentVariables
:
extension EnvironmentVariables {
public var appSecret: String {
get { self["APP_SECRET"]! }
set { self["APP_SECRET"] = newValue }
}
public var baseUrl: URL {
get { URL(string: self["BASE_URL"]!)! }
set { self["BASE_URL"] = newValue.absoluteString }
}
public var port: Int {
get { Int(self["PORT"]!)! }
set { self["PORT"] = String(newValue) }
}
}
For optional environment variables, use optional types:
import Logging
extension EnvironmentVariables {
public var logLevel: Logger.Level? {
get { self["LOG_LEVEL"].flatMap { Logger.Level(rawValue: $0) } }
set { self["LOG_LEVEL"] = newValue?.rawValue }
}
public var httpsRedirect: Bool? {
get { self.bool("HTTPS_REDIRECT") }
set { self["HTTPS_REDIRECT"] = newValue.map { $0 ? "true" : "false" } }
}
}
For environment variables that contain comma-separated values:
extension EnvironmentVariables {
public var allowedHosts: [String]? {
get {
self["ALLOWED_HOSTS"]?
.components(separatedBy: ",")
.map { $0.trimmingCharacters(in: .whitespaces) }
}
set { self["ALLOWED_HOSTS"] = newValue?.joined(separator: ",") }
}
}
This approach provides:
- Type safety: Variables are converted to their proper Swift types
- Auto-completion: Access environment variables using dot syntax
- Default values: Can specify defaults for optional variables
- Validation: Add validation logic in the getter if needed
You can add swift-environment-variables
to an Xcode project by adding it as a package dependency.
- From the File menu, select Add Packages...
- Enter "https://github.com/coenttb/swift-environment-variables" into the package repository URL text field
- Link the package to your target
For a Swift Package Manager project, add the following to your Package.swift
file:
dependencies: [
.package(url: "https://github.com/coenttb/swift-environment-variables", from: "0.0.1")
]
- coenttb/swift-web: Modular tools to simplify web development in Swift
- coenttb/coenttb-com-server: The backend server for coenttb.com that uses EnvironmentVariables.
If you're working on your own Swift project, feel free to learn, fork, and contribute.
Got thoughts? Found something you love? Something you hate? Let me know! Your feedback helps make this project better for everyone. Open an issue or start a discussion—I'm all ears.
This project builds upon foundational work by Point-Free (Brandon Williams and Stephen Celis). This package is inspired by their approach on pointfreeco
.
This project is licensed by coenttb under the Apache 2.0 License. See LICENSE for details.