Relayer logoRelayer

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 }.

OperatorMeaningValues
Inkey resolves to one of the values1+
NotInkey resolves, and to none of the values1+
Existskey resolves to anythingnone
DoesNotExistkey does not resolvenone
VersionGtesemver: resolved value >= the valueexactly 1, $version only
VersionLtsemver: resolved value < the valueexactly 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.

On this page