0005 — PII split by event type
PII is declared per event type and routed to a separate broker path.
Decided. An event never mixes PII and non-PII fields. A fact involving personal
data is two events with the same entity_id: a core event with no PII, and a
separate PII event. contains_pii is declared per event type in a table; the
publisher routes to core-events or pii-events based on that column.
Alternatives
- A
contains_piiflag consumers check. Enforcement lives in every consumer, so every consumer is a place to get it wrong. - Field-level filtering at publish time. The publisher decides what to strip per subscriber. One bug leaks, and the leak is invisible.
- A
pii.routing-key prefix on one shared exchange. Relies on every binding pattern being right forever, with nothing behind it. Binding#picks up PII.
Why
Because no event is partially sensitive, routing becomes a whole-message decision,
and a whole-message decision can be enforced by broker permissions. Consumers not
cleared for PII get credentials with no read access to pii-events. A
misconfigured binding fails with an access error rather than quietly delivering
personal data.
Everything above that layer — the declaration, the publisher branch, the CI check — is discipline. The permission grant is enforcement.
Consequences
- A consumer needing both binds both exchanges and correlates on
entity_id. - The event feed (
GET /v1/events?after=) bypasses broker permissions, so a feed serving PII event types is authorized like any other PII read. - The same
pii:"true"struct tag drives log and trace redaction. - Re-identification risk is not detected: a core event with no PII fields can still identify someone in combination with others. Reviewer responsibility.
On Azure, per-domain fanout topics replace the shared exchanges; see 0016.