Why?

FarmBot is an Open Source project that can be self hosted by third party developers. The way that FarmBot, inc. configures its servers for public use might not meet the needs of self hosting users. Additionally, there are security implications to configuration management in an Open Source project; We would not want to leak our database password into source control, for example.

Farmbot attempts to decouple configuration from code as much as possible, in keeping with the 12 Factor Methodology:

The twelve-factor app stores config in environment variables (often shortened to env vars or env). Env vars are easy to change between deploys without changing any code; unlike config files, there is little chance of them being checked into the code repo accidentally; and unlike custom config files, or other config mechanisms such as Java System Properties, they are a language- and OS-agnostic standard.

– 12 Factor Methodology Homepage

By using ENV vars as the first choice for server configuration, we can:

How?

For most parts of the app, environment variables are loaded into a .env file.

  • Information about the .env file format can be found here
  • Documentation on legal values can be found here.

Important Note

The app also exposes a secondary configuration system called GlobalConfig. This secondary system addresses use cases where ENV variables are not appropriate. The system is discussed later in this document.

Real World Examples

FarmBot, Inc. requires validation of user email addresses. Most self hosting users do not want this behavior, however. To allow server admins to choose between verifying emails or not, the application exposes a ENV["NO_EMAILS"] variable. When present, this ENV var will disable email verification.

Another example is the EXTRA_DOMAINS variable. It can be set to a comma separated list of alternative domain names that a server may control. This allows the same server to be hosted on more than one domain name (my.farm.bot, my.farmbot.io, etc..).

These examples are for illustrative purposes. The full list of variables is available here.

Problems

ENV vars work for most configuration use cases, but there are some caveats:

  • Changing ENV vars requires a server restart. In many cases, this creates an unacceptable downtime period, particularly for servers with high uptime requirements.
  • Environment variables are a server-side concern. When a client loads the FarmBot Web App in their browser, it is not possible to see the server’s ENV vars. If this were not the case, it would be possible for users to steal database credentials quite easily.
  • Not all customization happens on the server side (“back end”). There are many use cases that require ENV passing information from the server to the client.

Solutions

Solution: Runtime Reloading of ENV Vars

As stated previously, ENV variables require a process restart to take effect. For some values, a restart is not practical.

For values that must change after the server has started, the FarmBot Web App uses a model (database table) known as GlobalConfig. The GlobalConfig source code can be found here.

Some GlobalConfig values (such as "TOS_URL") are bootstraped using ENV vars if no value can be found in the global_configs table.

A GlobalConfigs value can be changed at runtime from the Rails console:

GlobalConfig.create!(key: "FAVORITE_VEGGIE", value: "Carrots")

WARNING

Unlike traditional ENV vars, GlobalConfig values are not hidden from the public. Do not store sensitive data in GlobalConfig!

Solution: Sharing ENV Vars with Clients

NOTE

This section requires knowledge from the previous section.

As stated in the “Problems” section, exposing all ENV vars to a browser would be an extreme security risk. We want to expose some ENV vars to clients, but not all of them.

To get around this problem the Web App’s UI exposes a global variable to the browser: window.globalConfig. This variable contains all GlobalConfig items, but not necessarily all ENV vars. For this reason it is important that you do not store sensitive data in the GlobalConfig table.

We can inspect the value of globalConfig in our browser:

console.log(window.globalConfig);

On production, globalConfig contains the following values:

globalConfig:

{
  // Current application environment:
  "NODE_ENV":"production",

  // Terms of Service URL:
  "TOS_URL":"https://farm.bot/tos/",

  // Privacy Policy URL:
  "PRIV_URL":"https://farm.bot/privacy/",

  // Lowest FarmBot OS versions the server supports:
  "MINIMUM_FBOS_VERSION":"7.0.0",
  "FBOS_END_OF_LIFE_VERSION":"7.0.1",

  // Web App version the server runs:
  "LONG_REVISION":"191b0ead3dfe1cc43ee416a5ff27a064af556192",
  "SHORT_REVISION":"191b0ead"
}

As stated, the server’s environment variables are not exposed to the public. We also mentioned that we had a second configuration system that complements traditional server ENV vars- the GlobalConfig database table.

GlobalConfig values are transferred from the server to the client using server-side templating.

When the Web App loads a web page that needs ENV vars, it injects a a <script> tag that declares the globalConfig variable.

app/views/dashboard/_common_assets.html.erb:

app/views/dashboard/_common_assets.html.erb:

<script>
  // ---SNIP!---

  // THIS IS WHERE ENV VARIABLES CROSS OVER
  // From the server to the client:

  window.globalConfig = <%= raw(@global_config) %>

  // The @global_config contains all database entries
  // for the `GlobalConfig` table (but not all ENV
  // vars).

  // ---SNIP!---

</script>

Please note that the <%= %> is Ruby ERB syntax

The value of @global_config is set in app/controllers/dashboard_controller.rb:

app/controllers/dashboard_controller.rb:

class DashboardController < ApplicationController
  before_action :set_global_config
  layout "dashboard"

# --- SNIP! ---

private

  def set_global_config
    # THIS IS WHERE @global_config is created:
    @global_config = GlobalConfig.dump.to_json
  end
end

With these systems in place, we can now add and remove ENV variables from the Rails console via:

GlobalConfig.create!(key: "FAVORITE_PLANT", value: "cabbage")
=> #<GlobalConfig:0x0000 key: "FAVORITE_PLANT", value: "cabbage">

To view the variable from the browser, we can simply type:

globalConfig.FAVORITE_PLANT;
// => "cabbage"