Targeting
Label rules and match expressions: computed cohort membership, fail-closed semantics, and the invariants that keep rollouts honest.
Targeting restricts which devices are eligible for a release. The design
principle behind all of it: membership is a query, not a list. A
device belongs to a cohort because of what it is at the moment it checks
in - never because someone put it on a roster that can go stale. Relayer
stores the rule; the set of matching devices is computed fresh on every
decision, by the same function that powers /u/, the fleet API, the
electron feed and every dashboard preview.
The two rule types
A release's targets has two parts, ANDed together:
{
"matchLabels": { "tier": ["canary", "staff"] },
"matchExpressions": [
{ "key": "$version", "operator": "VersionLt", "values": ["2.0.0"] }
]
}matchLabels
Exact-match rules over device labels: AND across keys, any-of within a
key's values. { "tier": ["canary", "staff"] } means the device must
carry a tier label whose value is canary or staff. Labels are
opaque key/values you attach via the push-mode fleet API
or inline l.<key>=<value> query parameters on update checks - Relayer
stores and matches them, never interprets them.
matchExpressions
Decision-time predicates: { key, operator, values }.
| Operator | Meaning | Values |
|---|---|---|
In | key resolves to one of the values | 1+ |
NotIn | key resolves, and to none of the values | 1+ |
Exists | key resolves to anything | none |
DoesNotExist | key does not resolve | none |
VersionGte | semver: resolved value >= the value | exactly 1, $version only |
VersionLt | semver: resolved value < the value | exactly 1, $version only |
Keys resolve against the device's labels - or against the update request itself through three reserved fields:
$version- the version the device reports running$platform- the request's platform (darwin,windows,linux, …)$arch- the request's architecture
The $ fields are the point: the device already sends these on every
check-in, so rules like "only macOS devices still below 2.0" need zero
client changes, no label distribution, and no stored cohort anywhere.
Fail-closed, always
If a rule's key cannot be resolved (the device never got that label, the
field is empty), the device is excluded from that release. The
dangerous direction for an updater is surprising inclusion, so a missing
key never means "eligible". This applies to every operator except
DoesNotExist, whose meaning is precisely "the key is absent".
Exclusion is never a dead end: a device that fails targeting falls through to the newest release below that it is eligible for. Targeting a canary build at 50 devices does not break updates for the other ten thousand.
The invariants
Both engage at the same moment - the release's first real offer to a device. Until then, targeting and rollout are just a plan and can be edited freely.
- Labels only broaden. After the first offer you may add values to
an existing key or drop a key entirely (both let more devices in);
adding a key or removing a value would strand devices that were
already told an update exists, and is rejected with a
422. Broadening is itself permanent. - Expressions freeze. After the first offer, any expression change
is rejected with a
422. Whether an expression edit broadens or narrows is not reliably decidable once operators interact, and a wrong answer strands devices silently - so no answer is given. To change the rules, cut a new release with the rules you want.
The rollout percentage applies within the targeted cohort, and follows its own ratchet: it only increases once offered.
Targeting one device
There is no device-pinning mechanism, on purpose - it would be a stored
list with one entry. Instead: attach a unique label to the device
(debug=case-4812 via the fleet API, or l.debug=case-4812 on its
update check) and target that label. Same rules, same invariants, same
explainability.
Seeing what a rule does
Because a query is harder to eyeball than a roster, every rule is inspectable:
- Why this update? (Releases tab) traces any real or hypothetical device through the production decision walk, naming the exact rule that excluded each release.
- The release device page (click a release, then "View all devices and their verdicts") lists every device in the channel with its computed verdict: would be offered, outside the rollout bucket, excluded by which rule - filterable and searchable.
From the API
Targeting works identically from CI via
PATCH /api/v1/releases:
curl -X PATCH https://www.relayercli.com/api/v1/releases \
-H "Authorization: Bearer rl_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"app": "YOUR_APP_REF",
"channel": "stable",
"version": "2.1.0",
"targets": {
"matchLabels": { "tier": ["canary"] },
"matchExpressions": [
{ "key": "$platform", "operator": "In", "values": ["darwin"] }
]
}
}'"targets": null clears targeting (subject to the same invariants). The
serving semantics are identical to the dashboard's - they are the same
code.