envie logo

Package VersionHex DocsBuilt with GleamLicense: 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

Quick start

gleam add envie
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.

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.

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:

Validated access

Use require_* when missing or invalid values should fail.

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.

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

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.

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:

# 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.

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:

Multiple environments

For more complex setups (multiple environments, overrides, deployments), you can load multiple .env files with explicit priority.

import envie/dotenv

let assert Ok(Nil) =
  dotenv.load([
    dotenv.override(".env"),
    dotenv.optional(".env.local"),
    dotenv.required(".env.production"),
  ])

Debugging configuration

When configuration becomes non-trivial, it’s often unclear:

envie/inspect lets you trace configuration loading:

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.

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
}

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.

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

FunctionReturnsNotes
getResult(String, Nil)Raw access
setNil
unsetNil
allDict(String, String)
get_stringStringFalls back to caller-supplied default
get_intIntFalls back to caller-supplied default
get_floatFloatFalls back to caller-supplied default
get_boolBoolFalls back to default; true/yes/1/on
get_string_listList(String)Falls back to default; splits & trims
requireResult(a, Error)Decoder-based
require_stringResult(String, Error)
require_intResult(Int, Error)
require_int_rangeResult(Int, Error)
require_floatResult(Float, Error)
require_float_rangeResult(Float, Error)
require_urlResult(Uri, Error)Permissive RFC parse
require_url_with_schemeResult(Uri, Error)e.g. [“postgres”]
require_web_urlResult(Uri, Error)http or https only
require_non_empty_stringResult(String, Error)
require_string_prefixResult(String, Error)
require_string_listResult(List(String), Error)
require_int_listResult(List(Int), Error)
require_boolResult(Bool, Error)true/yes/1/on
require_portResult(Int, Error)1–65 535
require_one_ofResult(String, Error)Allow-list check
optionalResult(Option(a), Error)
loadResult(Nil, LoadError).env in cwd
load_fromResult(Nil, LoadError)Custom path
load_from_stringResult(Nil, LoadError)From string
load_overrideResult(Nil, LoadError)Overwrites env
load_override_fromResult(Nil, LoadError)Overwrites env
load_from_string_overrideResult(Nil, LoadError)Overwrites env

Cross-platform


envie works on Erlang and all major JavaScript runtimes.

Dependencies & Requirements



Made with Gleam 💜

Search Document