Zero-PII Compliance Guide

How the zero-PII data model maps to GDPR, CCPA and CPRA, SEC Regulation S-ID, and the SOC 2 readiness evidence the platform collects, with the mechanism behind every row.

Last reviewed September 21, 2026Fresh

Overview

PasskeyBridge is built on one rule: a low-entropy personal identifier is never stored, logged or emitted in the clear. Phone numbers, email addresses and subject identifiers exist as keyed HMAC-SHA-256 digests computed under a server-held pepper. The few operational fields that have to be read back, a newsletter address and a display name, exist as AES-256-GCM ciphertext that is decrypted inside a single function invocation for a declared purpose, with the access recorded.

This guide maps that architecture onto GDPR, CCPA and CPRA, SEC Regulation S-ID, and the SOC 2 readiness evidence the platform collects. Every row names the mechanism behind it so an assessor can check it against the code.

PropertyImplementation
Primary identifiersKeyed HMAC-SHA-256 under a server-held pepper (_shared/identifier-hash.ts), stored as 64 hex characters. A missing pepper throws rather than degrading to an unkeyed digest.
Operational PIIAES-256-GCM at rest, decrypted just in time for a declared purpose code, every access written to pii_decryption_log
IP addressesKeyed HMAC-SHA-256 digest of the whole address; the raw value never reaches the database. Audit-log IP and user-agent values are cleared after 90 days.
Data subject rightsshield-dsar exports every row matching a subject digest and erases by tombstoning
Tenant isolationRow-level security on tenant tables, with tenant_id enforced in the policy rather than by the caller
TransportTLS, terminated at Cloudflare in front of the edge functions

Two caveats that belong in any assessment. Keyed hashing shipped in August 2026. Rows written earlier carry hash_version = 1, an unkeyed SHA-256, and cannot be upgraded in place because the plaintext was never retained to re-hash; reads match every scheme so historical correlation survives, and the non-enumerability claim applies to keyed digests, which are version 2 and, for subject identifiers since September 2026, version 3. Version 3 is the same keyed HMAC over a message that preserves case, so a case-sensitive subject such as a did:key or an OIDC sub no longer collides with a differently-cased one; nothing was migrated, because reads match both. Separately, one clear-text field exists by design: subject_ref, a tenant-supplied correlation handle sent with a signal, is stored on shield_events and copied into the tenant's own webhook delivery bodies. It is validated on write, it is the tenant's own value rather than one PasskeyBridge derives, and an erasure request nulls it in both places.

GDPR mapping

Regulation: General Data Protection Regulation (EU) 2016/679.

ArticleRequirementImplementation
Art. 5(1)(c), minimisationCollect only what is necessaryIdentifiers are keyed digests before storage. No plaintext phone number, email address or postal address is held outside the vault's ciphertext columns.
Art. 5(1)(e), storage limitationKeep data no longer than necessaryEnforced where the code enforces it: cached proofs expire on expires_at, BLAST sessions are torn down at expiry by cleanup_expired_records, hosted passkey challenges are single use with a five-minute TTL, and audit-log IP and user-agent digests are nulled after 90 days by anonymize_old_audit_entries (daily, 03:00 UTC). There is no general retention purge; other rows are kept until an erasure request.
Art. 5(1)(f), integrity and confidentialityAppropriate technical measuresAES-256-GCM at rest, TLS in transit, keyed digests for identifiers, row-level security per tenant, hybrid post-quantum signatures on artifacts that leave the platform.
Art. 6(1)(f), legitimate interestA lawful basis for processingThreat detection operates on digests and signal metadata; the core verification path never reads a personal identifier in the clear.
Art. 15, right of accessExport everything held about a subjectshield-dsar action access exports ten tables matching the subject's keyed digest: seven user_hash tables plus shield_cross_references, shield_events and pii_decryption_log. The response carries per-table counts taken from the database and a not_covered list. See DSAR Workflows.
Art. 17, right to erasureDelete everything held about a subjectshield-dsar action erasure tombstones: identifier columns nulled, encrypted columns overwritten with the literal SHREDDED, webhook request and response bodies nulled, records deactivated, and pii_decryption_log.user_hash unlinked while the audit row survives. No encryption key is destroyed.
Art. 25, data protection by designPrivacy built into the data modelIdentifiers go through hashIdentifier() before any write; operational PII goes through the vault's encrypt() or encryptAndHash(). Both fail closed when their key is absent.
Art. 30, records of processingMaintain processing recordspii_decryption_log records every decryption with purpose code, function, field and timestamp. shield_audit_log records administrative and security-relevant actions with actor, result and correlation id.
Art. 32, security of processingEncryption, pseudonymisation, testingAES-256-GCM (NIST SP 800-38D), keyed HMAC-SHA-256 pseudonymisation, ML-DSA plus ECDSA signing, tenant key rotation with a 24-hour grace window.
Art. 33 and 34, breach notificationNotify within 72 hoursA database copy yields keyed digests and ciphertext. The master key and the identifier pepper live in the platform secret store, never in the database, so neither is obtainable through a database compromise.
Art. 35, DPIAImpact assessmentThe assessment scope is narrowed by the data model: the categories of plaintext personal data the platform persists are the vault's two operational fields and the tenant-supplied subject_ref.

Where the export stops. Four tables cannot be matched to a subject digest at all, because they key on a different namespace: shield_rp_users, shield_rp_credentials, shield_rp_challenges and shield_attestation_state. Both DSAR actions name them in not_covered with the tenant's row count and the remedy, rather than omitting them silently. Hosted relying-party data is erased through shield-passkey-rp action delete_user.

CCPA and CPRA mapping

Regulation: California Consumer Privacy Act (Cal. Civ. Code 1798.100 to 1798.199.100), as amended by CPRA.

SectionRequirementImplementation
1798.100, right to knowDisclose the categories and the specific pieces collectedshield-dsar action access returns tables, record_counts, total_records and the row data itself, plus not_covered for what this path cannot reach.
1798.105, right to deleteDelete on requestshield-dsar action erasure returns affected per table, shredded_tables and shredded_field_count, each count read back from the database after the statement ran.
1798.110, categories and purposesDisclose sources and business purposeEvery decryption carries a purpose code and the calling function name into pii_decryption_log, which is the per-access purpose record.
1798.121, limit sharingLimit the sharing of personal informationThere is no plaintext identifier to share: correlation, scoring and intelligence all run on keyed digests. The one value that leaves in the clear is the tenant's own subject_ref, and only to that tenant's own webhook endpoint.
1798.130, verificationVerify the requesterDSAR requests require a Supabase user JWT and is_tenant_admin on the target tenant, and are deduplicated against a completed request for the same subject and action within the last hour.
1798.150, private right of actionStatutory damages for unencrypted PIIOperational PII is AES-256-GCM encrypted at rest with the key held outside the database.

CPRA additions.

SectionRequirementImplementation
1798.100(d), retentionRetain only as long as the disclosed purpose needsStated plainly: the platform enforces the 90-day audit-log scrub, the expiry paths above and the DSAR pipeline. The ttl_seconds on a purpose policy is a recorded expectation and is not enforced at decryption time.
1798.185(a)(15), cybersecurity auditsRisk assessment for high-risk processingpii_decryption_log and shield_audit_log provide the access record, shield-compliance-report aggregates decryptions by purpose and function over a period, and Settings, PII Migration Tooling shows how much plaintext is left per column.

SEC Regulation S-ID

Regulation: SEC Regulation S-ID (17 CFR 248.201 and 248.202), Identity Theft Red Flags.

Reg S-ID applies to covered financial institutions and creditors, which PasskeyBridge is not. This section maps platform mechanisms onto the program elements such an institution has to document, so a customer in scope can cite what the platform does. It is not legal advice.

Red flag categoryRequirementPlatform mechanism
Suspicious documentsDetect altered or forged identificationshield-vc-verify validates the credential signature and checks revocation status. See StatusList2021 Revocation.
Suspicious personal informationDetect inconsistent identity datashield-cross-reference binds a SIM signal digest to a credential subject digest and scores the match, all on keyed digests.
Unusual account activityDetect patterns indicating identity theftSignal ingest classifies each event as hard, soft or unknown. The nine hard types are sim_swap, sim_swap_detected, port_out, number_port, number_porting, device_compromise, ss7_intercept, account_takeover and scope_poisoning.
Alerts from service providersAct on warnings from othersThe Universal Ingest API, Okta Event Hooks, carrier webhooks and the SSE and CAEP receiver all accept third-party signals.
Program elementPlatform mechanism
Identify relevant red flagsThe signal classifier is the list a program can cite: hard signals map to takeover and porting red flags, soft signals to velocity, geography, device and credential-leak indicators.
Detect red flagsSignal ingest with in-request scoring, returning the playbook result and a correlation id on the same call.
Respond to red flagsPlaybook actions run in parallel per signal, and a hard signal also revokes agent delegates and A2A negotiations inline. The four-branch cascade across every identity subsystem is shield-cascade, an admin action on Enterprise.
Update the programAlert rules and thresholds are editable in the Observability tab, and the Intelligence tab holds the review queue for items flagged for a person to look at.

SOC 2 readiness evidence

Status: readiness. Evidence is collected and under review. No CPA firm is engaged, so nothing on this page is a certification, an attestation or an audit opinion. What follows is the evidence a reviewer can pull, mapped to the AICPA trust service criteria.

CriteriaControlsEvidence in the platform
Security (CC6)Logical access control, encryptionRow-level security per tenant, AES-256-GCM at rest, TLS in transit, API keys looked up by digest with scope and tenant binding, two-layer rate limiting
Availability (A1)Monitoring and incident responseshield-alerting threshold rules with Slack and PagerDuty dispatch, shield_alert_history with acknowledgement tracking, cron heartbeats per scheduled function
Processing integrity (PI1)Complete and accurate processingshield_audit_log for every mutation, hash-chained audit and event ledgers with a signed checkpoint every five minutes, HMAC webhook signatures, nonce-based replay protection
Confidentiality (C1)Protect confidential informationPurpose-bound decryption with an audit row per access, keyed identifier digests, tombstoning erasure
Privacy (P1 to P8)Notice, consent, retention, disclosurePurpose codes recorded per access, purpose policies recorded per tenant, the DSAR handler for access and erasure
Evidence sourceWhat it holdsRetention
shield_audit_logAdministrative and security-relevant actions, with actor, result and correlation idKept; the actor IP and user-agent values are cleared after 90 days
pii_decryption_logEvery PII decryption with its purpose codeKept
shield_dsar_requestsEvery access and erasure request with counts and notesKept
shield_alert_historyAlert firings and acknowledgementsKept; no retention job exists for this table
shield_scim_provisioning_logSCIM lifecycle operationsKept
shield_entropy_pool and shield_chain_checkpointsReseed provenance and the signed chain headsKept

Key rotation history is in shield_audit_log under the actions tenant_key.provision, tenant_key.rotate and rotation.tenant_key_rotated, each carrying the old and new key ids.

Compliance architecture summary

The architecture reduces what a compromise yields rather than adding controls on top of a conventional data model. Three scenarios, stated as the code behaves.

A copy of the database. The attacker gets keyed HMAC-SHA-256 digests, which cannot be reversed by enumeration without the pepper and disclose only that two rows refer to the same identifier; AES-256-GCM ciphertext, which is useless without the master key; and any subject_ref handles the tenant supplied in the clear. The pepper and the master key live in the platform secret store, so neither is in the copy.

An insider with database access. The same material, plus whatever the audit tables show. Worth being precise about the limit: the decryption audit row is written by the vault module rather than by a database trigger, so an insider who also holds the master key and decrypts ciphertext outside the platform leaves no pii_decryption_log row. What the log proves is that every decryption through the platform was purpose-coded and recorded, and the audit tables are append-only to clients (no insert, update or delete policy exists for them).

A compromised edge function. It gets what it can decrypt for the purpose it declares, and each decryption writes an audit row naming the purpose, the function and the field. Nothing throttles it: the vault performs no rate check, so a bulk read appears in the log as volume after the fact rather than being refused. That is why the purpose policy, the hourly figure a tenant records per purpose code, is a review criterion worth setting even though it gates nothing.

The net effect is that no single compromise yields plaintext personal data at scale, and that every path which does produce plaintext leaves a record of why.

Related from the blog