Keep your secrets out of your QuickApp — configs, env and --%%var

Real QuickApps talk to real services: weather APIs, MQTT brokers, push notifications. All of them want credentials — and hardcoding the key in the QA file is bad news. The file gets shared on the forum, committed to git, uploaded to the controller. flua has a small config machinery that keeps the secret on your machine and out of the code: a Lua config file, an environment chain, and --%%var directives.

The Lua config file

Drop a .flua.lua next to your QA (or in your home directory, ~/.flua.lua — handy for per-machine settings). It is plain Lua data:

-- .flua.lua (next to the QA, or ~/.flua.lua)
return {
  weatherKey = "sk-live-3f2a9c41",        -- the real key, on this machine only
  endpoints = { base = "https://api.weather.example", retries = 3 },
}

flua loads it once and merges it into _FLUA.config — QA code can read it directly as _FLUA.config.endpoints.retries. The file is Lua, not a static format: you can compute values, pull in another file with dofile, anything.

--%%var turns config values into QuickApp variables

--%%var:name=expr sets a QuickApp variable at startup. The expression is evaluated as Lua with config (your merged config) and os in scope — so it can read the config file or the environment. Any Lua value is legal, including tables:

--%%name:weather
--%%type:com.fibaro.binarySwitch
--%%var:key=config.weatherKey,""
--%%var:base=config.endpoints.base
--%%var:retries=config.endpoints.retries
--%%var:envKey=os.getenv("WEATHER_KEY")
--%%var:opt=config.nothere
--%%var:policy={alerts=true,units="C"}
-- --------------- EOH ---------------
function QuickApp:onInit()
  print("base:", self:getVariable("base"))
  print("retries:", self:getVariable("retries"))
  print("envKey:", self:getVariable("envKey"))
  print("opt unset:", self:getVariable("opt") == "")
  print("policy:", json.encode(self:getVariable("policy")))
  print("key present:", self:getVariable("key") ~= "")
end

Run it and the variables are there — read them like on the HC3 with self:getVariable(name):

[weather5000]: base: https://api.weather.example
[weather5000]: retries: 3
[weather5000]: envKey: the-key-for-this-machine
[weather5000]: opt unset: true
[weather5000]: policy: {"units": "C", "alerts": true}
[weather5000]: key present: true

Note that --%%var:key=... never appears in the QA body — the directive is the whole story, and the QA code just reads the variable. The variable part is the HC3-native mechanism, so the QA logic is identical online and offline.

And that little quoted string after the comma on the key line? It never runs — it is the package value, and it is the subject of the last section. Hang on to it.

The environment chain

os.getenv(name) in a --%%var expression reads through flua's environment chain: a local .env in the directory you run from, then ~/.env, then your shell environment. Files are plain KEY=value lines and are re-read when they change — perfect for API tokens that differ per machine:

# .env — per-machine, never committed
WEATHER_KEY=the-key-for-this-machine

The rules, so you know what to expect

What travels to the HC3

Two destinations, two rules — and both are what you want:

So before you pack for the forum, give every secret variable a package value. That is the quoted string after the comma — --%%var:key=config.weatherKey,"<set to service weather key>" — or the Plua spelling --%%clobber:key=<set to service weather key> on its own line. Pack the weather QA and the package comes out scrubbed:

# flua --tool pack weather.lua   — then look inside weather.fqa
quickAppVariables:
  base     = "https://api.weather.example"
  envKey   = "the-key-for-this-machine"
  key      = "<set to service weather key>"
  policy   = {"alerts": true, "units": "C"}
  retries  = 3

The real key appears nowhere — not in the variables, and not in the packaged main file either: its --%%var header lines are rewritten to the same literals. The placeholder itself never runs: flua weather.lua still gives you the real value on your machine, which is the whole point. And if an expression evaluates to nil (opt = config.nothere), that variable simply stays out of the package.

One last thing: when you downloadQA a QA home, its variables come along as literal --%%var header lines — move those values into .flua.lua (as config.xxx) before you commit or share the project.

Next time: real integrations — HTTP and MQTT clients talking to actual services, with the mock-server trick from post 8 for testing.

Config + env + variables: USAGE.md ("The Lua config file", "--%%var") · flua: https://github.com/jangabrielsson/flua