Account-name backfill and legacy PII coverage
Manual account-name preservation and cleanup: service authorization, exact-count preview, conflict checks, encrypted membership labels and legacy coverage.
Overview
New signup and browser-bootstrap paths no longer copy account names into profiles.display_name. The dashboard renders the signed-in account's authentication metadata, and SCIM stores new organization labels as purpose-bound ciphertext per membership. Older profile names can still exist until an operator completes the guarded backfill.
shield-pii-migrate remains console-only. Existing platform-and-tenant admin actions inspect legacy coverage and preserve an encrypted counterpart. Clearing profile names requires the separate service-key-only operator action; it cannot be authorized by a dashboard session. Newsletter addresses already exist as ciphertext plus keyed digest: the old plaintext email column was dropped, and there is no subscriber column to shred.
See Data handling for the distinction between plaintext account data, encrypted operational values and keyed signal identifiers.
Call path and behaviour
The manual backfill-profile-names handler matches the Bearer credential against the actual configured service key, then uses one transaction per profile. It locks the profile and auth account, checks updated_at, preserves unrelated auth metadata, rejects a conflicting nonempty auth full_name, preserves encrypted labels for current memberships without replacing newer IdP labels, and clears the profile name only after preservation succeeds. It reads back the affected count and the cleared value.
Legacy envelopes bind the source name to its account because the source was global. New IdP labels also bind the organization. The older encrypted profile counterpart remains retained. No migration, cron or dashboard click invokes this operator cleanup automatically.
Apply the additive schema before dependent handlers and verify the ledger/readback: the automatic apply and deploy jobs begin independently on main. The source runbook is docs/runbooks/profile-name-backfill.md.
Actions and responses
shield-pii-migrate has two authorization lanes. The existing status and encrypt actions require a signed-in platform admin who is also an admin of the named tenant. They inspect or preserve the legacy encrypted profile counterpart. They do not preserve the account's authentication name or its organization-specific provisioning labels, and encryption coverage is not proof that account-name cleanup is complete.
Account-name cleanup uses `backfill-profile-names`. This is a manual, global operator action, never a dashboard button or a scheduled job. It accepts only the actual configured service key as its Bearer credential; an administrator session or a JWT claiming a service role is insufficient. The selected project's direct function endpoint is used, not api.passkeybridge.io. For staging, select the dashboard labeled PasskeyBridge-Staging; production must not be touched.
Preview is the default:
{"action":"backfill-profile-names","dry_run":true,"batch_size":50}It returns dry_run, the exact global remaining count, selected and batch_size. Profiles are account-scoped rather than tenant-scoped. No name or authentication metadata is returned.
After reviewing that count, a write request explicitly supplies it:
{"action":"backfill-profile-names","dry_run":false,"expected_count":12,"batch_size":50,"confirm":"BACKFILL_AUTH_AND_CLEAR_PROFILE_NAMES"}Replace 12 with the count just observed. Batches are 1 to 100 rows. For each row, one transaction locks the profile and auth account, checks the source timestamp, preserves unrelated authentication metadata, sets full_name only when it does not conflict, preserves encrypted labels for existing memberships without replacing newer IdP labels, then clears and reads back the profile name. Existing encrypted profile counterparts are retained.
A conflicting auth name, stale source or missing account stops the batch; earlier successful rows remain committed. The response carries counts, blocked_profile_id and the last successful next_cursor. Review the conflict securely rather than automatically overwriting it. Preview again before each write and use the current count. An optional after_id UUID advances past a reviewed row; skipped rows still count in remaining, and restarting without a cursor revisits them. Completion means an exact post-batch remaining of zero, not the length of a returned page.
shred on profiles now returns 409, so the older action cannot bypass these preservation steps. Settings, PII Migration Tooling shows Account backfill required instead of a profile shred control. A legacy card's encrypted count does not certify the auth backfill. No hosted cleanup is implied by this documentation.
The repository operator runbook is docs/runbooks/profile-name-backfill.md. Release the additive schema before the dependent handlers and confirm its ledger/readback; the apply and deploy jobs start independently. The SQL rollback restores only the previous signup hook and retains accepted ciphertext and auth metadata. Keep cipher-aware SCIM readers and auth-record rendering after names have migrated; reverting to older readers first would lose label readback.
Dashboard panel
Dashboard, Settings, Data governance and compliance, PII Migration Tooling. The panel still loads the legacy status action. Counts describe profiles.display_name and its retained encrypted counterpart globally, even though the request names a tenant for authorization. A failed count renders Unknown rather than complete.
Encrypt Unprotected Rows preserves up to 500 legacy names as ciphertext per call and never clears the original. Account backfill required explains why the old profile shred control is unavailable. Having an encrypted counterpart alone does not prove that the auth account name and SCIM labels have been preserved.
Migration History lists older encrypt and shred run records for the selected tenant. The new service-key backfill returns its own counts and cursor to the operator; it does not append those batches to this history. Keep the response in the operator's controlled change record. Refresh and run a fresh backfill preview to verify remaining plaintext; do not infer completion from a legacy coverage percentage.
Data model: shield_pii_migration_runs
This is the legacy shield_pii_migration_runs record for encrypt and historical shred actions. The service-key account-name backfill does not append here; retain its response in the controlled operator record.
| Column | Type | Meaning |
|---|---|---|
id | uuid | Primary key |
tenant_id | uuid | The tenant in the request body |
action | text | encrypt or shred |
target_table | text | newsletter_subscribers or profiles |
target_column | text | email or display_name |
rows_processed | integer | Rows successfully written |
rows_failed | integer | Rows that raised |
errors | jsonb | Array of messages, one per failed row |
started_at | timestamptz | Defaults to insert time |
completed_at | timestamptz | Set when the run finishes |
created_by | uuid | The operator's user id |
created_at | timestamptz | Insert time |
The only current encryption target is profiles.display_name; the subscriber entry was removed after its plaintext column was dropped. An encrypt that found nothing to do and hit no errors writes no run row. Historical rows can still name the older subscriber or shred actions.
Row-level security allows tenant admins to select and to insert rows for their own tenant. The function itself writes with the service role, so the insert policy is not what authorises these rows.
started_at is the insert default rather than a captured start time, so for a long shred it reads as the moment the row landed, close to completed_at.
Recommended workflow
- Confirm the additive schema and dependent handlers have landed in the intended environment. For staging, select the dashboard labeled PasskeyBridge-Staging; production must not be touched.
- Run
backfill-profile-nameswithdry_run: true. Inspect the exact globalremainingcount. Keep credentials outside source files and shell history. - Submit a bounded write with
dry_run: false, the observedexpected_count, andconfirm: "BACKFILL_AUTH_AND_CLEAR_PROFILE_NAMES". This is authorized by the configured service key, not a tenant-admin session. - Inspect
processed,already_cleared,remainingandnext_cursor. On a conflict or stale row, stop and review the canonical account name securely; do not overwrite it automatically. Earlier successful rows stay committed. - Preview again before every write. When deliberately advancing with
after_id, keep a record of skipped rows; they remain in the global count and need revisiting without the cursor. - Verify the final aggregate count is zero, then confirm auth-record display and SCIM label readback. An existing auth session may need refresh before seeing updated metadata.
This clears the plaintext profile copy, not all account names. Authentication metadata and encrypted counterparts remain for their stated purposes. The SQL rollback restores only signup behavior; keep compatible encrypted-label readers and auth rendering after names have migrated.
Errors
| Status | Body or condition | Meaning |
|---|---|---|
| 401 | Unauthorized | Missing Bearer credential, or an invalid user session on a legacy action |
| 403 | Service authentication required | Account-name backfill credential does not equal the configured service key |
| 403 | Forbidden | Legacy action lacks platform-admin or tenant-admin authorization |
| 400 | Invalid preview, batch_size or after_id | Malformed operator options; batch size must be 1 to 100 |
| 400 | Write batches require the preview count and explicit confirmation | Write omitted its confirmation or nonnegative exact count |
| 409 | Profile count changed; preview again | The global count differs from expected_count |
| 409 | auth_name_conflict, stale or auth_user_missing | This row cannot be preserved safely; earlier successful rows remain committed |
| 503 | Count, batch, encryption or transaction unavailable | Do not report completion; inspect returned counts and retry only after reviewing the cause |
| 409 | Profile names require the operator auth-record backfill before cleanup | The old shred action is blocked for profiles |
| 400 / 413 | Invalid JSON / request body too large | The bounded body could not be accepted |
| 405 | Method not allowed | Account-name backfill requires POST |
Operator responses contain counts, statuses and UUIDs, never a name or authentication metadata snapshot. The legacy encrypt action has its existing response and migration history; its success does not authorize bypassing the account-preservation checks.