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.
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.
| Property | Implementation |
|---|---|
| Primary identifiers | Keyed 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 PII | AES-256-GCM at rest, decrypted just in time for a declared purpose code, every access written to pii_decryption_log |
| IP addresses | Keyed 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 rights | shield-dsar exports every row matching a subject digest and erases by tombstoning |
| Tenant isolation | Row-level security on tenant tables, with tenant_id enforced in the policy rather than by the caller |
| Transport | TLS, 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.
| Article | Requirement | Implementation |
|---|---|---|
| Art. 5(1)(c), minimisation | Collect only what is necessary | Identifiers 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 limitation | Keep data no longer than necessary | Enforced 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 confidentiality | Appropriate technical measures | AES-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 interest | A lawful basis for processing | Threat detection operates on digests and signal metadata; the core verification path never reads a personal identifier in the clear. |
| Art. 15, right of access | Export everything held about a subject | shield-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 erasure | Delete everything held about a subject | shield-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 design | Privacy built into the data model | Identifiers 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 processing | Maintain processing records | pii_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 processing | Encryption, pseudonymisation, testing | AES-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 notification | Notify within 72 hours | A 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, DPIA | Impact assessment | The 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.
| Section | Requirement | Implementation |
|---|---|---|
| 1798.100, right to know | Disclose the categories and the specific pieces collected | shield-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 delete | Delete on request | shield-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 purposes | Disclose sources and business purpose | Every decryption carries a purpose code and the calling function name into pii_decryption_log, which is the per-access purpose record. |
| 1798.121, limit sharing | Limit the sharing of personal information | There 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, verification | Verify the requester | DSAR 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 action | Statutory damages for unencrypted PII | Operational PII is AES-256-GCM encrypted at rest with the key held outside the database. |
CPRA additions.
| Section | Requirement | Implementation |
|---|---|---|
| 1798.100(d), retention | Retain only as long as the disclosed purpose needs | Stated 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 audits | Risk assessment for high-risk processing | pii_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 category | Requirement | Platform mechanism |
|---|---|---|
| Suspicious documents | Detect altered or forged identification | shield-vc-verify validates the credential signature and checks revocation status. See StatusList2021 Revocation. |
| Suspicious personal information | Detect inconsistent identity data | shield-cross-reference binds a SIM signal digest to a credential subject digest and scores the match, all on keyed digests. |
| Unusual account activity | Detect patterns indicating identity theft | Signal 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 providers | Act on warnings from others | The Universal Ingest API, Okta Event Hooks, carrier webhooks and the SSE and CAEP receiver all accept third-party signals. |
| Program element | Platform mechanism |
|---|---|
| Identify relevant red flags | The 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 flags | Signal ingest with in-request scoring, returning the playbook result and a correlation id on the same call. |
| Respond to red flags | Playbook 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 program | Alert 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.
| Criteria | Controls | Evidence in the platform |
|---|---|---|
| Security (CC6) | Logical access control, encryption | Row-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 response | shield-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 processing | shield_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 information | Purpose-bound decryption with an audit row per access, keyed identifier digests, tombstoning erasure |
| Privacy (P1 to P8) | Notice, consent, retention, disclosure | Purpose codes recorded per access, purpose policies recorded per tenant, the DSAR handler for access and erasure |
| Evidence source | What it holds | Retention |
|---|---|---|
shield_audit_log | Administrative and security-relevant actions, with actor, result and correlation id | Kept; the actor IP and user-agent values are cleared after 90 days |
pii_decryption_log | Every PII decryption with its purpose code | Kept |
shield_dsar_requests | Every access and erasure request with counts and notes | Kept |
shield_alert_history | Alert firings and acknowledgements | Kept; no retention job exists for this table |
shield_scim_provisioning_log | SCIM lifecycle operations | Kept |
shield_entropy_pool and shield_chain_checkpoints | Reseed provenance and the signed chain heads | Kept |
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.