Configuration

Firefly provides typed configuration from .env files and environment variables via Env.load.

.env Files

Create a .env file in your project root:

# .env
DATABASE_URL=postgres://localhost:5432/myapp
PORT=3000
JWT_SECRET=my-secret-key
DEBUG=true
API_BASE_URL=https://api.example.com

Supported formats:

# Comments
KEY=value
KEY="quoted value"
KEY='single quoted'
EMPTY=

Typed Configuration

Define an F# record and load it with Env.load:

type AppConfig = {
    DatabaseUrl: string
    Port: int
    JwtSecret: string
    Debug: bool
    ApiBaseUrl: System.Uri
}
 
let config = Env.load<AppConfig>()
// config.DatabaseUrl = "postgres://localhost:5432/myapp"
// config.Port = 3000
// config.Debug = true

Naming Convention

Record field names are converted to SCREAMING_SNAKE_CASE:

Field NameEnvironment Variable
DatabaseUrlDATABASE_URL
JwtSecretJWT_SECRET
ApiBaseUrlAPI_BASE_URL
PortPORT

Supported Types

F# TypeAccepted Values
stringAny string
intInteger (e.g., 3000)
floatNumber (e.g., 3.14)
booltrue, 1, yes / false, 0, no
System.UriValid URI string
System.TimeSpanTimeSpan string (e.g., 00:30:00)

Optional Fields

Use option types for fields that may not be present:

type AppConfig = {
    DatabaseUrl: string       // required — throws if missing
    RedisUrl: string option   // optional — None if missing
    Port: int                 // required
    Debug: bool option        // optional
}

Missing required fields produce a clear error listing all missing variable names:

Missing required environment variables: DATABASE_URL, PORT

Environment Priority

Env.load layers values from three sources, highest priority first:

  1. Real environment variables — always win; .env files only fill in what is not already set in the process environment.
  2. .env.{environment} — a profile file selected by ASPNETCORE_ENVIRONMENT (or DOTNET_ENVIRONMENT), e.g. .env.Development or .env.Production.
  3. .env — the shared base file.
# .env  (shared defaults)
PORT=8080
LOG_LEVEL=info
 
# .env.Development  (overrides the base in dev)
PORT=5005
LOG_LEVEL=debug

With ASPNETCORE_ENVIRONMENT=Development, Env.load resolves PORT=5005; in any other environment it falls back to the base 8080. A real PORT env var still overrides both. Commit .env and .env.{environment} profiles, but keep secrets in real environment variables.

Usage with DI

Register your config as a singleton service:

type AppConfig = { DatabaseUrl: string; Port: int }
 
let appConfig = Env.load<AppConfig>()
 
let config =
    App.defaults
    |> App.port appConfig.Port
    |> App.services [
        Service.instance appConfig
    ]

Then access it in handlers via auto-DI (if it is an interface) or manually:

let handler (req: Request) = task {
    let config = req.Raw.RequestServices.GetRequiredService<AppConfig>()
    return Response.json {| db = config.DatabaseUrl |}
}

Per-Environment Configuration

Use the CLI template pattern with separate config modules:

// Config/Dev.fs
module MyApp.Config.Dev
 
let config =
    App.defaults
    |> App.port 3000
    |> App.middleware Cors.allowAll
 
// Config/Prod.fs
module MyApp.Config.Prod
 
let config =
    App.defaults
    |> App.port 8080
    |> App.middleware SecureHeaders.middleware
    |> App.middleware Compress.auto