Skip to content

Configuring an Application and its Environment

This guide covers configuring a Conduit application.

Configuration Files

Conduit applications use YAML configuration files to provide environment-specific values like database connection information. Use separate configuration files for testing and different deployment environments.

The path of a configuration file is available to an ApplicationChannel through its options property.

class TodoAppChannel extends ApplicationChannel {
  @override
  Future prepare() async {
    var config = TodoConfiguration(options.configurationFilePath);
    ...
  }
}

The default value is config.yaml.

The best practice for reading a configuration file is to subclass Configuration. A Configuration declares a property for each key in a configuration file. For example, see the following Configuration subclass:

class TodoConfiguration extends Configuration {
  TodoConfiguration(String fileName) : super.fromFile(File(fileName));

  DatabaseConnectionConfiguration database;
  String apiBaseURL;

  @optionalConfiguration
  int identifier;
}

This would read a YAML file like this:

database:
  username: fred
  password: fredspassword
  host: db.myapp.com
  port: 5432
  databaseName: fredsdb
apiBaseURL: /api
identifier: 2

If required properties are omitted from the YAML file being read, application startup will fail and throw an informative error.

Environment Variables

A configuration file can use an environment variable instead of a literal value. In config.yaml, use a $-prefixed environment variable name instead of a value:

database: $DATABASE_CONNECTION_URL
apiBaseURL: /api

If the environment variable DATABASE_CONNECTION_URL's value were "postgres://user:password@localhost:5432/test", the value of TodoConfiguration.database will be that string at runtime.

The safe_config package has instructions for more additional usages.

Configuration Conventions and Deployment Options

Conduit uses two configuration files for a project: config.yaml and config.src.yaml. The latter is the configuration source file. The configuration source file declares key-value pairs that will be used when running the application tests. Deployed instances use config.yaml.

This pattern is used for two reasons:

  • It is the template for the config.yaml that will be read on deployed applications, providing documentation for your application's configuration.
  • It has the configuration values used during testing to inject mock dependencies.

For example, a production API instance might have the following config.yaml file with connection info for a production database:

database: postgres://app_user:$%4jlkn#an*@mOZkea2@somedns.name.com:5432/production_db

Whereas config.src.yaml would have connection info for a local, test database:

database: postgres://test:test@localhost:5432/temporary_db

The source configuration file should be checked into version control. Whether or not config.yaml is checked in depends on how you are deploying your code. If you are using environment variables to control application configuration, you should check config.yaml into source control and provide $-prefixed environment variable values. If you are using managing configuration files on each deployed instance, do not check config.yaml into source control because it'll be a different file for each instance.

It can sometimes makes sense to have a local.yaml with values for running the application locally, e.g. when doing client testing. Use --config-path with conduit serve to use a non-default name.

Preventing Resource Leaks

When a Conduit application starts, the application and its ApplicationChannels will likely create services that they use to respond to requests. In order for application tests to complete successfully, these services must be "closed" when the application stops. For built-in services, like PostgreSQLPersistentStore, this happens automatically when Application.stop() is invoked.

A ServiceRegistry automatically stops registered services. Registration looks like this:

var connection = new ConnectionOfSomeKind();
await connection.open();
ServiceRegistry.defaultInstance
  .register<ConnectionOfSomeKind>(connection, (c) => c.close());

This method takes the service to be closed and a closure that closes it. The service is passed as an argument to the closure. If the closure returns a Future, ServiceRegistry.close will not complete until the Future completes. ServiceRegistry.defaultInstance is closed in Application.stop(), any registries created by the programmer must be closed manually.

The return type of ServiceRegistry.register is the object being registered. This makes registration syntax a bit more palatable:

var connection = ServiceRegistry.defaultInstance
  .register<ConnectionOfSomeKind>(
    new ConnectionOfSomeKind(), (c) => c.close());

await connection.open();

Configuring CORS Headers

All controllers have built-in behavior for handling CORS requests from a browser. When a preflight request is received from a browser (an OPTIONS request with Access-Control-Request-Method header and Origin headers), the response is created by evaluating the policy of the Controller that will respond to the real request.

In practice, this means that the policy of the last controller in a channel is used. For example, the policy of FooController generates the preflight response:

router
  .route("/foo")
  .link(() => new Authorizer(...))
  .link(() => new FooController());

Every Controller has a policy property (a CORSPolicy instance). The policy has properties for configuring CORS options for that particular endpoint. By having a policy, every Controller automatically implements logic to respond to preflight requests without any additional code.

Policies can be set at the controller level or at the application level. The static property CORSPolicy.defaultPolicy can be modified at initialization time to set the CORS options for every controller.

class MyApplicationChannel extends ApplicationChannel {
  @override
  Future prepare() async {
    CORSPolicy.defaultPolicy.allowedOrigins = ["http://mywebsite.com/"];
  }
}

The default policy is very permissive: POST, PUT, DELETE and GET are allowed methods. All origins are valid (*).

Each individual controller can override or replace the default policy by modifying its own policy in its constructor.

class MyResourceController extends ResourceController {
  MyResourceController() {
    policy.allowedMethods = ["POST"];
  }
}

Configuring HTTPS

By default, a Conduit application does not use HTTPS. In many cases, a Conduit application sits behind an SSL-enabled load balancer or some other proxy. The traffic from the load balancer is sent to the Conduit application unencrypted over HTTP.

However, Conduit may be configured to manage HTTPS connections itself. By passing the value private key and SSL certificate paths as options to --ssl-key-path and --ssl-certificate-path in conduit serve, a Conduit application will configure itself to only allow HTTPS connections.

conduit serve --ssl-key-path server.key.pem --ssl-certificate-path server.cert.pem

Both the key and certificate file must be unencrypted PEM files, and both must be provided to this command. These files are typically issued by a "Certificate Authority", such as letsencrypt.org.

When an application is started with these options, the certificateFilePath and keyFilePath are set on the ApplicationOptions your application is being run with. (If you are not using conduit serve, you can set these values directly when instantiating ApplicationOptions.)

For more granular control over setting up an HTTPS server, you may override securityContext in ApplicationChannel. By default, this property will create a SecurityContext from the certificateFilePath and keyFilePath in the channels's options. A SecurityContext allows for password-encrypted credential files, configuring client certificates and other less used HTTPS schemes.

class MyApplicationChannel extends ApplicationChannel {
  @override
  SecurityContext get securityContext {
    return new SecurityContext()
      ..usePrivateKey("server.key", password: "1234")
      ..useCertificateChain("server.crt", password: "1234");
  }
}