Back to Encore

Configuration

docs/go/develop/config.md

1.56.912.2 KB
Original Source

Configuration files let you define default behavior for your application, and override it for specific environments. This allows you to make changes without affecting deployments in other environments.

Encore supports configuration files written in CUE, which is a superset of JSON. It adds the following:

  • C-style comments
  • Quotes may be omitted from field names without special characters
  • Commas at the end of fields are optional
  • A comma after last element in list is allowed
  • The outer curly braces on the file are optional
  • Expressions such as interpolation, comprehensions and conditionals are supported.
<Callout type="important">

For sensitive data use Encore's secrets management functionality instead of configuration.

</Callout>

Using Config

Inside your service, you can call config.Load[*SomeConfigType]() to load the config. This must be done at the package level, and not inside a function. See more in the package documentation.

Here's an example implementation:

go
package mysvc

import (
    "encore.dev/config"
)

type SomeConfigType struct {
    ReadOnly config.Bool    // Put the system into read-only mode
    Example  config.String
}

var cfg *SomeConfigType = config.Load[*SomeConfigType]()

The type you pass as a type parameter to this function will be used to generate a encore.gen.cue file in your services directory. This file will contain both the CUE definition for your configuration type, and some metadata that Encore will provide to your service at runtime. This allows you to change the final value of your configuration based on the environment the application is running in.

Any files ending with .cue in your service directory or sub-directories will be loaded by Encore and given to CUE to unify and compute a final configuration.

<Toggle label="Example CUE files">
-- mysvc/encore.gen.cue --
// Code generated by encore. DO NOT EDIT.
package mysvc

#Meta: {
	APIBaseURL: string
	Environment: {
		Name:  string
		Type:  "production" | "development" | "ephemeral" | "test"
		Cloud: "aws" | "gcp" | "encore" | "local"
	}
}

#Config: {
	ReadOnly: bool   // Put the system into read-only mode
    Example:  string
}
#Config
-- mysvc/myconfig.cue --
// Set example to "hello world"
Example: "hello world"

// By default we're not in read only mode
ReadOnly: bool | *false

// But on the old production environment, we're in read only mode
if #Meta.Environment.Name == "old-prod" {
    ReadOnly: true
}
</Toggle> <Callout type="info">

Loading configuration is only supported in services and the loaded data can not be referenced from packages outside that service.

</Callout>

CUE tags in Go Structs

You can use the cue tag in your Go to specify additional constraints on your configuration. For example:

go
type FooBar {
    A int `cue:">100"`
    B int `cue:"A-50"` // If A is set, B can be inferred by CUE
    C int `cue:"A+B"`  // Which then allows CUE to infer this too
}

var _ = config.Load[*FooBar]()

Will result in the following CUE type definition being generated:

cue
#Config: {
    A: int & >100
    B: int & A-50 // If A is set, B can be inferred by CUE
    C: int & A+B  // Which then allows CUE to infer this too
}

Config Wrappers

Encore provides type wrappers for config in the form of config.Value[T] and config.Values[T] which expand into functions of type T and []T respectively. These functions allow you to override the default value of your configuration in your CUE files inside tests, where only code run from that test will see the override.

In the future we plan to support real-time updating of configuration values on running applications, thus using these wrappers in your configuration today will future proof your code and allow you to automatically take advantage of this feature when it is available.

Any type supported in API requests and responses can be used as the type for a config wrapper. However for convenience, Encore ships with the following inbuilt aliases for the config wrappers:

  • config.String, config.Bool, config.Int, config.Uint, config.Int8, config.Int16, config.Int32, config.In64, config.Uint8, config.Uint16, config.Uint32, config.Uint64, config.Float32, config.Float64, config.Bytes, config.Time, config.UUID
<Toggle label="Example Application using Wrappers">
go
-- svc/svc.go --
type mysvc

import (
    "encore.dev/config"
)

type Server struct {
    // The config wrappers do not have to be in the top level struct
    Enabled config.Bool
    Port    config.Int
}

type SvcConfig struct {
    GameServerPorts config.Values[Server]
}

var cfg = config.Load[*SvcConfig]()

func startServers() {
    for _, server := range cfg.GameServerPorts() {
        if server.Enabled() {
            go startServer(server.Port())
        }
    }
}

func startServer(port int) {
  // ...
}
-- svc/servers.cue --
GameServerPorts: [
    {
        Enabled: false
        Port:    12345
    },
    {
        Enabled: true
        Port:    1337
    },
]
</Toggle>

Provided Meta Values

When your application is running, Encore will provide information about that environment to your CUE files, which you can use to filter on. These fields can be found in the encore.gen.cue file which Encore will generate when you add a call to load config. Encore provides the following meta values:

  • APIBaseURL: The base URL of the Encore API, which can be used to make API calls to the application.

  • Environment: A struct containing information about the environment the application is running in.

       Name: The name of the environment

       Type: One of production, development, ephemeral or test.

       Cloud: The cloud the app is running on, which is one of aws, gcp, encore or local.

The following are useful conditionals you can use in your CUE files:

cue
// An application running due to `encore run`
if #Meta.Environment.Type == "development" && #Meta.Environment.Cloud == "local" {}

// An application running in a development environment in the Cloud
if #Meta.Environment.Type == "development" && #Meta.Environment.Cloud != "local" {}

// An application running in a production environment
if #Meta.Environment.Type == "production" {}

// An application running in an environment that Encore has created
// for an open Pull Request on Github
if #Meta.Environment.Type == "ephemeral" {}

Testing with Config

Through the provided meta values, your applications configuration can have different values in tests, compared to when the application is running. This can be useful to prevent external side effects from your tests, such as emailing customers across all test.

Sometimes however, you may want to test specific behaviors based on different configurations (such as disabling user signups), in this scenario using the Meta data does not give you fine enough control. To allow you to set a configuration value at a per test level, Encore provides the helper function et.SetCfg. You can use this function to set a new value only in the current test and any sub tests, while all other tests will continue to use the value defined in the CUE files.

go
-- config.cue --
// By default we want to send emails
SendEmails: bool | *true

// But in all tests we want to disable emails
if #Meta.Environment.Type == "test" {
    SendEmails: false
}
-- signup.go --
import (
    "context"

    "encore.dev/config"
)

type Config struct {
    SendEmails config.Bool
}

var cfg = config.Load[Config]()

//encore:api public
func Signup(ctx context.Context, p *SignupParams) error {
    user := createUser(p)

    if cfg.SendEmails() {
        SendWelcomeEmail(user)
    }

    return nil
}
-- signup_test.go --
import (
    "errors"
    "testing"

    "encore.dev/et"
)

func TestSignup(t *testing.T) {
    err := Signup(context.Background(), &SignupParams { ... })
    if err != nil {
        // We don't expect an error here
        t.Fatal(err)
    }

    if emailWasSent() {
        // We don't expect an email to be sent
        // as it's disabled for all tests
        t.Fatal("email was sent")
    }
}

func TestSignup_TestEmails(t *testing.T) {
    // For this test, we want to enable the welcome
    // emails so we can test that they are sent
    et.SetCfg(cfg.SendEmails, true)

    err := Signup(context.Background(), &SignupParams { ... })
    if err != nil {
        // We don't expect an error here
        t.Fatal(err)
    }

    // Check the email was sent
    if !emailWasSent() {
        t.Fatal("email was not sent")
    }
}

Useful CUE Patterns

If you're new to CUE, we'd recommend checking out the CUE documentation and cuetorials, however to get you started, here are some useful patterns you can use in your CUE files.

<Accordion>

Defaults

CUE supports the concept of a default value, which it will use if no other concrete value is provided. This can be useful for when you normally want one value, but occasionally might want to provide an override in a certain scenario. A default value is specified by prefixing it with a *.

cue
// ReadOnlyMode is a boolean and if we don't provide a value, it
// will default to false.
ReadOnlyMode: bool | *false

if #Meta.Environment.Name == "old-prod" {
    // On this environment, we want to set ReadOnlyMode to true
    ReadOnlyMode: true
}
</Accordion> <Accordion>

Validation within CUE

Any field prefixed with an _ will not be exported to the concrete configuration once evaluated by CUE and can be used to hold intermediate values. Because CUE allows you to define the same field as many times as you want, as long as the values unify, we can build complex validation logic.

cue
import (
    "list" // import CUE's list package
)

// Set some port numbers defaulting just to 8080
// but in development including 8443
portNumbers: [...int] | *[8080]
if #Meta.Environment.Type == "development" {
    portNumbers: [8080, 8443]
}

// Port numbers must be an array and all values
// are integers 1024 or above.
portNumbers: [...int & >= 1024]

// The ports are considered valid if they contain the port number 8080.
_portsAreValid: list.Contains(portNumbers, 8080)

// Ensure that the ports are valid by constraining the value to be true.
// CUE will report an error if the value is false (that is if the portNumbers list
// does not contain the value 8080).
_portsAreValid: true
</Accordion> <Accordion>

Switch Statements

If statements in CUE do not have else branches, which can make it difficult to write complex conditionals, we however can use an array to emulate a switch statement, where the first value that matches the condition is returned. The following example will set SendEmailsFrom to a single string.

cue
SendEmailsFrom: [
	// These act as individual case statements
    if #Meta.Environment.Type == "production" { "[email protected]" },
    if #Meta.Environment.Name == "staging"    { "[email protected]" },

    // This last value without a condition acts as the default case
    "[email protected]",
][0] // Return the first value which matches the condition
</Accordion> <Accordion>

Using Map Keys as Values

CUE allows us to extract map keys and use them as values to simplify the config we need to write and minimize duplication.

cue
// Define the type we want to use
#Server: {
	server: string
	port: int & > 1024
	enabled: bool | *true
}

// Specify that servers is a map of strings to #Server
// where they key we assign the variable Name
servers: [Name=string]: #Server & {
	// Then we union the key with the value of server
	server: Name
}

servers: {
	"Foo": {
        port: 8080
    },
    "Bar": {
        port:    8081
        enabled: false
    },
}

This will result in the concrete configuration of:

json
{
    "servers": {
        "Foo": {
            "server":  "Foo",
            "port":    8080,
            "enabled": true
        },
        "Bar": {
            "server":  "Bar",
            "port":    8081,
            "enabled": false
        }
    }
}
</Accordion>