Skip to content

lupodevelop/envie

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

envie logo

Package VersionHex DocsBuilt with GleamLicense: MIT

envie

Type-safe environment variables for Gleam. Zero external dependencies, cross-platform (Erlang + JavaScript).

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

envie gets out of your way: import it, call get(...), and you're done. When you need more typed parsing, validation, .env loading, test isolation, it's all there without changing the core workflow.

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 without boilerplate. When the variable is missing or the value cannot be parsed, you get the default back.

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 (case-insensitive):

Truthy Falsy
true yes 1 on false no 0 off

Validated access

When a missing or malformed variable should be a hard error, use require_*. Every function returns Result(value, Error) with a structured error you can format for logs.

No extra imports — just envie:

import envie

let assert Ok(key)    = envie.require_string("API_KEY")
let assert Ok(port)   = envie.require_port("PORT") // 1–65 535
let assert Ok(url)    = envie.require_web_url("DATABASE_URL") // requires http/https
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

If none of the built-in require_* functions covers your case, import envie/decode and compose a custom decoder:

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)

This is the only scenario where you need envie/decode.

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

Supports comments (#), inline comments (PORT=8080 # default), blank lines, export prefix, and single/double-quoted values.

By default, existing environment variables are not overwritten. Use load_override / load_override_from when .env values should win.

let assert Ok(Nil) = envie.load()                          // .env in cwd
let assert Ok(Nil) = envie.load_from("config/.env.local")  // custom path
let assert Ok(Nil) = envie.load_from_string("PORT=8080")   // from string

// Force overwrite
let assert Ok(Nil) = envie.load_override()

Example .env file:

# Application
PORT=8080
DEBUG=true

# Secrets
export API_KEY="sk-1234567890"
DB_PASSWORD='hunter2'

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
}

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.

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.

Made with Gleam 💜

About

Cross-platform type-safe environment variables for Gleam

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors