Browse the docs · Apsi 2.4.4
Configuration, secrets and virtual environments
.env files (use env)
use env
env.load(".env") # KEY=value lines
let db_url = env.get("DATABASE_URL") # the .env file first, then the real environment
let key = env.secret("OPENAI_API_KEY") # prints as ******; key.reveal() gives the text
let port = env.int("PORT", 8080)
let debug = env.bool("DEBUG", false) # true/false, 1/0, yes/no, on/off
let hosts = env.list("ALLOWED_HOSTS") # comma-separated
env.require("DATABASE_URL", "SECRET_KEY") # one clear error listing everything that is missing
The file format is the one python-dotenv and Docker Compose read: KEY=value, export KEY=value, single and double
quotes, multi-line double-quoted values, \n escapes in double quotes, # comments, and ${OTHER} expansion.
apsi run app.apsi --env-file .env loads the file before the program starts.
Permissions: values from a .env file need only --allow-read for that file. Reading the process environment
needs --allow-env (or --allow-env=PORT,DEBUG for named variables). A default (env.int("PORT", 8080)) is used
when the permission is missing, so a program still starts.
Secrets stay secret: a Secret prints and logs as ******, is left out of error messages, and compares in
constant time. Pass it to db.postgres(...), web clients or crypto functions directly; call .reveal() only
where the text itself is needed.
Python virtual environments
apsi run app.apsi --venv .venv # pyimport() uses the packages installed in that venv
apsi run app.apsi # a .venv or venv folder next to the program is found automatically
The venv's site-packages are put first on the import path for pyimport/pyfrom and converted programs. The
venv's Python version must match the one Apsi runs on (Apsi says so when it does not).
Reference (use env)
| Function | Returns | Behaviour |
|---|---|---|
env.load(path=".env", override=true, export=false) |
names defined | Reads a .env file. A missing file is an error. With override=false, names already loaded keep their value. With export=true, the values are also put into the process environment for child processes. Needs --allow-read for the file. |
env.get(name, default=none) |
Str or default |
The loaded .env value first, then the process environment. Without a permission to read the environment, returns default when one is given and raises a permission error otherwise. |
env.has(name) |
Bool |
Whether the name is set in the loaded files or the environment. |
env.require(name, …) |
the values | Raises one error listing every missing or empty name, so a deployment shows all problems at once. |
env.secret(name, default=none) |
Secret or none |
The value wrapped so it never prints. .reveal() returns the text. |
env.int(name, default=none), env.float(…) |
number | Converts the value. An empty or missing value gives default. A value that is not a number is an error (never silently 0). |
env.bool(name, default=false) |
Bool |
1, true, yes, on, y (any case) are true; any other set value is false. |
env.list(name, sep=",", default=none) |
List[Str] |
Splits and trims; empty items are dropped. |
env.parse(text) |
Dict |
Parses .env text without touching any state — useful in tests. |
env.values |
Dict |
Everything loaded from files so far. |
File format in detail
# comments start with #
PORT=8080
export DEBUG=true # "export " is allowed and ignored
NAME='single quotes: taken literally, no ${EXPANSION}'
GREETING="double quotes: \n becomes a line break"
KEY="-----BEGIN KEY-----
spans several lines
-----END KEY-----"
DATABASE_URL=postgres://${DB_USER}:${DB_PASS}@db/app # ${NAME} from this file or the environment
EMPTY=
TRAILING=value # an unquoted value ends before " #"
A byte-order mark at the start of the file (common with Windows editors) is ignored.
Secrets in practice
use env
use db
env.load()
let pg = db.postgres(host=env.get("DB_HOST"), user="app", password=env.secret("DB_PASSWORD"))
print(env.secret("DB_PASSWORD")) # ******
log(f"connecting with {env.secret("DB_PASSWORD")}") # connecting with ******
- Keep
.envout of version control (add it to.gitignore) and commit a.env.examplewith the names and harmless values instead. - On servers, prefer real environment variables or your platform's secret store, and run with
--allow-env=NAME1,NAME2so the program can read only what it needs. Secretvalues compare in constant time, so checking an API key with==does not leak it through timing.
Found a mistake? Report it · Markdown source