Enviable is a small collection of functions to improve Elixir project configuration via environment variables following the 12-factor application model. It provides robust value conversion and works well with environment loaders like Dotenvy, Nvir, or Envious.
Core Principles
- Import in
config/runtime.exs- Standard location for runtime configuration - Use specific conversion functions - Prefer
fetch_env_as_integer!/1over manual conversion - Choose the right variant -
fetch_*!raises,fetch_*returns{:ok, value} | :error,get_*returns value or default - Leverage type-specific functions - Use
get_env_as_boolean/2,fetch_env_as_integer!/1, etc.
Decision Guide: When to Use What
Choose Your Fetch Variant
Use fetch_env!/1 when:
- Variable is required for application to run
- You want the application to crash immediately if missing
- No sensible default exists
Use fetch_env/1 when:
- Variable is required but you want to handle absence explicitly
- You need pattern matching on
{:ok, value}or:error - Building conditional configuration logic
Use get_env/2 when:
- Variable is optional
- You have a sensible default value
- Application can run when the result is
nil
Choose Your Conversion Function
Use fetch_env_as_TYPE!/1 when:
- Variable is required AND needs type conversion
- You want immediate crash on missing or invalid value
- Examples:
fetch_env_as_integer!("PORT"),fetch_env_as_boolean!("ENABLE_SSL")
Use fetch_env_as_TYPE/1 when:
- Variable is required but you want explicit error handling
- Returns
{:ok, converted_value}or:error - Example:
fetch_env_as_integer("PORT")
Use get_env_as_TYPE/2 when:
- Variable is optional with a default
- Returns converted value or default
- Example:
get_env_as_integer("PORT", default: 4000)
Common Patterns
Basic Configuration
# config/runtime.exs
import Config
import Enviable
config :my_app,
# Required values - crash if missing
secret_key: fetch_env!("SECRET_KEY"),
database_url: fetch_env!("DATABASE_URL"),
# Required with conversion
port: fetch_env_as_integer!("PORT"),
# Optional with defaults
ssl_enabled: get_env_as_boolean("SSL_ENABLED"),
pool_size: get_env_as_integer("POOL_SIZE", default: 10),
log_level: get_env_as_log_level("LOG_LEVEL", default: :info)With Environment Loaders
Using Nvir
import Nvir
import Enviable
client = fetch_env!("CLIENT")
dotenv!([".env", ".env.#{client}"])
config :my_app,
key: fetch_env!("SECRET_KEY"),
port: fetch_env_as_integer!("PORT")Using Dotenvy
import Config
import Enviable
client = fetch_env!("CLIENT")
Dotenvy.source([".env", ".env.#{client}"], side_effect: &put_env/1)
config :my_app,
key: fetch_env!("SECRET_KEY"),
port: fetch_env_as_integer!("PORT")Important: Dotenvy requires side_effect: &put_env/1 because Enviable works
with the system environment table. If there is another side effect specified,
ensure that it eventually uses System.put_env/1.
Using Envious
import Config
import Enviable
client = fetch_env!("CLIENT")
env_files = [".env", ".env.#{client}"]
loaded_env =
Enum.reduce(env_files, %{}, fn file, acc ->
with {:ok, contents} <- File.read(file),
{:ok, env} <- Envious.parse(contents) do
Map.merge(acc, env)
else
_ -> acc
end
end)
for {key, value} <- loaded_env, do: put_env_new(key, value)
config :my_app,
key: fetch_env!("SECRET_KEY"),
port: fetch_env_as_integer!("PORT")Type Conversions
Boolean Conversion
# Only "1" and "true" return true by default (case-insensitive)
# All other values return false
# Default is false if unset
ssl_enabled: get_env_as_boolean("SSL_ENABLED")
# With explicit default
ssl_enabled: get_env_as_boolean("SSL_ENABLED", default: true)
# With custom truthy values (other values return false)
debug: get_env_as_boolean("DEBUG", truthy: ["enabled", "on"])
# With custom falsy values (other values return true)
debug: get_env_as_boolean("DEBUG", default: true, falsy: ["disabled", "off"])
# Note: Cannot specify both truthy and falsyInteger Conversion
# Base 10 (default)
port: fetch_env_as_integer!("PORT")
# Different bases
hex_value: get_env_as_integer("HEX_VALUE", default: 0, base: 16)Atom Conversion
# Unsafe - creates new atoms
env: get_env_as_atom("MIX_ENV", default: :dev)
# Safe - only existing atoms
env: get_env_as_safe_atom("MIX_ENV", default: :dev, allowed: [:dev, :test, :prod])Module Conversion
# Unsafe - creates new atoms
adapter: get_env_as_module("ADAPTER", default: MyApp.DefaultAdapter)
# Safe - only allowed modules
adapter: get_env_as_safe_module("ADAPTER", default: MyApp.DefaultAdapter,
allowed: [MyApp.Adapter.Postgres, MyApp.Adapter.MySQL])List Conversion
# Comma-separated by default
hosts: get_env_as_list("HOSTS", default: ["localhost"])
# Custom delimiter
paths: get_env_as_list("PATHS", default: [], delimiter: ":")
# With type conversion
ports: get_env_as_list("PORTS", default: [], as: :integer)
# With complex type conversion
modules: fetch_env_as_list!("MODULES", as: :safe_module, allowed: [MyApp.A, MyApp.B])Chained Conversions
Base encoding and list conversions support an :as option to chain conversions:
# Decode base64, then parse as JSON
config: fetch_env_as_base64!("CONFIG", as: :json)
# Decode base32, then convert to atom (unsafe)
name: fetch_env_as_base32!("NAME", as: :atom, downcase: true)
# Split list, then convert each element to integer
ports: fetch_env_as_list!("PORTS", as: :integer)
# Split list, then convert each to safe module
adapters: fetch_env_as_list!("ADAPTERS", as: :safe_module,
allowed: [MyApp.Adapter.A, MyApp.Adapter.B])
# Decode URL-safe base64, then parse as Elixir term
data: fetch_env_as_url_base64!("DATA", as: :elixir)Available base encoding conversions with :as:
*_as_base16- Base16/hex encoding*_as_base32- Base32 encoding*_as_hex32- Base32 hex encoding*_as_base64- Base64 encoding*_as_url_base64- URL-safe Base64 encoding
When using :as, you can also pass options for the target type:
# Decode base64, parse as JSON with custom engine
config: fetch_env_as_base64!("CONFIG", as: :json, engine: Jason)
# Split list, convert to atoms with downcase
tags: fetch_env_as_list!("TAGS", as: :atom, downcase: true)JSON Conversion
# Uses configured JSON engine (Jason, JSON, :json, etc.)
config: get_env_as_json("APP_CONFIG", default: %{})
# Custom engine
config: get_env_as_json("APP_CONFIG", default: %{}, engine: Jason)Timeout Conversion
# Accepts timeout strings like "30s", "5m", "1h"
# Returns milliseconds as integer or :infinity
# Default is :infinity if unset
timeout: get_env_as_timeout("TIMEOUT")
# With explicit default (can be integer ms, :infinity, Duration, or keyword)
timeout: get_env_as_timeout("TIMEOUT", default: 5000)
timeout: get_env_as_timeout("TIMEOUT", default: "30s")
timeout: get_env_as_timeout("TIMEOUT", default: "PT30S")
timeout: get_env_as_timeout("TIMEOUT", default: Duration.new!(second: 30))
timeout: get_env_as_timeout("TIMEOUT", second: 30)Duration Conversion
# Accepts ISO8601 duration strings like "PT30S", "PT1H30M"
# Returns Duration struct
# Default is nil if unset
duration: get_env_as_duration("DURATION")
# With explicit default (can be Duration struct or ISO8601 string)
duration: get_env_as_duration("DURATION", default: "PT30S")
duration: get_env_as_duration("DURATION", default: Duration.new!(second: 30))Base Encoding Conversions
# Base16 (hex)
secret: fetch_env_as_base16!("SECRET_HEX")
# Base32 (standard alphabet: A-Z, 2-7)
token: fetch_env_as_base32!("TOKEN_B32")
# Base32 hex (extended hex alphabet: 0-9, A-V)
token: fetch_env_as_hex32!("TOKEN_HEX32", case: :lower)
# Base64
cert: fetch_env_as_base64!("CERTIFICATE")
# URL-safe Base64
key: fetch_env_as_url_base64!("API_KEY")PEM Conversion
# Parse PEM-encoded certificates/keys
cert: fetch_env_as_pem!("SSL_CERT")
# Filter specific entry types
cert: fetch_env_as_pem!("SSL_CERT", filter: :cert)
key: fetch_env_as_pem!("SSL_KEY", filter: :key)Conditional Configuration
case fetch_env("FEATURE_FLAG") do
{:ok, "enabled"} ->
config :my_app, feature_enabled: true
_ ->
config :my_app, feature_enabled: false
endSetting Variables
# Set unconditionally
put_env("MY_VAR", "value")
# Set only if not already set
put_env_new("MY_VAR", "default_value")
# Set multiple at once
put_env(%{"VAR1" => "value1", "VAR2" => "value2"})Available Conversion Types
| Type | Function | Description |
|---|---|---|
:atom | *_as_atom | Convert to atom (unsafe - creates new atoms) |
:safe_atom | *_as_safe_atom | Convert to existing atom only |
:boolean | *_as_boolean | Convert to boolean |
:charlist | *_as_charlist | Convert to charlist |
:decimal | *_as_decimal | Convert to Decimal (requires decimal package) |
:duration | *_as_duration | Convert to Duration |
:elixir | *_as_elixir | Parse as Elixir term (unsafe) |
:erlang | *_as_erlang | Parse as Erlang term (unsafe) |
:float | *_as_float | Convert to float |
:integer | *_as_integer | Convert to integer |
:json | *_as_json | Parse as JSON |
:list | *_as_list | Split into list |
:log_level | *_as_log_level | Convert to Logger level atom |
:module | *_as_module | Convert to module (unsafe - creates new atoms) |
:safe_module | *_as_safe_module | Convert to allowed module only |
:pem | *_as_pem | Parse PEM-encoded data |
:timeout | *_as_timeout | Convert to timeout |
:base16 | *_as_base16 | Decode Base16/hex |
:base32 | *_as_base32 | Decode Base32 |
:base64 | *_as_base64 | Decode Base64 |
:url_base64 | *_as_url_base64 | Decode URL-safe Base64 |
Configuration Options
Boolean Downcase
Configure case-folding for boolean conversions:
# config/config.exs
config :enviable, :boolean_downcase, :default # or :ascii, :greek, :turkicJSON Engine
Configure the JSON parsing engine:
# config/config.exs
config :enviable, :json_engine, Jason
# or
config :enviable, :json_engine, {Jason, :decode, [[floats: :decimals]]}Default engines (in order of preference):
Common Gotchas
Atom Creation -
*_as_atomcreates new atoms which are never garbage collected. Use*_as_safe_atomwith:allowedoption for user input.Module Creation -
*_as_modulehas the same atom creation issue. Use*_as_safe_modulewith:allowedoption.Dotenvy Side Effects - Must use
side_effect: &put_env/1with Dotenvy for Enviable to see loaded variables.Boolean Defaults -
get_env_as_boolean/2returnsfalseby default if unset. Only"1"and"true"(case-insensitive) returntrueby default.Integer Bases - Base must be between 2 and 36 for
*_as_integerwith:baseoption.List Delimiters - Default delimiter is comma. Use
:delimiteroption for other separators.Chained Conversions - Base encoding and list functions support
:asoption to chain conversions (e.g.,fetch_env_as_base64!("VAR", as: :json)).Decimal Dependency -
*_as_decimalfunctions require thedecimalpackage to be installed.
Function Reference
Delegates to System
delete_env/1- Delete environment variablefetch_env/1- Fetch variable, returns{:ok, value} | :errorfetch_env!/1- Fetch variable, raises if missingget_env/0- Get all environment variables as mapget_env/2- Get variable with defaultput_env/1- Set multiple variables from mapput_env/2- Set single variable
Enviable-Specific
put_env_new/2- Set variable only if not already set
Generic Conversion
get_env_as/3- Get and convert with defaultfetch_env_as/3- Fetch and convert, returns{:ok, value} | :errorfetch_env_as!/3- Fetch and convert, raises on error
Type-Specific Conversions
Each type has three variants:
get_env_as_TYPE/2- With defaultfetch_env_as_TYPE/1- Returns{:ok, value} | :errorfetch_env_as_TYPE!/1- Raises on error
See "Available Conversion Types" table above for all supported types.
Resources
- Hex Package - Package on Hex.pm
- HexDocs - Complete API documentation
- GitHub Repository - Source code and issues
- 12-Factor App - Configuration methodology
Performance Tips
- Minimize conversions - Cache converted values rather than converting repeatedly
- Use specific functions -
fetch_env_as_integer!/1is more efficient thanfetch_env!/1+ manual conversion - Avoid atom creation - Use
*_as_safe_atomand*_as_safe_modulewith:allowedlists - Batch variable setting - Use
put_env/1with a map for multiple variables