Module 3 · Lesson 8

Expressions & Resource Configuration

The full $ reference grammar, and how to name, version, and configure the resources your app requires.

What you'll learn
  • The complete $ expression grammar — bare props, dot paths, templates, fallbacks, and escapes
  • How a reference is resolved — the namespace order
  • Naming resources to tell two instances of the same type apart
  • Version constraints and config hints on required resources
  • Declaring what the app uses of a resource — and the $resource.use.property fields it unlocks
  • The property vocabulary — and how validate catches typos

One grammar, not five tricks

You've been using expressions since Lesson 2 — $url on a database, ${secrets.app-key|base64} in Lesson 3, $storage.data.path in Lesson 4, $components.backend.url in Lesson 6. Each looked like its own trick. They're not: every one is the same $ reference grammar, resolved by one set of rules. This lesson puts the whole system in one place — and covers the resource fields that make expressions necessary: name, version, and config.

Build it up

1
The form you know from Lesson 2. Inside a resource's set_env, a bare $prop resolves against that resource — $url here is the Postgres connection string, because Postgres is the enclosing resource.
requires:
  - type: postgres
    set_env:
      DATABASE_URL: $url
2
Expressions compose. The braced form ${prop} means the same as $prop but can sit inside a longer string — here a JDBC URL assembled from three properties. ${port:-5432} adds a fallback: the literal after :- is used when the reference has no value.
requires:
  - type: postgres
    set_env:
      JDBC_URL: "jdbc:postgresql://${host}:${port}/${name}"
      DB_PORT: "${port:-5432}"
3
Two Postgres instances would be ambiguous — which one is $postgres.host? The name field disambiguates: references elsewhere in the file become $primary-db.host and $analytics-db.host. Without name, a resource's name defaults to its type.
requires:
  - type: postgres
    name: primary-db
    set_env:
      PRIMARY_DB_URL: $url
  - type: postgres
    name: analytics-db
    set_env:
      ANALYTICS_DB_URL: $url
4
Two more resource fields. version constrains what the platform may provision, using semver range syntax — ">=15", "^7.0", "20.x". config passes provisioning hints whose keys are resource-type-specific; a platform that doesn't understand a key ignores it.
requires:
  - type: postgres
    name: primary-db
    version: ">=15"
    config:
      extensions: [pgvector]
    set_env:
      PRIMARY_DB_URL: $url
5
uses says what the app needs of the resource — here one logical database and pub/sub channels. Each declared use registers its own fields under $resource.use.property: $redis.db.url is the Redis URL with the database selected by path (redis://host:6379/3), $redis.db.index that integer. The bare $url is still the instance address. Leave uses off and the entry means exactly what it meant before.
requires:
  - type: redis
    uses: [db, pubsub]
    set_env:
      QUEUE_URL: $redis.db.url
      QUEUE_DB: $redis.db.index
      PUBSUB_URL: $url
6
An app that needs two isolated databases names each one. db is repeatable, so it may occur more than once as a single-key map — - db: queue — and each name gets its own fields under $resource.use.name.property: $redis.db.queue.url and $redis.db.sessions.url select different database indexes, never the same one. Name every occurrence or declare the token once unnamed; naming pubsub or server is a validation error, because the type cannot hand over a second one.
requires:
  - type: redis
    uses:
      - db: queue
      - db: sessions
      - pubsub
    set_env:
      QUEUE_URL: $redis.db.queue.url
      SESSION_URL: $redis.db.sessions.url
      SESSION_DB: $redis.db.sessions.index
      PUBSUB_URL: $url

The grammar at a glance

SyntaxMeaning
$propProperty of the enclosing resource
$resource.propProperty of a named resource (name defaults to type)
$resource.use.propProperty registered by a declared use of a named resource, like $redis.db.url
$resource.use.name.propProperty registered by a named occurrence of a repeatable use, like $redis.db.cache.url
$components.name.propAnother component's endpoint (Lesson 6)
$components.name.endpoint.propOne named endpoint on another component — host, port, protocol, and url for an http/https/ws endpoint
$secrets.nameApp-wide generated secret (Lesson 3)
$app.propPlatform-injected app property, like $app.url (Lesson 3)
$app.endpoints.name.propPublic address of a named published endpoint, like $app.endpoints.ssh.port (Lesson 3)
$storage.name.pathProvider-resolved storage path (Lesson 4)
${prop}Braced form — same reference, composable inside strings
${prop:-default}Reference with a fallback value
$ref|transformAny reference piped through a transform, like |base64 (Lesson 3)
$$A literal $ — the escape for values like $$HOME/bin

How a reference is resolved

When the provider meets a dotted path, it checks the namespaces in a fixed order: app.* first, then secrets.*, then components.*, then storage.*. Only then does it treat the first segment as a resource name. A single bare segment — $host, $port — never goes through that lookup: it always means the enclosing resource.

What the app uses of a resource

requires: redis says the app needs Redis. It does not say what the app needs of it — one keyspace, pub/sub channels, or the whole server — and a platform that pools a Redis instance between apps has to guess. uses takes the guess away. It is a short list from a per-type vocabulary: for redis, db, pubsub and server; for postgres and mysql, database and server. A platform may satisfy the entry from a shared unit only when every declared use fits in it; otherwise it provisions its own, or refuses.

Each use that hands the app something of its own registers it under $resource.use.property. db registers url — the standard Redis URL with the database selected by path, redis://host:6379/3 — and index, that integer; database registers url and name. pubsub and server register nothing: the instance's own $url, $host and $port already address them, and those bare properties keep their meaning whatever you declare.

A use marked repeatable — db on redis, database on postgres and mysql — may occur more than once on one entry, each occurrence a single-key map naming it: - db: cache, - db: sessions. A named occurrence registers the same fields under its name, $redis.db.cache.url and $redis.db.cache.index, and the platform hands each name its own database — two names never share an index. An entry that needs one database keeps the bare token and the three-segment $redis.db.url. Within one entry a token is bare or named, never both.

A use you did not declare is an error, not an empty string

On an entry that declares uses, a three-segment path either resolves from the declared use's registered fields or fails at wiring time. $redis.db.url beside uses: [pubsub] does not quietly fall back to the instance URL — it stops the deployment and names the path. The same holds for the named form: beside - db: cache, $redis.db.url and $redis.db.nosuch.url are errors, not the instance URL and not one of the named databases. A token outside the vocabulary is a validate warning (the vocabulary is open), but at deploy time a provider refuses a requires entry declaring a use it does not recognise, because no provider can claim to cover a use it does not know. On a supports entry the same shortfall leaves the entry unfulfilled instead.

app and storage are reserved

Because the namespaces are checked first, a resource named app or a volume named storage can't be reached by expression — $app.url still resolves to the platform-injected app property, not your resource. Pick a different name.

The property vocabulary

What can go after the $? Each resource type exposes a standard property set: postgres gives you url, host, port, user, password, and name; redis gives url, host, port, password; and so on. Where a type exposes url, it is the resource's fully-formed address — a connection string for a database, cache or queue — and the type's other properties are that address's pieces, for apps that want them separately. Not every type has one: certificate registers cert_file and key_file, two paths inside the app's filesystem, and no address at all.

One type registers url and nothing else: https-origin, which asks for a public HTTPS origin in front of the app rather than a service behind it. There url is that origin — https://vault.example.com — the same string $app.url resolves to. It is the one entry that references one of the app's own listeners, with an endpoint: naming a provides entry:

provides:
  - name: web
    protocol: http
    port: 80
    exposed: true
requires:
  - type: https-origin
    endpoint: web
    set_env:
      DOMAIN: $url

endpoint: is not a keyword — it is whatever the app called that endpoint. It is required on an https-origin entry, meaningless on any other type, and must name an exposed: true listener speaking http, https, ws or grpc on the same component; naming a tcp or udp one is a validation error, because those listeners have no origin. An app declares at most one, and the endpoint it names is the app's primary — the one $app.* describes.

One type registers no address at all: certificate, which hands the app a certificate and a private key so its own listener can serve TLS. Its two properties are app-filesystem paths — cert_file and key_file — and the provides entry points back at it with tls::

provides:
  - name: web
    protocol: http      # the baseline listener
    port: 3000
    exposed: true
    tls: server-cert    # shorthand for `tls: { certificate: server-cert }`
supports:
  - name: server-cert
    type: certificate
    set_env:
      GITEA__server__PROTOCOL: https
      GITEA__server__HTTP_PORT: "3000"
      GITEA__server__CERT_FILE: $cert_file
      GITEA__server__KEY_FILE: $key_file

The two entries point in opposite directions. An https-origin sits in front of the app; a certificate is delivered to it. Declaring one never implies the other, and a certificate binding does not by itself satisfy an https-origin entry. protocol: http keeps describing the baseline; while the binding is active the listener's effective protocol is https, and that is what a sibling's $components.<name>.url reads. That is why the bound entry must speak http, https, ws or grpc, the same family an https-origin endpoint: requires: tls: on a tcp or udp listener is a validation error, because https is not a protocol such a listener can speak. Selection happens outside the file — nobody selects it, and the app runs the HTTP baseline, which is the correct deployment rather than a degraded one. key_file is credential-bearing: a provider registers its value with its redactor before it generates anything, so a private-key path never lands in a log.

One string — most apps
requires:
  - type: postgres
    set_env:
      DATABASE_URL: $url
The pieces — apps that insist
requires:
  - type: postgres
    set_env:
      DB_HOST: $host
      DB_PORT: $port
      DB_USER: $user
      DB_PASSWORD: $password
      DB_NAME: $name
Why typos don't fail — and don't hide either

The vocabulary is standard but open: a provider may expose extra properties, so a reference outside the standard set isn't invalid — it might be a deliberate extension. But it might be a typo, and an unknown property resolves to empty string. Write $hoost and your app gets an empty DB_HOST — a failure that surfaces at first connection, far from its cause. That's why launchfile validate reports any reference outside the standard vocabulary as an advisory warning, never an error. Run it before you deploy.

In the wild

Metabase is a business-intelligence tool that refuses the single-URL shortcut — its config wants every database detail in its own variable. Its Launchfile is the property vocabulary at full stretch.

metabase/LaunchfileView on GitHub
version: launch/v1
name: metabase
description: "Business intelligence and analytics tool"
repository: https://github.com/metabase/metabase
logo: https://raw.githubusercontent.com/metabase/metabase/master/resources/frontend_client/app/assets/img/logo.svg

image: metabase/metabase:latest
provides:
  - protocol: http
    port: 3000
    exposed: true
requires:
  - type: postgres
    set_env:
      MB_DB_HOST: $host
      MB_DB_PORT: $port
      MB_DB_DBNAME: $name
      MB_DB_USER: $user
      MB_DB_PASS: $password
env:
  MB_DB_TYPE:
    default: "postgres"
  MB_ENCRYPTION_SECRET_KEY:
    generator: secret
    sensitive: true
    description: "Secret key for encrypting database credentials"
health:
  path: /api/health
  start_period: 120s
  interval: 10s
  timeout: 5s
  retries: 12
restart: always
MB_DB_HOST: $host
Bare $prop resolves against the enclosing resource — this is the Postgres host, not the app's.
MB_DB_DBNAME: $name
$name is the database name from the vocabulary — one letter off ($naame) and it would resolve to empty string, which is exactly what validate's advisory warning exists to catch.
MB_DB_TYPE:
A plain literal default — not every value needs an expression.
generator: secret
The Lesson 3 machinery, side by side with resource expressions — one grammar across the whole file.

Try it: npx launchfile up metabase to launch this app locally.View in catalog

Check your understanding

Inside a requires entry's set_env, what does a bare $host resolve to?
Key takeaways
  • Every expression in a Launchfile is one grammar: bare $prop for the enclosing resource, dotted paths for everything else, ${...} to compose inside strings, ${prop:-default} for fallbacks, $$ for a literal dollar.
  • Dotted paths resolve namespaces in a fixed order — app, secrets, components, storage, then resource names. app and storage are reserved.
  • Use name: when you require two resources of the same type — references become $primary-db.host instead of an ambiguous $postgres.host.
  • version: constrains provisioning with semver ranges; config: passes type-specific hints that unsupporting platforms ignore.
  • uses: declares what the app needs of a resource — [db, pubsub] on redis — and unlocks $resource.use.property fields like $redis.db.url. A repeatable use is named to occur more than once — - db: cache → $redis.db.cache.url. Undeclared means what the entry always meant; a provider covers every declared use or refuses.
  • Each resource type has a standard property vocabulary. An unknown property resolves to empty string — launchfile validate warns about it before your app fails at first connection.

Course complete!

You're ready to write Launchfiles for any application.

esc
Type to search the docs