Lens is designed so the dashboard path does not emit configured PII/PHI. It is not HIPAA or GDPR certified. You are the operator.

What is protected

Merged, case-insensitive exact identifiers:

  1. config :phoenix_lens, masked_fields: [:email, :first_name, ...]
  2. masked_fields_by_source per database id
  3. Every Ecto schema field with redact: true on the metadata repo's OTP app
  4. Runtime rules from Settings → Column protection: global names, per source, and per table

redact: true on User.email also masks a result column named email on other tables. That is intentional in v1.

How masking is applied

Every result goes through PhoenixLens.Policy before it leaves PhoenixLens.Query. There is no public unmasked run.

A column is masked when:

  • the output name is protected (SELECT email), or
  • the origin identifier is protected (SELECT email AS contact), or
  • it is a computed expression whose SELECT item contains a protected identifier (first_name || last_name)

Masked cells are the atom :redacted, rendered [redacted], including CSV, embeds, the audit viewer, MCP tool results, and alert email/webhook payloads.

WHERE email = ... is allowed. Quoted email/phone literals are redacted in stored SQL and in the audit log.

Threat model (known limits)

  • IEx / Repo.get — out of scope. This library polices the dashboard, not the BEAM.
  • Subquery smugglingSELECT (SELECT email FROM users u2 WHERE u2.id = u.id) may not resolve an origin. Treat as a reason to keep the mount admin-only.
  • EXPLAIN — plan text can contain literals; it is still passed through the SQL redactor, not cell-level policy.
  • Result logging — do not log %PhoenixLens.Result{} rows. The library does not.

Query guards

  • One statement
  • SELECT / WITH only (optional EXPLAIN)
  • SET LOCAL transaction_read_only = on
  • statement_timeout (default 5s)
  • Hard row cap (default 10_000)