V Platform Features

Keep Settings and Secrets out of the Code

The shop emails receipts through a mail provider. The provider’s password must not be in the repository, the sender address differs between your machine and production, and the catalog needs a whole table of currency rates. All of these are configuration.

Add Configuration declares a value — the limit of store.orders, above which its create answers LIMIT_EXCEEDED — reads it from context.configuration and sets it per environment. What follows is everything else: secrets, what a schema can hold, and how the values of the Context combine with those of the manifest.

§Keep an API key out of the repository

Receipts are sent by a component of their own, notifications. Declare the value with format: secret:

# components/notifications/manifest.toa.yaml
name: notifications

configuration:
  schema:
    properties:
      provider: { type: string, enum: [console, smtp] }
      sender: { type: string, format: email }
      password: { type: string, format: secret }
    required: [provider, sender]

operations:
  send:
    input:
      type: object
      properties:
        to: { type: string }
        text: { type: string }
      required: [to, text]

In the Context, give a reference instead of the value: an uppercase name prefixed with $. Add the component under configuration:

# context.toa.yaml
configuration:
  identity.tokens:
    # …
  notifications:
    provider: smtp
    sender: shop@example.com
    password: $SMTP_PASSWORD

The key under identity.tokens, which toa create wrote, is such a reference too.

A plain string in place of a secret is refused:

'password' is a secret and must be given as a $NAME reference.

A secret may sit anywhere in the schema, inside nested objects and array items, and several components may refer to the same name.

§Read it

On the context a secret is not a string. Call unwrap() where the value is used:

// components/notifications/operations/send.ts
import type { Context, SendInput } from '../types/index.d.ts'

export async function effect (input: SendInput, context: Context) {
  const { provider, sender, password } = context.configuration

  await deliver(provider, {
    from: sender,
    to: input.to,
    text: input.text,
    password: password?.unwrap()
  })
}

deliver stands for the code that hands the message to the provider. Converted to a string, serialized to JSON or inspected, a secret gives <REDACTED>, so returning or logging context.configuration does not leak it. Log specific values rather than the whole object anyway.

§Supply the value locally

toa env, which npm run env runs, writes one empty variable per reference into .env, named TOA_CONFIGURATION__<NAME>:

$ npm run env
TOA_CONFIGURATION__SMTP_PASSWORD=

Fill it in; values already in the file are kept when toa env runs again. Then restart the application and call the operation:

$ npm run restart
$ npx toa call notifications.send "{ input: { to: 'customer@example.com', text: 'Thank you' } }"

The variable is not visible to the component’s code, which reads the secret only through context.configuration.

§Supply the value to a deployment

Secret values are not deployed with the application. Deploy them once per environment, into the secret named configuration:

$ npx toa conceal configuration SMTP_PASSWORD=xxxxxxxx

Several values can be given in one command, and values are added to a secret that already exists.

§What the manifest takes

configuration takes two keys and nothing else:

Key
schemaRequired. A JSON Schema of the configuration object.
defaultsOptional. The values the component runs with unless the Context says otherwise.

context.configuration is one object, already checked against the schema. A value that does not fit never reaches an operation, so operations do not check it.

§Defaults

defaults must fit the schema. It is the only way to give a value a default: a default keyword written inside the schema fills nothing.

Mark a value required when there is no sensible default and the Context has to supply it:

configuration:
  schema:
    properties:
      sender: { type: string, format: email }
    required: [sender]

§Nested values

The schema can be as deep as the values need. Objects and arrays work as in any JSON Schema:

# components/store.catalog/manifest.toa.yaml
configuration:
  schema:
    properties:
      currencies:
        type: array
        items:
          type: object
          properties:
            code: { type: string }
            rate: { type: number }
          required: [code, rate]
    required: [currencies]
  defaults:
    currencies:
      - code: EUR
        rate: 1
const { rate } = context.configuration.currencies.find((c) => c.code === input.currency)

§With a prototype

A component whose prototype declares configuration extends it: the two schemas and the two sets of defaults are combined, and the component’s operations read the prototype’s values and its own from the same object.

§What the Context takes

The configuration annotation holds values per component, keyed by component id:

# context.toa.yaml
configuration:
  store.orders:
    limit: 10000

The Context’s values stand over the manifest’s defaults value by value: naming limit here leaves every other default as the manifest declares it. A component of the default namespace may be written by its bare name.

A key that names no component, or a component that declares no configuration, stops the command that reads the Context:

Component 'foo.bar' does not request configuration or does not exist.

§Per environment

The @environment suffix works at any depth:

# context.toa.yaml
configuration:
  store.orders:
    limit: 10000
    limit@staging: 100
  notifications:
    provider: console
    provider@production: smtp
    sender@local: shop@localhost
    sender@production: shop@example.com

A key with only suffixed forms has no value from the Context in the other environments, which then take the manifest’s default if there is one.

§Resources of the values service

Configuration is served to components by a service deployed with the application. One key of the annotation is not a component: resources states what that service may take in a deployment.

# context.toa.yaml
configuration:
  resources:
    cpu: [200m, 1000m]
    memory: [200Mi, 500Mi]
  store.orders:
    limit: 10000

Each is a pair of request and limit; resources: ~ deploys the service without any. A component that is really named resources is written with its namespace: default.resources.

§Try a component with other values

Set TOA_CONFIGURATION_<NAMESPACE>_<NAME> to a YAML or JSON object to give a component its configuration directly. The manifest’s defaults fill what the object leaves out, the schema is applied, and the Context is not consulted. The components of the application run in app, one of the two processes npm start keeps under PM2; stop it and start store.orders by hand, with the variable:

$ pm2 stop app
$ TOA_CONFIGURATION_STORE_ORDERS='{"limit":1}' npx toa compose components/store.orders
$ npx toa call store.orders.create "{ input: { customer: 'c1', items: [], total: 1500 } }"

The reply is the error LIMIT_EXCEEDED. Use the variable to try a component with values that differ from the Context, or to run it where the values service is absent. Stop the composition with Ctrl+C; npm run restart starts app again.

§Where values come from when a component starts

A component asks the values service for its configuration when it starts, and serves nothing until it is answered. In the application toa create wrote, the service is part of sys, the process that runs beside the components. toa mono runs it inside its one process; beside compositions started by hand, start it on its own:

$ npx toa serve configuration

It answers with, in order:

  1. the latest configuration created for the component while running, if there is one;
  2. otherwise the values the component was deployed with: the manifest’s defaults with the Context’s values over them.

The first is how a value changes without a deployment: Change a Setting without Redeploying.

§What does not belong in configuration

  • Addresses of the database, the broker or the cache. They are annotations of the Context: Addresses and Credentials.
  • Data that users of the application change. That is entity state, with operations and events of its own.
Search
Search the documentation