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
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${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}"$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: $urlversion 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: $urlThe grammar at a glance
| Syntax | Meaning |
|---|---|
$prop | Property of the enclosing resource |
$resource.prop | Property of a named resource (name defaults to type) |
$components.name.prop | Another component's endpoint (Lesson 6) |
$secrets.name | App-wide generated secret (Lesson 3) |
$app.prop | Platform-injected app property, like $app.url (Lesson 3) |
$storage.name.path | Provider-resolved storage path (Lesson 4) |
${prop} | Braced form — same reference, composable inside strings |
${prop:-default} | Reference with a fallback value |
$ref|transform | Any 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.
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. url is always the fully-formed connection string; the rest are its pieces, for apps that want them separately.
requires:
- type: postgres
set_env:
DATABASE_URL: $urlrequires:
- type: postgres
set_env:
DB_HOST: $host
DB_PORT: $port
DB_USER: $user
DB_PASSWORD: $password
DB_NAME: $nameThe 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.
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: alwaysMB_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
- Every expression in a Launchfile is one grammar: bare
$propfor 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.appandstorageare reserved. - Use
name:when you require two resources of the same type — references become$primary-db.hostinstead of an ambiguous$postgres.host. version:constrains provisioning with semver ranges;config:passes type-specific hints that unsupporting platforms ignore.- Each resource type has a standard property vocabulary. An unknown property resolves to empty string —
launchfile validatewarns about it before your app fails at first connection.
Course complete!
You're ready to write Launchfiles for any application.