Skip to content
🔒

Login Required

You need to be logged in to view this content. This page requires Admin access.

GDPR Compliance Implementation ​

Story: 2-5-2 Database Sync After Email Verification Version: 1.0 Last Updated: 2025-11-25

Overview ​

This document describes how the Hello World DAO platform implements GDPR (General Data Protection Regulation) compliance for user PII (Personally Identifiable Information). The implementation covers both the encrypted canister storage and plaintext database storage in the hybrid architecture.

Regulatory Context ​

Applicability ​

GDPR applies when:

  • ✅ Platform has users in the EU (European Union)
  • ✅ Platform offers services to EU residents
  • ✅ Platform processes PII of EU residents

Hello World DAO status: GDPR compliant architecture implemented.

Key GDPR Principles ​

  1. Lawfulness, Fairness, Transparency: User consent obtained during registration
  2. Purpose Limitation: PII used only for stated purposes (membership, marketing)
  3. Data Minimization: Only necessary PII collected (email, name)
  4. Accuracy: Users can update their information
  5. Storage Limitation: Retention policies defined
  6. Integrity and Confidentiality: Encryption and access controls
  7. Accountability: Audit logs and compliance procedures

User Rights Implementation ​

1. Right to Access (Article 15) ​

Requirement: Users must be able to obtain confirmation that their PII is being processed and access that data.

Implementation:

Canister Access (Encrypted PII) ​

rust
// user-service/src/lib.rs
#[update]
fn get_individual_decryption_data(email_hash: String) -> Result<EncryptedIndividual, String> {
    STATE.with(|state| {
        let s = state.borrow();
        s.individuals.get(&email_hash)
            .cloned()
            .ok_or_else(|| "Individual not found".to_string())
    })
}

User Flow:

  1. User logs in with password
  2. Frontend derives master key from password
  3. Frontend fetches encrypted PII from canister
  4. Frontend decrypts PII in browser
  5. User sees their data (email, first_name, last_name, recovery key)

Database Access (Plaintext PII) ​

sql
-- Query user's plaintext PII
SELECT
    id,
    email,
    first_name,
    last_name,
    verified,
    verified_at,
    submitted_at,
    gdpr_marketing_consent,
    gdpr_deleted,
    created_at,
    updated_at,
    synced_at
FROM individuals
WHERE email = 'user@example.com'
  AND gdpr_deleted = false;

API Endpoint (planned):

GET /api/users/me/data
Authorization: Bearer <user_session_token>

Response:
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "email": "user@example.com",
  "first_name": "John",
  "last_name": "Doe",
  "verified": true,
  "verified_at": "2025-11-01T10:30:00Z",
  "marketing_consent": true
}

2. Right to Rectification (Article 16) ​

Requirement: Users must be able to correct inaccurate PII.

Implementation:

Canister Update (Encrypted PII) ​

rust
// user-service/src/lib.rs
#[update]
fn update_individual(
    email_hash: String,
    encrypted_email: String,
    encrypted_first_name: String,
    encrypted_last_name: String,
) -> Result<(), String> {
    STATE.with(|state| {
        let mut s = state.borrow_mut();
        let record = s.individuals.get_mut(&email_hash)
            .ok_or_else(|| "Individual not found".to_string())?;

        // Update encrypted fields
        record.email_encrypted = encrypted_email;
        record.first_name_encrypted = encrypted_first_name;
        record.last_name_encrypted = encrypted_last_name;

        Ok(())
    })?;

    // Trigger database sync with updated plaintext PII
    // (requires plaintext provided by frontend)
    sync_individual_to_database(...).await?;

    Ok(())
}

User Flow:

  1. User updates PII in frontend
  2. Frontend encrypts new PII with master key
  3. Frontend sends encrypted PII to canister
  4. Canister updates encrypted record
  5. Canister syncs plaintext to database (if verification status unchanged)

3. Right to Erasure / "Right to be Forgotten" (Article 17) ​

Requirement: Users must be able to request deletion of their PII.

Implementation: Soft Delete (data marked deleted, not physically removed)

Why Soft Delete? ​

  • ✅ Audit compliance: Regulatory requirements may mandate data retention
  • ✅ Fraud prevention: Need to track deleted accounts to prevent re-registration fraud
  • ✅ Referential integrity: Avoid breaking foreign key relationships
  • ✅ Recovery: User can undo deletion within grace period (planned)

Canister Soft Delete ​

rust
// user-service/src/lib.rs
#[update]
fn delete_individual(email_hash: String) -> Result<(), String> {
    let current_time = ic_cdk::api::time();

    STATE.with(|state| {
        let mut s = state.borrow_mut();
        let record = s.individuals.get_mut(&email_hash)
            .ok_or_else(|| "Individual not found".to_string())?;

        // Mark as deleted (GDPR flag)
        record.gdpr_deleted = true;
        record.gdpr_deleted_at = Some(current_time);

        // Optionally: Zero out encrypted PII fields
        record.email_encrypted = String::new();
        record.first_name_encrypted = String::new();
        record.last_name_encrypted = String::new();
        record.recovery_key_encrypted = String::new();

        Ok(())
    })?;

    // Sync deletion status to database
    sync_deletion_to_database(&email_hash).await?;

    Ok(())
}

Database Soft Delete ​

Database sync propagates gdpr_deleted = true:

sql
-- Sync deletion status
UPDATE individuals
SET
    gdpr_deleted = true,
    gdpr_deleted_at = NOW(),
    gdpr_marketing_consent = false, -- Revoke marketing consent
    updated_at = NOW()
WHERE email_hash = '2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae';

Marketing Exclusion ​

CRITICAL: Deleted users MUST be excluded from all marketing queries:

sql
-- Marketing list (GDPR compliant)
SELECT email, first_name, last_name
FROM individuals
WHERE verified = true
  AND gdpr_deleted = false           -- Exclude deleted users
  AND gdpr_marketing_consent = true; -- Exclude opt-outs

Audit Query (verify no deleted users in marketing):

sql
-- Should return 0
SELECT COUNT(*)
FROM individuals
WHERE gdpr_deleted = true
  AND gdpr_marketing_consent = true;

4. Right to Data Portability (Article 20) ​

Requirement: Users must be able to export their PII in a machine-readable format.

Implementation:

Export Endpoint (Planned) ​

typescript
// oracle-bridge/src/routes/users.ts
router.get('/export', requireAuth, async (req: Request, res: Response) => {
  const userId = req.user.id;

  // Fetch from database
  const result = await pool.query(
    `SELECT
       id, email, first_name, last_name,
       verified, verified_at, submitted_at,
       gdpr_marketing_consent, gdpr_deleted
     FROM individuals
     WHERE id = $1 AND gdpr_deleted = false`,
    [userId]
  );

  if (result.rows.length === 0) {
    return res.status(404).json({ error: 'User not found' });
  }

  const user = result.rows[0];

  // Return JSON export
  res.json({
    format: 'GDPR Data Export',
    exported_at: new Date().toISOString(),
    data: {
      personal_information: {
        email: user.email,
        first_name: user.first_name,
        last_name: user.last_name,
      },
      account_status: {
        verified: user.verified,
        verified_at: user.verified_at,
        submitted_at: user.submitted_at,
        marketing_consent: user.gdpr_marketing_consent,
      },
    },
  });
});

Export Format: JSON (machine-readable, GDPR compliant)

User Flow:

  1. User clicks "Export My Data" in dashboard
  2. Frontend requests export via authenticated API
  3. Oracle-bridge fetches plaintext PII from database
  4. User downloads JSON file

Opt-In by Default:

  • Default: gdpr_marketing_consent = true
  • Rationale: User registers to participate in DAO activities (legitimate interest)
  • User can opt-out anytime

Alternative (Opt-Out by Default):

  • Default: gdpr_marketing_consent = false
  • Requires explicit consent checkbox during registration
  • More conservative, lower marketing reach

Current Implementation: Opt-in (can be changed via config).

Canister Storage ​

rust
pub struct IndividualRecord {
    pub id: String,
    pub email_encrypted: String,
    pub first_name_encrypted: String,
    pub last_name_encrypted: String,
    pub email_hash: String,
    pub verified: bool,
    pub verified_at: Option<u64>,
    pub submitted_at: u64,
    pub recovery_key_encrypted: String,
    pub encryption_key_id: String,
    pub gdpr_marketing_consent: bool,  // ✅ Tracked in canister
    pub gdpr_deleted: bool,
    pub gdpr_deleted_at: Option<u64>,
}

Database Storage ​

sql
-- individuals table
gdpr_marketing_consent BOOLEAN NOT NULL DEFAULT TRUE,
gdpr_deleted BOOLEAN NOT NULL DEFAULT FALSE,
gdpr_deleted_at TIMESTAMP NULL

User Flow:

  1. User clicks "Unsubscribe from Marketing" in dashboard or email footer
  2. Frontend calls consent withdrawal endpoint
  3. Canister updates gdpr_marketing_consent = false
  4. Canister syncs to database
  5. User excluded from future marketing campaigns

Implementation:

rust
#[update]
fn withdraw_marketing_consent(email_hash: String) -> Result<(), String> {
    STATE.with(|state| {
        let mut s = state.borrow_mut();
        let record = s.individuals.get_mut(&email_hash)
            .ok_or_else(|| "Individual not found".to_string())?;

        record.gdpr_marketing_consent = false;
        Ok(())
    })?;

    // Sync to database
    sync_consent_to_database(&email_hash, false).await?;

    Ok(())
}

Marketing Query Pattern ​

Always filter by consent:

sql
SELECT email, first_name, last_name
FROM individuals
WHERE verified = true
  AND gdpr_deleted = false           -- Exclude deleted
  AND gdpr_marketing_consent = true  -- Only consented users
ORDER BY created_at DESC;

Data Retention Policies ​

Active Users ​

Policy: Retain data indefinitely while account is active.

Definition of Active:

  • User has logged in within last 12 months
  • OR user has active memberships
  • OR user has pending governance votes

Inactive Users ​

Policy: Retain encrypted canister data, optionally purge database plaintext after 24 months of inactivity.

Rationale:

  • Canister data is encrypted (low GDPR risk)
  • Database plaintext can be purged (convenience copy)
  • User can always re-verify to restore database PII

Implementation (planned):

sql
-- Identify inactive users (no login in 24 months)
SELECT id, email, last_login_at
FROM individuals
WHERE last_login_at < NOW() - INTERVAL '24 months'
  AND gdpr_deleted = false;

-- Soft delete inactive users
UPDATE individuals
SET
    gdpr_deleted = true,
    gdpr_deleted_at = NOW(),
    gdpr_marketing_consent = false
WHERE last_login_at < NOW() - INTERVAL '24 months'
  AND gdpr_deleted = false;

Deleted Users ​

Policy: Soft delete (retain for audit purposes), optionally hard delete after 90 days.

Soft Delete Period: 90 days (allows user to undo deletion)

Hard Delete (optional):

sql
-- Hard delete users deleted more than 90 days ago
DELETE FROM individuals
WHERE gdpr_deleted = true
  AND gdpr_deleted_at < NOW() - INTERVAL '90 days';

Recommendation: Consult legal counsel before implementing hard deletes.


Audit Trail ​

Database Audit Log ​

Table: audit_log

Captures:

  • All sync operations (SYNC, MANUAL_RESYNC)
  • All deletions (DELETE)
  • All consent changes (UPDATE)
  • All data exports (EXPORT)

Schema:

sql
CREATE TABLE audit_log (
    id SERIAL PRIMARY KEY,
    action VARCHAR(50) NOT NULL,           -- 'SYNC', 'DELETE', 'EXPORT', etc.
    table_name VARCHAR(50) NOT NULL,       -- 'individuals', 'addresses', etc.
    record_id UUID NULL,                   -- Affected record ID
    user_role VARCHAR(50) NULL,            -- 'canister', 'admin', 'user'
    ip_address VARCHAR(45) NULL,           -- IP address (if applicable)
    details JSONB NULL,                    -- Additional context
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

Example Entries:

sql
-- User deletion
INSERT INTO audit_log (action, table_name, record_id, user_role, details)
VALUES (
    'DELETE',
    'individuals',
    '550e8400-e29b-41d4-a716-446655440000',
    'user',
    '{"reason": "User requested deletion via dashboard", "email_hash": "2c26b46b68ffc68f..."}'
);

-- Marketing consent withdrawal
INSERT INTO audit_log (action, table_name, record_id, user_role, details)
VALUES (
    'UPDATE',
    'individuals',
    '550e8400-e29b-41d4-a716-446655440000',
    'user',
    '{"field": "gdpr_marketing_consent", "old_value": true, "new_value": false}'
);

Canister Audit Log ​

Stored in canister stable memory:

rust
pub struct AuditEntry {
    pub timestamp: u64,
    pub action: String,          // "DELETE", "UPDATE", "ACCESS"
    pub user_id: String,
    pub email_hash: String,
    pub ip_hash: Option<String>,
    pub details: String,
}

Example:

rust
// Log deletion
let audit_entry = AuditEntry {
    timestamp: ic_cdk::api::time(),
    action: "DELETE".to_string(),
    user_id: record.id.clone(),
    email_hash: email_hash.clone(),
    ip_hash: Some(ip_hash),
    details: "User requested deletion via dashboard".to_string(),
};

STATE.with(|state| {
    state.borrow_mut().audit_log.push(audit_entry);
});

Audit Retention ​

Database: 90 days minimum (configurable) Canister: 1 year minimum (limited by stable memory size)

Rationale: GDPR requires audit trails for compliance verification.


Compliance Procedures ​

Procedure 1: User Deletion Request ​

Trigger: User clicks "Delete My Account" in dashboard.

Steps:

  1. Confirm Identity: Require password re-entry
  2. Display Impact: Show user what data will be deleted
  3. Grace Period: Offer 30-day grace period before permanent deletion
  4. Canister Soft Delete: Mark gdpr_deleted = true, zero out encrypted PII
  5. Database Sync: Propagate deletion to database
  6. Marketing Exclusion: Remove from all email lists
  7. Audit Log: Record deletion request
  8. Confirmation Email: Send deletion confirmation to user

Timeline: Immediate soft delete, optional hard delete after 90 days.

Procedure 2: Data Access Request ​

Trigger: User clicks "Export My Data" or sends email to privacy@helloworlddao.com.

Steps:

  1. Verify Identity: Require password or email verification
  2. Gather Data:
    • Canister encrypted PII (user decrypts in browser)
    • Database plaintext PII (via API endpoint)
    • Audit log entries (related to user)
  3. Format Export: Generate JSON file
  4. Deliver: Provide download link (expires in 24 hours)
  5. Audit Log: Record data export
  6. Timeline: Within 30 days (GDPR requirement)

Trigger: User clicks "Unsubscribe" in email footer or dashboard.

Steps:

  1. One-Click Unsubscribe: No password required (email link token)
  2. Canister Update: Set gdpr_marketing_consent = false
  3. Database Sync: Propagate to database
  4. List Removal: Remove from active marketing campaigns
  5. Confirmation: Show "You've been unsubscribed" page
  6. Audit Log: Record consent withdrawal
  7. Timeline: Immediate (within 1 hour)

Procedure 4: Data Breach Response ​

Trigger: Database breach detected (unauthorized access to plaintext PII).

Steps:

  1. Immediate: Disconnect database, revoke credentials
  2. Assess Impact: Identify affected users (audit log)
  3. Notify Users: Email notification within 72 hours (GDPR requirement)
  4. Notify Authorities: Report to supervisory authority within 72 hours
  5. Remediation: Patch vulnerability, rebuild database from canister
  6. Post-Mortem: Document incident, update security procedures
  7. Timeline: 72 hours for notification

Important: Canister breach has lower impact (data is encrypted).


Technical Safeguards ​

Encryption ​

Canister Storage:

  • AES-256-GCM (Authenticated Encryption)
  • User-controlled keys (zero-knowledge)
  • Recovery keys encrypted with master key

Database Storage:

  • Plaintext (by design, for operational use)
  • Database credentials in environment variables (not in code)
  • SSL/TLS for all connections (planned)

Access Controls ​

Canister:

  • Only user with password can decrypt their PII
  • Admin cannot access encrypted PII without user cooperation

Database:

  • Role-based access (SELECT, INSERT, UPDATE only)
  • No DELETE permission (soft delete only)
  • Audit log for all access

Network Security ​

HTTP Outcalls:

  • HTTPS only (TLS 1.3)
  • Ed25519 signature authentication
  • Timestamp validation (5-minute window)

Database:

  • Firewall: Only oracle-bridge can connect
  • No public access
  • IP allowlist (planned)

Compliance Checklist ​

Pre-Launch ​

  • [x] User consent obtained during registration
  • [x] Privacy policy displayed and accepted
  • [x] Soft delete implemented (canister + database)
  • [x] Marketing consent tracking implemented
  • [x] Audit log tables created
  • [ ] Data export endpoint implemented (planned)
  • [ ] Deletion grace period implemented (planned)
  • [ ] Legal review of privacy policy (pending)

Ongoing ​

  • [ ] Annual GDPR compliance audit
  • [ ] Privacy policy review (quarterly)
  • [ ] Data retention policy enforcement (automated)
  • [ ] Audit log review (monthly)
  • [ ] User education (dashboard tooltips)

Post-Incident ​

  • [ ] Breach notification procedures documented
  • [ ] Supervisory authority contact information
  • [ ] User notification templates
  • [ ] Post-mortem report template

Known Gaps and Future Work ​

Gaps (Epic 2.5) ​

  1. No Data Export Endpoint: User cannot self-serve export yet (manual process)
  2. No Deletion Grace Period: Deletion is immediate (no undo)
  3. No Automated Retention: Inactive user purge is manual (not automated)
  4. No SSL/TLS Enforcement: Database connections use SSL but not enforced
  5. No Privacy Dashboard: User cannot view audit log of their data access

Future Enhancements (Epic 3) ​

  1. Differential Privacy: Add noise to analytics queries
  2. Federated Learning: ML models without raw PII access
  3. Homomorphic Encryption: Query encrypted database directly
  4. Multi-Party Computation: Admin access requires multiple approvals

This document is not legal advice. Consult with qualified legal counsel to ensure GDPR compliance for your specific jurisdiction and use case.

Last Review: 2025-11-25 Next Review: 2026-02-25 (Quarterly)



Change Log ​

VersionDateAuthorChanges
1.02025-11-25SystemInitial GDPR compliance documentation

Hello World Co-Op DAO