Configuration management
The PWA buildpack library provides small tools to configure your environment and larger, overall workflows that a developer has to configure and control.
These configurations differ across projects and different environments within those projects. For example, environments for development, testing, staging, and production are configured to support different behaviors.
The .env
file
Like the rest of PWA Studio, buildpack uses environment variables as its central source of configuration settings.
A PWA Studio project using buildpack requires a .env
file in its root directory.
Each line in the file contains a configuration using the following form:
NAME=value
In any script in any programming language, you can access these environment variables directly by sourcing the file as a legal POSIX shell script.
Command Line Interface (CLI)
Buildpack provides a buildpack
CLI for creating .env
files and validating environments.
It also provides library methods for connecting environment management workflows with other tools.
Using these provided tools, you can keep global configuration values in a central location and propagate them throughout your project. This lets you pass common settings down to different library functions without tightly coupling those settings together.
Configuration management rationale
PWA Studio follows the principle that all configuration that can be environment variables, should be environment variables.
Environment variables are portable, cross-platform, and reasonably secure. They can be individually overridden to give the user a great deal of control over a complex system. The twelve-factor app methodology recommends storing config in the environment as its third factor.
Many tools use environment variables strictly as edge-case overrides and store their canonical configuration in other formats because under the strict POSIX definition, environment variables have some limitations:
- An environment variable name is case insensitive.
- An environment variable’s value can only be a string.
- Environment variables cannot be nested nor schematized, so they have no built-in data structures.
- Environment variables all belong to a single namespace, and every running process has access to all of them.
These drawbacks are serious enough that some applications use alternate formats, such as:
XML
JSON
YAML
INI
/TOML
.properties
files in Java.plist
files in MacOS- PHP associative arrays
- Apache directives
These formats have the following advantages over environment variables:
- They are a standard human-readable file format
- They can support nesting and/or namespacing to organize values
- They support data types and metadata
However, none of these formats have won and become an undisputed replacement for environment variables. Each one has its own set of quirks and undefined behaviors. None of them are deeply integrated with OS, shell, and container environments, and they often do not work consistently across language runtimes.
PWA Studio chooses to use environment variables, while providing simple tools for file format, namespacing, and validation.
A centralized configurator passes on formatted pieces of the environment to specific tools as parameters, so
these tools do not need to know the specifics of the configuration scheme.
Entry point scripts, such as server.js
and webpack.config.js
, can use the loadEnvironment()
tool to deserialize environment variables into any kind of data structure, while storing persistent values in an .env
file in the project root directory.
Buildpack combines the features of several tools:
- dotenv for managing environment variables with
.env
files - envalid for describing, validating, and making defaults for settings
- camelspace for easily translating configuration between flat environment variables and namespaced objects
Best practices
The config rule in the twelve-factor app methodology distinguishes configuration that “does not vary between deploys” from configuration that does.
It requires that configuration that does change between deploys be stored in the environment.
PWA Studio does not make such a distinction.
For config that must never vary, the PWA project maintainer can hardcode that configuration in the entrypoint scripts what use loadEnvironment()
.
To have environment-variable-based configuration management and enjoy the benefits of file format, namespacing, and validation at the same time, it’s important to use loadEnvironment()
in a certain way.
Configuration object
The purpose of a function such as loadEnvironment()
is to keep configuration organized without tightly coupling a system to a manager object.
To achieve this, it is important to use loadEnvironment()
and the Configuration
object it produces at the “top level” or entry point of a program.
Avoid passing a Configuration
object directly to other tools.
These tools should be usable without loadEnvironment()
.
It is always the responsibility of an outer function to pass plain configuration to an inner dependency.
Use the Configuration
object only when moving between logic layers:
Bad: passing the Configuration
object to library methods
await PWADevServer.configure({
publicPath: config.output.publicPath,
graphqlPlayground: true,
projectConfig: loadEnvironment(__dirname)
}, config);
The same principle holds when creating your own utilities.
Bad: expecting a Configuration
object in a library function
class MyWebpackPlugin {
constructor(config) {
this.options = config.section('myWebpackPlugin');
}
}
Good: passing plain objects created by the Configuration object
const projectConfig = await loadEnvironment(__dirname);
await PWADevServer.configure({
publicPath: config.output.publicPath,
graphqlPlayground: true,
...projectConfig.sections(
'devServer',
'imageService',
'customOrigin'
),
...projectConfig.section('magento')
}, config);
Naming convention
POSIX standard environment variables may not be case sensitive and may not allow very many special characters.
The best policy is to use ALL_CAPS_UNDERSCORE_DELIMITED_ALPHANUMERIC_VARIABLE_NAMES
when defining environment variables directly.
Buildpack will ignore any environment variables which do not follow this convention.
Buildpack converts between this strict all-caps format (also known as SCREAMING_SNAKE_CASE) and a more convenient JavaScript object which can be nested at any level of delimiter.
When defining new environment variables, make their names long and safely namespace them with prefixes as long as necessary.
Configuration
objects have .section()
and .sections()
methods to create targeted, small JavaScript objects with shorter names.
Fallback
By default, buildpack respects three levels of “fallback” values:
- Currently declared environment variables, which can be populated on process startup
- Values from the
.env
file in the project root - Defaults from the metadata in the Project Environment Definitions
Additional layers of configuration and on-disk fallback are discouraged. Inside scripts, environment variables may be combined and merged, but too much fall-through of project configuration can result in unpredictable and hard-to-maintain runtime configuration.
Project environment definitions
All the environment variables expected and/or used by buildpack are defined in packages/pwa-buildpack/envVarDefinitions.json
.
This file is used for:
- Creating a self-documenting
.env
file - Validating environments
- Deprecating and supporting older settings which have changed
If you are contributing to the PWA Studio project and want to add new functionality that should be configured via the environment or change any environment configuration, follow these best practices:
- Define any new variables in the
packages/pwa-buildpack/envVarDefinitions.json
file. The variable definition object follows the API of envalid, with the addition of atype
property indicating theenvalid
method to use. - Organize variables into named sections in the
sections
list. -
Use the namespacing practices encouraged by camelspace and
loadEnvironment()
.For example, a new utility
goodStuff()
might demand environment variables starting withGOOD_STUFF_
, andpackages/pwa-buildpack/envVarDefinitions.json
might include a new section in itssections
list. -
After making any changes to
packages/pwa-buildpack/envVarDefinitions.json
, record them in thechanges
list in that file.- Change entries are objects which include:
type
: Required. The type of the change, eitherremoved
orrenamed
. No other types of change need change entries.name
: Required. The affected environment variable name.dateChanged
: Required. The date the change entry was added, in a format parseable by the JavaScriptDate
constructor.warnForDays
: Optional, default180
. A number of days that the warning should keep logging on every run, counting from thedateChanged
. The default and maximum is 180 days, so use this property only if you want the change to log for a shorter time than default. This prevents an old, out-of-date warning message from cluttering logs long after the user no longer needs to see it.removed
entries should include a human-readablereason
. After removing a variable definition, leave theremoved
entry permanently to log an error if the old variable is found, encouraging out-of-date installations to upgrade.renamed
entries should include the old name asname
, and the new name asupdated
. They must also include asupportLegacy
boolean. If this istrue
, thenloadEnvironment()
will continue to support the old value while logging a warning, until either the new variable name has a value, or the change entry expires.