Functional, centralized configuration (?)

I’m working on a team which uses Node.js primarily, but I’ve created a Phoenix app to introduce Elixir as an experiment. In our Node.js apps there is a central config.js file which can be shared, which is actually pretty great because all of the environment variables are loaded there and it’s super easy to figure out what’s required to run the app.

I would like to be able to do something in Elixir / Phoenix. Currently, just to get things running, I added some System.get_env("SOME_VAR") calls in my modules.

Obviously there are config/config.exs and config/<env>exs files, but those are loaded at compile-time. I know some libraries support configuration like config :my_app, :the_library, {:env, "SOME_VAR"} or whatever. Maybe there’s a generic library for me to deal with my own application configuration that way?

But another big thing, and maybe this is just me being picky, but it would be nice if this were all more functional (as I understand it). If I could get the configuration inside of the application.ex file and then pass it down to the child processes, that would be more traceable for people reading the code, I think. For Phoenix I guess that would mean putting something into the MyApp.Endpoint child spec. I played around with defining my own init inside of the MyApp.EndPoint module, which is cool (using vapor to define the configuration). But then when a request comes in to a controller, I guess I would pass in the needed configuration vars into my function calls for my business-logic modules. But looking at the conn it doesn’t seem to have the custom configuration.

Is there a different way? Is there something I don’t know? Or maybe I’m overthinking it :grin:

Since 1.11 there is runtime.exs, where you can do all the System.get_env("SOME_VAR") calls. It is executed right before applications start for both Mix and releases.

Oh, well, awesome. I’ll try that then, thanks :sweat_smile:

That is what I am using in most cases. Of course that has disadvantages that it cannot configure everything (it cannot configure startup-time configuration of already running applications obviously).

You can use “raw” sys.config that you either write by hand (it uses Erlang syntax) or you can generate it before starting your application. I am still working on an article of using BEAM with systemd, where such thing can be easily done via using proper unit configuration options.

I thought I would share what I came up with for anybody coming along later (it’s been anonymized):

# config/runtime.ex

defmodule MyApp.Config do
  use Vapor.Planner

  config MyApp.Module.API,
           {:url_base, "SERVICE_URL_BASE"}

  config MyApp.OtherModule.Source.Name,
           {:api_key, "OTHER_SERVICE_API_KEY"}

import Config

config :sentry,
  dsn: System.get_env("SENTRY_DSN"),
  release: "my-app@1.0.0",
  environment_name: config_env(),
  included_environments: [:prod],
  enable_source_code_context: false,
  root_source_code_paths: [File.cwd!()]

if config_env() != :test do
  config = Vapor.load!(MyApp.Config)
  config :my_app, MyApp.Module.API, config[MyApp.Module.API]

  config :my_app,

And then as an example of how it’s used in the module:

defmodule MyApp.OtherModule.Source.Name do
  # ... code ...

  defp api_key do

  defp config do
    Application.get_env(:my_app, __MODULE__)

I used to do something like this, ie use nested map to sub divide the config space. It feels cleaner. However, I now try to make a config space as flat as possible with simple key/values. The reasons are:

  • OTP’s config is stored in ETS, it is fast to read and scalable to many keys. If you load a map, then access one value from the map then throw away the rest it will be slower.
  • if you ever want to mutate config within your application, the nested map will get in your way because if you read a map, update a key, put it back, you are creating WAW hazards among the setters.
1 Like