Env

Luma provides built-in access to environment variables through the env namespace — no imports needed.

Environment variables are the standard way to configure applications across every deployment platform: local development, Docker, CI/CD, cloud services. The env module covers the two things every application needs:

  • Read and write system environment variables
  • Load from .env files for local development

Reading Environment Variables

env.get returns the value of an environment variable as a string. If the variable is not set, it returns an empty string.

host: str = env.get("DATABASE_HOST")
port: str = env.get("PORT")
secret: str = env.get("JWT_SECRET")

Default Values

Pass a second argument to provide a fallback when the variable is not set:

port: str = env.get("PORT", "8080")
host: str = env.get("HOST", "localhost")
mode: str = env.get("APP_MODE", "development")

The default is only used when the variable is not set at all. An empty value (PORT=) is still a value — the default won’t apply.

Checking Existence

env.has returns true if the variable is set, false otherwise:

if env.has("API_KEY") {
    key: str = env.get("API_KEY")
    // use the key
}

This distinguishes between “not set” and “set to empty string”, which env.get alone cannot do.

Setting Environment Variables

env.set sets an environment variable for the current process:

env.set("APP_MODE", "testing")
env.set("LOG_LEVEL", "debug")

This affects the current process and any child processes it spawns. It does not modify your shell or persist after the program exits.

Loading from .env Files

env.load reads a file and sets environment variables from its contents:

env.load(".env")

If the file does not exist, env.load does nothing — no error, no panic, just a silent skip. This is intentional: your code works the same in development (where .env exists) and production (where it doesn’t). One line, zero if checks, deploy everywhere.

// In development: .env exists, variables are loaded
// In production:  .env is missing, system env vars are used
// Same code. No changes needed.
env.load(".env")

This is the standard dotenv pattern used across every major language ecosystem. Place a .env file in your project root with configuration values, and load it at the start of your program.

File Format

# Database configuration
DATABASE_HOST=localhost
DATABASE_PORT=3306
DATABASE_NAME=myapp

# API keys
JWT_SECRET=super-secret-key-change-in-production
API_KEY=dev-key-12345

# Quoted values (for spaces and special characters)
APP_NAME="My Cool App"
GREETING='Hello World'

# Empty value
EMPTY_VAL=

The format rules:

Syntax Meaning
KEY=value Set a variable
KEY="value" Quoted value — preserves spaces
KEY='value' Single-quoted value — preserves spaces
KEY= Set to empty string
# comment Comment — ignored
Empty lines Ignored
export KEY=value export prefix is stripped silently (shell compatibility)

System Variables Take Precedence

env.load does not override variables that are already set in the system environment. This is the correct behavior for production:

  • In development, the .env file provides defaults
  • In production, the deployment platform sets real values through the OS
  • The .env file never silently overrides a production secret
// If DATABASE_HOST is already set in the system environment,
// the value from .env is ignored for that variable.
env.load(".env")
host: str = env.get("DATABASE_HOST")  // system value wins

Multiple Files

You can load multiple files. Later files fill in gaps but never override earlier values or system variables:

env.load(".env")           // base defaults
env.load(".env.local")     // local developer overrides

A common project layout:

.env              # shared defaults, committed to git
.env.local        # personal overrides, in .gitignore
.env.production   # production values, managed by deployment

Getting All Variables

env.all returns every environment variable as a map:

all: {str: str} = env.all()

This includes both system variables and any loaded from .env files.

Practical Patterns

Application configuration

env.load(".env")

mysql = import "mysql"
jwt = import "jwt"

conn = db.open(mysql, env.get("DATABASE_DSN"))
secret: str = env.get("JWT_SECRET")
port: str = env.get("PORT", "8080")

print("Starting on port ${port}")

Feature flags

if env.get("ENABLE_DEBUG") == "true" {
    print("Debug mode enabled")
}

Conditional behavior by environment

env.load(".env")

mode: str = env.get("APP_MODE", "development")

if mode == "production" {
    print("Running in production")
}
if mode == "development" {
    print("Running in development")
}

Quick Reference

Operation Code
Get a variable val = env.get("KEY")
Get with default val = env.get("KEY", "fallback")
Check if set env.has("KEY")
Set a variable env.set("KEY", "value")
Load .env file env.load(".env") (skips silently if missing)
Get all variables all = env.all()

Method Summary

Method Arguments Returns Description
env.get() key: str str Get variable value (empty string if not set)
env.get() key: str, default: str str Get variable value with fallback
env.has() key: str bool Check if variable is set
env.set() key: str, value: str Set variable for current process
env.load() path: str Load variables from a dotenv file (silent skip if missing)
env.all() {str: str} Get all variables as a map

Design Notes

  • env is a built-in namespace — no import needed, like json, crypto, and db
  • env.get returns an empty string for missing variables, not a panic — this matches the convention in Go, PHP, and most other languages
  • env.load silently skips missing files — the same code works in development (.env present) and production (.env absent) without any conditional logic
  • env.load never overrides existing system variables — production environment always wins over .env files
  • env.set only affects the current process — it doesn’t modify your shell or write to files
  • The .env file format follows the widely-adopted dotenv convention, compatible with Docker, Heroku, Vercel, and most deployment platforms
Last updated on