Once a load has run, read a variable from Envless.env with a Symbol or a String:
require "envless/env/load"
database_url = Envless.env[:DATABASE_URL]
same_value = Envless.env["DATABASE_URL"]A name that starts with a capital letter also reads as a method, Envless.env.DATABASE_URL, the way @goenvless/env reads it. A method read is resolved at run time, so Steep cannot see it: in typed code, use []. Assigning one, Envless.env.DATABASE_URL = "...", raises Envless::Env::ReadOnlyError like any other assignment.
Coercion
Every read is coerced to the type the variable has in Envless, set in the dashboard or with envless var set --type.
| Type in Envless | Ruby value | Examples |
|---|---|---|
string | String | "postgres://db/app" |
number | Integer when whole, otherwise Float | "3000" reads 3000, "0.25" reads 0.25, "abc" reads Float::NAN |
boolean | true or false | true for exactly "true" or "1", false for anything else, "TRUE" included |
Numbers are parsed the way JavaScript's Number() parses them, so every runtime agrees on every input: surrounding whitespace is ignored, "0x1A" reads 26, "1e3" reads 1000, an empty value reads 0, and anything unparseable reads Float::NAN rather than raising. Envless does not validate values, so if you need a port that is really a port, check it before you use it.
Nothing is memoised behind a read, so a reload or an override is visible at once.
Missing variables
Reading a name the store does not hold raises Envless::Env::MissingVariableError with the message Missing env var: NAME, and variable on the error names it. fetch works like Hash#fetch and ENV.fetch: a default or a block instead of the error.
require "envless/env/load"
port = Envless.env.fetch(:PORT, 3000)
region = Envless.env.fetch(:AWS_REGION) { "eu-west-1" }For a read that is genuinely allowed to come back empty, use optional. It is the same view with one rule relaxed: a missing name reads nil. It still raises Envless::Env::NotLoadedError before the first load, so a misconfigured process fails loudly instead of reading nil everywhere.
require "envless/env/load"
stripe_key = Envless.env[:STRIPE_SECRET]
sentry_dsn = Envless.env.optional[:SENTRY_DSN]Envless.env.key?(:NAME) answers without raising, and Envless.env.keys lists the names, never the values.
The client view
Envless.env.client sees only the variables marked client in Envless, the ones that are safe to send to a browser. Reading a server variable through it raises Envless::Env::ServerOnlyError, and server names are hidden from key? and keys.
require "envless/env/load"
public_url = Envless.env.client[:PUBLIC_API_URL]
public_values = Envless.env.client.snapshotsnapshot returns a Hash of every client variable, coerced, which is what the Rails view helper hands to templates. Use it for a JSON config endpoint your frontend reads, and never build that payload from Envless.env itself. See server vs client for how visibility is set.
Typing
The gem ships RBS signatures. On its own, a read is typed String | Integer | Float | bool, or that with nil through optional, because the type of a variable lives in Envless rather than in your code. Add library "envless-sdk", "envless-env" to your Steepfile and narrow the value where you use it:
require "envless/env/load"
port = Envless.env[:PORT]
raise "PORT must be a whole number" unless port.is_a?(Integer)
puts port + 1Or let Envless write the types: bundle exec envless-env types generates sig/envless-env.rbs from your variables, and every read is then typed by name, Envless.env.PORT as Integer | Float and Envless.env.client.PUBLIC_URL as String, with a misspelt name or a server variable read on the client caught by Steep.
Overrides in tests
Envless.env is read-only. Assigning a name raises Envless::Env::ReadOnlyError. Envless::Env.override layers temporary values on top of whatever was loaded, and the innermost layer wins until its block returns. It works before a load too, which is often all a unit test needs.
require "envless/env"
Envless::Env.override(API_URL: "https://api.example.test") do
puts Envless.env[:API_URL]
endAn override always carries Strings, and coercion still follows the type of the loaded variable, so a name the loader never saw reads back as a String. Without a block, override returns a handle whose restore removes the layer, for a setup and teardown pair.
Secrets stay inside Envless.env
Envless.env holds decrypted secrets, so it refuses the operations that would copy every one of them somewhere else.
| Operation | Result |
|---|---|
inspect, to_s, string interpolation, a log line | #<Envless::Env::Values 12 variables>, never a value |
to_h, snapshot, to_json, as_json | Envless::Env::SerialisationError |
Marshal.dump, YAML.dump | Envless::Env::SerialisationError, on every view |
dup, clone | Envless.env itself |
The client view is the exception: its to_h, snapshot, to_json and as_json work, so render json: Envless.env.client does what it says, because everything in it is meant for a browser. This stops the accidental copy, an error tracker calling inspect, a debug page dumping an object, a serialiser walking it. It is not a containment boundary: reading every name you know still copies the values out. When you genuinely need every value at once, use the Hash that Envless::Env.load returns, a copy you own.