<p align="center">
<img src="https://raw.githubusercontent.com/lupodevelop/envie/91a8dd6/assets/img/envie.png" alt="envie logo" width="200" />
</p>
[](https://hex.pm/packages/envie)[](https://hexdocs.pm/envie/)[](https://gleam.run)[](https://opensource.org/licenses/MIT)
# envie
> Why the name? `envy` was already taken on Hex, so we went with `envie`.
> Because you shouldn't be jealous of other languages' config loaders β you should just *desire* (French: *envie*) a better one. Plus, it makes your environment variables feel 20% more sophisticated. π₯
Type-safe environment configuration for Gleam.
Cross-platform and zero runtime dependencies.
envie is simple at first, and scales cleanly as your application grows.
From single variables to fully structured configuration.
## What envie helps you do
- read and write env vars without ceremony
- parse values into real types
- keep configuration explicit and predictable
- scale from simple defaults to structured config
- inspect what actually happened at runtime
## Quick start
```sh
gleam add envie
```
```gleam
import envie
pub fn main() {
let port = envie.get_int("PORT", 3000)
let debug = envie.get_bool("DEBUG", False)
// That's it. No setup, no ceremony.
}
```
## Core API
Read, write, and remove environment variables on any target.
```gleam
envie.get("HOME") // -> Ok("/Users/you")
envie.set("APP_ENV", "production")
envie.unset("TEMP_TOKEN")
let all = envie.all() // -> Dict(String, String)
```
## Type-safe getters
Parse common types with defaults.
```gleam
let port = envie.get_int("PORT", 3000)
let ratio = envie.get_float("RATIO", 1.0)
let debug = envie.get_bool("DEBUG", False)
let origins = envie.get_string_list("ORIGINS", separator: ",", default: [])
```
Boolean parsing accepts:
- `true`, `yes`, `1`, `on`
- `false`, `no`, `0`, `off`
## Validated access
Use `require_*` when missing or invalid values should fail.
```gleam
import envie
let assert Ok(key) = envie.require_string("API_KEY")
let assert Ok(port) = envie.require_port("PORT")
let assert Ok(url) = envie.require_web_url("DATABASE_URL")
let assert Ok(name) = envie.require_non_empty_string("APP_NAME")
let assert Ok(on) = envie.require_bool("DEBUG")
let assert Ok(ratio) = envie.require_float_range("RATIO", min: 0.0, max: 1.0)
let assert Ok(env) = envie.require_one_of("APP_ENV", ["development", "staging", "production"])
```
## Custom validation
Compose a decoder with `envie/decode` when built-in checks are not enough.
```gleam
import envie
import envie/decode
import gleam/string
let secret_decoder =
decode.string()
|> decode.validated(fn(s) {
case string.length(s) >= 32 {
True -> Ok(s)
False -> Error("Secret must be at least 32 characters")
}
})
let assert Ok(secret) = envie.require("JWT_SECRET", secret_decoder)
```
## Optional variables
```gleam
let assert Ok(maybe_port) = envie.optional("METRICS_PORT", decode.int())
// Ok(None) when missing, Ok(Some(value)) when present and valid
```
## `.env` file loading
Load `.env` files with comments, quoted values, and `export` prefixes.
By default, existing environment values are preserved.
Use `load_override` / `load_override_from` to overwrite.
```gleam
let assert Ok(Nil) = envie.load()
let assert Ok(Nil) = envie.load_from("config/.env.local")
let assert Ok(Nil) = envie.load_from_string("PORT=8080")
let assert Ok(Nil) = envie.load_override()
```
Example `.env` file:
```env
# Application
PORT=8080
DEBUG=true
# Secrets
export API_KEY="sk-1234567890"
DB_PASSWORD='hunter2'
```
## When you need more than defaults
As your app grows, configuration tends to spread across modules and become harder to validate and reason about.
`envie/schema` keeps everything in one place.
```gleam
import envie/schema
import envie/decode
pub type Config {
Config(
port: Int,
debug: Bool,
)
}
let config_schema =
schema.build2(
schema.field("PORT", decode.int())
|> schema.default(3000),
schema.field("DEBUG", decode.bool())
|> schema.default(False),
Config,
)
let assert Ok(config) = schema.load(config_schema)
```
This gives you:
- one place for config declarations
- typed parsing and validation
- explicit defaults and optional values
## Multiple environments
For more complex setups (multiple environments, overrides, deployments), you can load multiple `.env` files with explicit priority.
```gleam
import envie/dotenv
let assert Ok(Nil) =
dotenv.load([
dotenv.override(".env"),
dotenv.optional(".env.local"),
dotenv.required(".env.production"),
])
```
- `override` overwrites existing values
- `optional` is ignored if missing
- `required` fails if missing
## Debugging configuration
When configuration becomes non-trivial, itβs often unclear:
- which values were actually read
- which defaults were applied
- where a failure happened
`envie/inspect` lets you trace configuration loading:
```gleam
import envie/inspect
let trace = inspect.load_with_trace(config_schema)
```
This is especially useful in debugging production issues or validating deployments.
## Testing utilities
Helpers that guarantee the environment is restored after each test.
```gleam
import envie
import envie/testing
pub fn my_feature_test() {
testing.with_env([#("PORT", "3000"), #("DEBUG", "true")], fn() {
let port = envie.get_int("PORT", 8080)
port |> should.equal(3000)
})
// Original environment is restored automatically
}
pub fn isolated_test() {
testing.isolated(fn() {
envie.get("PATH") |> should.equal(Error(Nil))
})
// Everything restored
}
```
> [!WARNING]
> **Test Concurrency**: Environment variables are global to the process. Since Gleam runs tests in parallel by default, multiple tests using `testing.*` concurrently may interfere with each other. Use `gleam test -- --seed 123` (or any seed) to run tests with a single worker if you encounter flaky tests.
## Error formatting
Every error type carries enough context to produce clear messages.
```gleam
case envie.require_int("PORT") {
Ok(port) -> start_server(port)
Error(err) -> {
io.println_error(envie.format_error(err))
// "PORT: invalid value "abc" β Expected integer, got: abc"
}
}
```
## API at a glance
| Function | Returns | Notes |
|--------------------------------------|----------------------------|----------------------|
| `get` | `Result(String, Nil)` | Raw access |
| `set` | `Nil` | |
| `unset` | `Nil` | |
| `all` | `Dict(String, String)` | |
| `get_string` | `String` | Falls back to caller-supplied default |
| `get_int` | `Int` | Falls back to caller-supplied default |
| `get_float` | `Float` | Falls back to caller-supplied default |
| `get_bool` | `Bool` | Falls back to default; true/yes/1/on |
| `get_string_list` | `List(String)` | Falls back to default; splits & trims |
| `require` | `Result(a, Error)` | Decoder-based |
| `require_string` | `Result(String, Error)` | |
| `require_int` | `Result(Int, Error)` | |
| `require_int_range` | `Result(Int, Error)` | |
| `require_float` | `Result(Float, Error)` | |
| `require_float_range` | `Result(Float, Error)` | |
| `require_url` | `Result(Uri, Error)` | Permissive RFC parse |
| `require_url_with_scheme` | `Result(Uri, Error)` | e.g. ["postgres"] |
| `require_web_url` | `Result(Uri, Error)` | http or https only |
| `require_non_empty_string` | `Result(String, Error)` | |
| `require_string_prefix` | `Result(String, Error)` | |
| `require_string_list` | `Result(List(String), Error)` | |
| `require_int_list` | `Result(List(Int), Error)` | |
| `require_bool` | `Result(Bool, Error)` | true/yes/1/on |
| `require_port` | `Result(Int, Error)` | 1β65 535 |
| `require_one_of` | `Result(String, Error)` | Allow-list check |
| `optional` | `Result(Option(a), Error)` | |
| `load` | `Result(Nil, LoadError)` | `.env` in cwd |
| `load_from` | `Result(Nil, LoadError)` | Custom path |
| `load_from_string` | `Result(Nil, LoadError)` | From string |
| `load_override` | `Result(Nil, LoadError)` | Overwrites env |
| `load_override_from` | `Result(Nil, LoadError)` | Overwrites env |
| `load_from_string_override` | `Result(Nil, LoadError)` | Overwrites env |
## Cross-platform
---
envie works on Erlang and all major JavaScript runtimes.
- **Erlang/OTP** β uses `os:getenv/1`, `os:putenv/2`, `os:unsetenv/1`.
- **JavaScript (Node.js, Bun)** β uses `process.env` and `fs` module.
- **JavaScript (Deno)** β uses `Deno.env` and `Deno.readTextFileSync`.
- **JavaScript (Browser)** β environment variables and `.env` loading are safely disabled (returning `Error`) without crashing your build or runtime.
## Dependencies & Requirements
---
* Gleam **1.14** or newer.
* OTP 27+ on the BEAM.
* Just `gleam_stdlib` β no runtime dependencies.
---
<p align="center">Made with Gleam π</p>