Pro GDPR
Viewing latest docs.
Switch version: v3

GDPR

Petal Pro ships with a built-in GDPR compliance system covering two user rights: the right to data portability (export) and the right to erasure (account deletion). Both are exposed to users through Settings > Your Data and processed by PetalPro.Accounts.GDPR.

Enabling GDPR mode

The system is gated behind the :gdpr_mode config key. When disabled, the Settings > Your Data page is hidden from users.

elixir
# config/config.exs
config :petal_pro, :gdpr_mode, true

Check programmatically with:

elixir
PetalPro.gdpr_mode()
# => true

Data export

What users see

Users visit Settings > Your Data (/app/users/your-data) and click “Download my data”. The browser receives a JSON file generated on the fly.

What’s included

PetalPro.Accounts.GDPR.export_user_data/1 assembles a single map with these top-level keys:

Key Contents
exported_at ISO 8601 timestamp
user Name, email, avatar, role, onboarding status, timestamps
organizations Org name, slug, membership role, join date
notifications Up to 1 000 most recent (truncated flag if capped)
notification_preferences Per-type in-app/email toggles
billing Masked provider customer ID, subscription history
posts Post titles, slugs, categories, publish dates
files File names and URLs
feedback User’s own submitted feedback items
ai_usage AI call log entries — provider, model, token counts, cost
activity_log Audit log entries attributed to the user
changelog_interactions Changelog reads and likes

Security-sensitive data is deliberately excluded: password hashes, auth tokens, TOTP secrets, and PINs are never exported. Billing provider IDs (Stripe customer IDs) are masked to the last four characters.

Generating the JSON

elixir
user = Repo.get!(User, user_id)

# Returns a map
data = PetalPro.Accounts.GDPR.export_user_data(user)

# Returns a pretty-printed JSON string
json = PetalPro.Accounts.GDPR.generate_export_json(user)

Adding custom data to exports

When you add a new schema that associates data with a user, update export_user_data/1 to include it. The function builds a map — just add a new key:

elixir
def export_user_data(%User{} = user) do
  user = Repo.preload(user, [:memberships, :customer])

  %{
    exported_at: DateTime.to_iso8601(DateTime.utc_now()),
    user: export_user_profile(user),
    # ... existing keys ...
    survey_responses: export_survey_responses(user)  # your addition
  }
end

defp export_survey_responses(user) do
  MyApp.Surveys.SurveyResponse
  |> where(user_id: ^user.id)
  |> Repo.all()
  |> Enum.map(fn r ->
    %{
      question: r.question,
      answer: r.answer,
      submitted_at: r.inserted_at
    }
  end)
end

Account deletion

How it works

Deletion is a two-step process:

  1. The user clicks “Delete my account” in Settings > Your Data and confirms.
  2. PetalPro.Accounts.GDPR.can_delete_user?/1 runs a guard check — a user cannot be deleted if they are the sole admin of any organization.
  3. If the check passes, an Oban job (GDPRDeletionWorker) is enqueued. The user is informed their deletion is scheduled and the session ends shortly after.
  4. The worker calls execute_data_deletion/1 which runs inside a single database transaction.

What execute_data_deletion/1 does

The deletion cascade runs in this order:

  1. Logs the deletion — audit record created before anything is anonymized
  2. Deletes AI call logs — contain sensitive prompt/response data
  3. Deletes file records — storage object cleanup is a separate concern
  4. Anonymizes feedbackuser_id set to nil, content kept
  5. Anonymizes postsauthor_id set to nil, posts shown as “Deleted User”
  6. Deletes notifications — recipient notifications deleted; sender references nullified
  7. Deletes notification preferences
  8. Deletes changelog interactions — reads and likes
  9. Deletes auth data — tokens, PINs, TOTP configs
  10. Removes org memberships and invitations
  11. Anonymizes audit logsuser_id nullified, metadata cleared (record kept for compliance)
  12. Anonymizes billing customer — email replaced, user_id cleared (record kept for tax/legal)
  13. Soft-deletes the user — name set to “Deleted User”, email replaced with deleted-{uuid}@deleted.local, deleted_at timestamp set
  14. Removes marketing email contact — Resend contact deleted outside the transaction; failure is logged but does not roll back the DB deletion

Adding custom deletion logic

Add cleanup for your new schema inside the transaction in execute_data_deletion/1. Choose the right strategy:

  • Delete the records when they contain personal data that serves no compliance purpose.
  • Nullify the foreign key when you want to keep the record but disassociate it from the user.
elixir
def execute_data_deletion(%User{} = user) do
  result =
    Repo.transaction(fn ->
      # ... existing steps ...

      # Delete records that belong to this user
      MyApp.Surveys.SurveyResponse
      |> where(user_id: ^user.id)
      |> Repo.delete_all()

      # Or anonymize by nullifying the user reference
      MyApp.Comments.Comment
      |> where(author_id: ^user.id)
      |> Repo.update_all(set: [author_id: nil])

      # ... rest of cascade ...
    end)

  result
end

The CLAUDE.md for petal_pro explicitly calls this out as a required step whenever you add a new user-associated schema: update both export_user_data/1 and execute_data_deletion/1.