The shared change-event schema reads a country-shaped field

Table of Contents

This page is a capture in the inbox bucket of the product backlog — a pre-sprint idea, not yet pulled into a sprint as a story.

1. What

The browser decodes every change notification with one schema (changeEventSchema in projects/ores.web/packages/wire-protocol/src/client.ts), and that schema's identifier field is named alpha2_codes:

const changeEventSchema = z.object({
  timestamp: z.string().default(''),
  alpha2_codes: z.array(z.string()).default([]),
  tenant_id: z.string().default(''),
});

alpha2_codes is the field of projects/ores.refdata/api/include/ores.refdata.api/eventing/country_changed_event.hpp. The generic event every other component publishes is projects/ores.eventing/api/include/ores.eventing.api/domain/entity_change_event.hpp, whose identifier field is entity_ids.

The event subject convention is uniform: the BFF subscribes to ores.<component>.<entity>_changed for whatever a session asks to watch. The payload is not uniform, and the browser can only read one of the two shapes.

Every field in the schema has a default, and the comment above it says why: an event is news rather than a contract, and a partly-readable notification beats none. That reasoning is sound, and it is what makes this silent. Decoding a workflow event yields no error, no warning and ids: [], so a subscriber concludes nothing changed.

2. Why

Found on 2026-09-27 by the independent verification of PR #2287, while checking the claim that a journey can follow a workflow's progress on the existing change-notification channel. The subject claim was right and the payload claim was wrong.

It is latent today rather than live, because the consumer the channel was built for — the browser's change-event watcher — has no screen using it yet. The Change notification page already records that the transport survives and "the visual language for a consumer that is not built yet".

It stops being latent with the first journey that waits on server work. Provisioning is that journey: the person watches a workflow run, and the event that should tell the page to re-read the instance arrives with an empty id list.

Two ways out, and the choice belongs to whoever owns the event contract:

  • Name the field for what it is. The browser schema reads entity_ids, and a country event becomes a special case rather than the default. The event's own comment already says the contents vary by entity — "For currencies, this contains ISO codes. For accounts, UUIDs." — so a name that says alpha2_codes was always describing one caller.
  • Keep the shared shape and give the workflow its own subject and schema.

Either way the failure mode must stop being silent, which means the schema should not default the identifier list to empty without the subscriber being able to tell a real empty list from an unreadable payload.

What was decided, and by whom. The task that followed a run's progress and added the retry verb settled the consumer's half on 2026-09-28: a journey follows its run through the progress read, and this channel keeps its owner here. The read answers what the rail renders, and the browser reaches it through its own route, so nothing waits on this payload.

The decision rests on the payload being per-entity rather than merely mismatched. A country event names its identifiers alpha2_codes, a party event party_ids, a currency event iso_codes; each is generated from its own model and none follows a shared shape, so no one browser schema can read them all and "name the field for what it is" is not sufficient on its own. The choice between aligning every event's payload and giving each entity its own subject and schema is a change to the event contract, which is why it belongs here rather than to the screen that first wanted to watch a run.

3. References

  • Change notification — the channel, the payload, and the consumer that is not built.
  • Journey execution — where the claim this falsifies was made.
  • projects/ores.eventing/api/include/ores.eventing.api/domain/entity_change_event.hpp — the generic payload.

4. See also

  • Zod: basics — .default() and why a defaulted field cannot distinguish absent from empty.

Emacs 29.3 (Org mode 9.6.15)