---
title: "PII & Data Handling"
slug: pii-data-handling
section: Governance & Operations
status: stable
description: "What counts as PII in Globus and how it is classified, redacted, access-logged, and retained."
last-updated: 2026-10-07
---

# PII & Data Handling

What counts as PII in Globus and how it is classified, redacted, access-logged, and retained. Verified against Globus source 2026-07-04 — the redaction registry, unmask gating, access logging, and retention policies below are live code, not proposals.

## Status

> **The protection machinery is live; erasure/DSAR are not**
>
> The original draft planned a "Platform PII Registry" as a future ticket — it already exists as the hard-coded redaction key lists in server/middleware/piiRedactor.ts and server/lib/redactForRole.ts. Right-to-erasure and DSAR workflows remain unbuilt (see the last section). Audience visibility is governed by the step-level data-scope dropdown plus this registry — there is no per-field redaction enum.

## Platform Redaction Registry (live)

server/middleware/piiRedactor.ts defines the always-mask tiers, applied to audit before/after payloads and sensitive reads. Coverage is enforced by pii-redaction-coverage.checker.test.ts against pii-redaction-coverage-allowlist.json.

| Tier | Mask | Fields |
| --- | --- | --- |
| FULLY_REDACTED_KEYS | value replaced with `[REDACTED]` | password, ssn, address / streetAddress, emergencyContact, permanentAddress, taxId |
| LAST4_KEYS | *****NNNN | passportNumber, phone, bankAccountNumber, routingNumber |
| DATE_YEAR_ONLY_KEYS | YYYY-**-** | dateOfBirth |
| DATE_YEAR_MONTH_KEYS | YYYY-MM-** | passportExpiryDate |

## Permission-Gated Unmasking

server/middleware/piiRedactionGuard.ts (Task #771) gates unmasked reads: the caller needs sensitive_data.view (canonical), applicant.pii.read (alias), or god-mode. Unauthorized callers receive the masked value — the guard fails closed. Every successful unmask writes an applicant.pii.unmasked audit row.

## Mandatory PII Access Logging

server/middleware/piiAccessLogger.ts (Task #800) implements write-before-respond: the access log row is committed before the response body is sent, and if logging fails the body is withheld with a 500 PII_ACCESS_LOG_FAILED. Detected fields: dateOfBirth/dob, passportNumber, ssn, email, phone, address, countryOfBirth.

Log table pii_access_logs (shared/models/sevis.ts): uuid id, userId (FK users), action, polymorphic entityType/entityId, fields_accessed text[], endpoint, ipAddress, userAgent, accessedAt — indexed by user, entity, time and action.

## Retention Policies

server/services/dataRetentionService.ts reads policy rows from data_retention_policies (isActive-gated); purge is a hard delete past the cutoff. Defaults:

| Data set | Retention | Archive first? |
| --- | --- | --- |
| sevis_batch_items | 1095 days (~3 years) | yes |
| sevis_batches | 1095 days (~3 years) | yes |
| pii_access_logs | 2555 days (~7 years) | no |
| audit_logs | 2555 days (~7 years) | no |

## Storage Reality

There is no application-level column encryption or app-managed key rotation in the codebase. Protection is redaction + permission gating + mandatory access logging on top of platform at-rest encryption (Cloud SQL, GCS). Don't cite app-managed encryption in compliance answers without adding it first.

## Deleted Staff Users — Tombstones & Audit Attribution

Staff users are deletable with preserved audit attribution (QA, 2026-07 — Task #1953). Previously, deleting a user fired `ON DELETE SET NULL` updates against append-only audit tables, which the append-only protection trigger blocked — the delete surfaced as a 500 `USER_DELETE_FAILED`, so departed staff could not be removed at all.

- A `deleted_users` tombstone table captures name, email, role, and `deletedBy` at deletion time.
- The migration drops the `users` foreign keys on `partner_org_lifecycle_audit` (actor), `audit_logs` (user + impersonated user), and the applicant-lifecycle history staff columns, so audit rows keep the original actor id verbatim. The agreement and screening `SET NULL` keys were deliberately left as-is.
- Deletion is transactional (tombstone upsert + delete). Genuinely blocked deletions map to **409** with `USER_DELETE_BLOCKED_BY_AUDIT_PROTECTION` or `USER_DELETE_BLOCKED_BY_RECORDS`; the user management screen shows a clear message.
- Actor enrichment falls back to the tombstone for unresolved ids, returning name, role, and `"deleted": true`.

> **Tombstone retention is indefinite**
>
> GDPR erasure / retention policy for `deleted_users` tombstones is flagged for a separate compliance review (deferred follow-up #1956) — it belongs to the same unbuilt erasure/DSAR family below.

## Not Yet Built — Erasure & DSAR

> **Target-state only**
>
> Right-to-erasure and a DSAR (right-to-access) workflow are not implemented. Retention purges only SEVIS batch data and the two log tables — applicant PII is never purged today. Cross-border transfer rules, a BI-warehouse PII stance, and a breach runbook also remain to be authored. Related contracts: RBAC (sensitive-data redaction), Field Classification Contract, Integration Contract, Private OCR, OCR Compliance Ledger.
