PII policy
This page describes how the product-events channel treats fields that carry personally-identifiable information. The policy applies to every event accepted by the ingest endpoint, regardless of which feature emitted it.
PII property paths
A property is treated as PII if its dotted path within the event's
properties object is on the canonical PII paths list:
- Source of truth:
pii_paths.py. - Current list:
("destination.email",).
Adding or removing a path is a versioned change. It goes through a PR review on the backend, paired with a same-PR update to this page and the affected entries on the registry. Runtime mutation of the list is not supported.
Properties whose dotted paths are not on the list are stored and forwarded as-is. The list is intentionally short and exact: no globs, no regex, no hand-wavy "looks like an email" heuristics — only fields the team has decided in advance must be hashed.
Hashing algorithm
Values at PII paths are hashed at ingest, before persistence and before the value is forwarded to the analytics destination. The raw value is never persisted and never forwarded.
- Implementation of record:
pii_hashing.py. - Algorithm: SHA-256, with a deployment-managed salt prepended to the serialized value before hashing.
- Salt management: the salt is provisioned per deployment environment and is not rotated on a schedule. Rotation would defeat determinism (see below).
- Determinism: the same input produces the same hash within a given deployment. This is intentional — joinable analytics on the destination (counting unique recipients, for example) depend on it.
- Non-string values: numbers, booleans, lists, and nested objects at a PII path are JSON-serialized to a canonical string form before hashing, so the function is well-defined for any value type.
The hash is the only representation that lives anywhere. There is no "original value" stored alongside it; the hash cannot be reversed.
Deletion
When an account is deleted (auth.users.deletion is emitted by the
auth module), every product-event row belonging to that user is
anonymized in place within 24 hours of the deletion event.
Anonymize-in-place means:
- The row's actor identity is cleared.
- The foreign key to the
auth.usersrecord is severed. - Every value at every declared PII path is redacted (replaced with
null) so that the deployment salt cannot be brute-forced against the hash.
The row itself is not hard-deleted. Aggregate counts remain stable — funnel queries that already excluded user identity continue to produce the same shape of result. This trade-off is intentional: anonymization preserves analytic value while removing the link to a specific user.
If the configured forward destination supports a user-deletion API, the backend invokes it as part of the same cascade. The default forward destination (the logging adapter) does not, in which case the backend emits a structured audit-log entry so operators can reconcile downstream warehouses manually.
Retention
The events store runs a scheduled retention sweep. Rows older than the configured retention window are anonymized using the same anonymize-in-place rule as user deletion.
- Default window: 365 days.
- Ops-tunable: the window is a service configuration value and can be adjusted without a code change.
Retention is policy, not contract: it can change. Anything you build on top of the events store should not assume PII-redacted values can be re-derived after the deletion or retention sweep has run.
Affected events
The events currently on the registry that touch the PII paths list are
listed here. Today, neither of the seed events carries a property at
the destination.email path — both emit destination_id as a flat
number instead — so no value is hashed in practice for the current
emissions:
The path is declared so that any future event that does carry a
nested destination: { email: ... } payload is hashed automatically
without the contributor having to remember to update this list.
When this page is updated
The PII policy page is updated in the same PR as any change to:
- the canonical PII paths list (
pii_paths.py); - the hashing implementation (
pii_hashing.py) — rare; a backend-level decision; - the deletion or retention behaviour on the backend — also rare.
See Adding a new product event for the checklist that gates these updates.