Configuration
Brut strives to avoid configuration and flexibility. Much of Brut's behavior is convention-based, however several aspects of Brut's behavior relate to literal values like file paths. Some of this set up is designed to be overridden.
Overview
Any configured value, or value otherwise intended to be abstracted from its literal value, is available via Brut.container
, which returns an instance of Brut::Framework::Container
. This class uses method_missing
to provide direct access to configured values. It can also be used to store or override configured values.
An instance of Brut::Framework::Config
is used to set up all of Brut's initial values. This file is a good reference for determining the literal values of various configuration options.
Basics of Configuration
The configuration system can be used to set literal values, or lazily-derived values. The method store
is used for both purposes. Lazy values are managed by calling store
with a block.
Brut.container.store(
"database_url",
String,
"URL to the primary database"
ENV.fetch("DATABASE_URL")
)
Brut.container.store(
"database_url",
String,
"URL to the primary database"
) do
# Only evaluated when Brut.container.database_url is called
ENV.fetch("DATABASE_URL")
end
The configuration system also enables dependencies between values. For example, the Sequel connection to the database requires the URL to the database. Since database_url
is configured seperately, sequel_db_handle
depends on its value. It declares this by declaring a block parameter with the same name as the configuration option, database_url
in this case.
Brut.container.store(
"sequel_db_handle",
Object,
"Handle to the database",
) do |database_url|
Sequel.connect(database_url)
end
When Brut.container.sequel_db_handle
is called, the block is examined. Brut sees that it has a parameter named "database_url"
, and will then call Brut.container.database_url
to fetch the value and pass it in.
This reduces repetition amongst the various configuration options.
Special Types of Configuration
Brut's configuration system enforces some rules, and may enforce more in the future. It's a strong desire that no Brut app even boot if the configuration is not usable.
Many of Brut's configuration options are paths. To avoid having to have a bunch of .keep
files all over the plces, Brut allows storing an ensured path, which Brut will created when needed.
Brut.container.store_ensured_path(
"images_src_dir",
"Path to where images are stored"
) do |front_end_src_dir|
front_end_src_dir / "images"
end
If your Brut app has no images at all, when Brut.container.images_src_dir
is accessed, the path to where the images should go will be created.
Some paths must exist in advance. For those, store_required_path
can be used. It throws an error if the path does not exist:
Brut.container.store_required_path(
"pages_src_dir",
"Path to where page classes and templates are stored"
) do |front_end_src_dir|
front_end_src_dir / "pages"
end
Type and Name Enforcement
You'll note that store
accepts a class parameter. This is mostly used for documentation, with two exceptions:
- If the type is
Pathname
(which is what is used bystore_ensured_path
andstore_required_path
), the configuration parameter name must end in_file
or_dir
. - If the type is
"boolean"
or:boolean
, the configuration parameter name must end in a question mark. In this case, the value itself is coerced intotrue
orfalse
.
Brut may add more constraints or conversions over time.
Overridable and nil
able Values
By default, Brut configuration values cannot be overridden and they cannot be nil
. When calling store
, allow_app_override: true
and allow_nil: true
, can be passed to change this behavior.
In Flash and Session, we discussed that you can set your own class for the flash. This is possible due to how Brut defines the configuration parameter flash_class
:
Brut.container.store(
"flash_class",
Class,
"Class to use to represent the Flash",
Brut::FrontEnd::Flash,
allow_app_override: true,
)
Since allow_app_override
is true, you can call override
in your App
:
Brut.container.override("flash_class",AppFlash)
Calling override
for parameters where allow_app_override
is not true results in an error. Further, calling store
on a previously store
-d parameter results in an error.
The idea is to make it extremely clear what values are being set and overridden, and to avoid setting values that don't exist or aren't relevant.
Some values can be nil
. Generally, nil
is a pain and will cause you great hardship. On occasion, it's needed. For example, external IDs only work if the app provides an app-wide prefix.
Here is how Brut sets this up by default:
Brut.container.store(
"external_id_prefix",
String,
"String to use as a prefix for external ids in tables using the external_id feature. Nil means the feature is disabled",
nil,
allow_app_override: true,
allow_nil: true,
)
Brut defaults this to nil
, meaning there is no external ID prefix to be used. Apps can opt into this behavior by overriding the value:
Brut.container.override("external_id_prefix","cc")
Conversly, apps can opt out of Content Security Policy Reporting. By default, Brut sets up its own reporting hook:
Brut.container.store(
"csp_reporting_class",
Class,
"Route Hook to use for setting the Content-Security-Policy-Report-Only header",
Brut::FrontEnd::RouteHooks::CSPNoInlineStylesOrScripts::ReportOnly,
allow_app_override: true,
allow_nil: true,
)
If you don't want this, you can override it and set it to nil
:
Brut.container.override("csp_reporting_class",nil)
allow_nil
is a good indicator that Brut can handle a nil
value for that configuration parameter—it wouldn't allow it if couldn't.
Setting Your Own Configuration Values
In your App
class, you can call store
as needed to setup configuration values relevant to your app. For example, if you are using SendGrid to send emails, you may want to set the client up during startup:
# app/src/app.rb
class App < Brut::Framework::App
# ...
def initialize
# ...
Brut.container.store(
"send_grid_client",
SendGrid::API,
"SendGrid API Client",
SendGrid::API.new(api_key: ENV["SENDGRID_API_KEY"])
)
# ...
end
end
Then, elsewhere in your app, you can call Brut.container.send_grid_client
to access this object. Because allow_nil
is not specified (and therefore false), your app can confidently rely on a value being returned.
Testing
Do not test configuration. Ideally, the configuration values for production are the same as for dev and test. If you really cannot use the production value for something in dev or test, you can have your value depend on project_env
, which is a Brut::Framework::ProjectEnvironment
:
Brut.container.store(
"send_grid_client",
SendGrid::API,
"SendGrid API Client") do |project_env|
if project_env.testing?
FakeSendGrid.new
else
SendGrid::API.new(api_key: ENV["SENDGRID_API_KEY"])
end
end
end
Recommended Practices
Try to avoid project environment-specific behavior. The less of that you have, the more confidence you will have that your app will boot in production.
In genreal, configure custom parameters only when you want them to be a check on app startup. For example, we don't want our app to even start if SENDGRID_API_KEY
is not in the environment. If we defered instantiating send_grid_client
until the first time we tried to send email, we wouldn't notice things were broken.
Conversely, do not place every object you could ever imagine into the container. Brut::Framework::Container
is not intended as a fully armed and operational dependency injection container. Perhaps someday, but not today.
Lastly, do not store
or override
outside of your App
class' initializer. It will be confusing and override
may not work as expected.
Technical Notes
IMPORTANT
Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's internals, the source code is always more correct.
Last Updated May 7, 2025
Brut::Framework::Config
is created and executed in the initializer of Brut::Framework::MCP
, which happens before your App
's initializer is run. This also happens when CLI apps run. Brut tries very hard not to make network connections inside Brut::Framework::Config#configure!
for just this reason.
Brut::Framework::Container
does not really check for circular dependeices, so please try to avoid making them. If this happens, there will be a stack overflow.