Data retention & privacy (GDPR)
How long the CRM keeps audit logs, login attempts, billing events and recordings — plus tenant data export, the 30-day erasure countdown and who can do what.
Data retention & privacy (GDPR)
Zodiac Tech CRM stores personal data (leads, contacts, calls, recordings, messages) on behalf of the business that owns the workspace. This page is the operator's view: what is kept, for how long, which jobs delete it, and how an export or erasure request is handled.
Who this is for
- Workspace owners and admins answering a data request.
- Super admins and developers configuring retention on a live install.
Difficulty: Medium · Time: 20 min · Prerequisites: Admin access for the workspace-level flows; shell access for the commands.
What is kept, and for how long
| Data | Retention | Deleted by |
|---|---|---|
| Audit log entries (admin and sensitive actions) | 180 days (default) | leadhub:purge-audit-logs, daily at 03:00 |
Login attempts (login_attempts) |
30 days (default) | leadhub:purge-audit-logs, daily at 03:00 |
| Processed billing events (idempotency records) | 90 days (default) | leadhub:purge-audit-logs, daily at 03:00 |
| Call recording audio | Per the install's recording retention | leadhub:purge-recordings, daily at 03:15 |
| Leads, contacts, messages, call metadata | Until the workspace deletes them or executes erasure | App actions / erasure job |
Retention windows are configurable per install with the environment variables LH_AUDIT_RETENTION_DAYS, LH_LOGIN_ATTEMPT_RETENTION_DAYS and LH_BILLING_EVENT_RETENTION_DAYS (see config/leadhub.php). Change them deliberately — shortening audit retention weakens your own incident forensics.
Recordings keep their metadata leadhub:purge-recordings deletes the audio file and clears the stored path, but keeps the call record itself with status = expired. The history that a call happened survives; the audio does not.
The deletion jobs (and how to dry-run them)
Always preview before purging:
php artisan leadhub:purge-audit-logs --dry-run
php artisan leadhub:purge-recordings --dry
php artisan leadhub:execute-erasure --dry-run
leadhub:purge-audit-logs {--days=} {--login-days=} {--billing-events-days=} {--dry-run}— overlays for a one-off run with different windows than the configured defaults.leadhub:purge-recordings {--dry}— audio-only cleanup; metadata stays.leadhub:execute-erasure {--dry-run}— executes scheduled tenant deletions whose 30-day countdown has passed. Runs daily at 04:00.
Tenant data export (access requests)
When someone asks "what do you hold about me / us":
- The workspace owner opens Admin → Data & Privacy (
/admin/privacy). - They press the export button. The CRM builds a ZIP of the tenant's data as JSON.
- The download link is valid for 24 hours (
ExportTenantDataJob::DOWNLOAD_TTL_SECONDS = 86400). The build is capped atLH_EXPORT_MAX_BYTES(default 2 GB) — very large workspaces may need a narrower export. - Hand the file over through a secure channel. It is not encrypted at rest by the portal; treat it as sensitive.
Erasure requests (the 30-day countdown)
The erasure flow is deliberately slow — deleting a workspace is irreversible:
- The owner schedules deletion on Admin → Data & Privacy (
/admin/privacy). This setsdeletion_requested_atanddeletion_scheduled_at— a 30-day cool-off (TenantErasureService::COOL_OFF_DAYS = 30). - During the countdown the subscription status becomes
pending_deletion: regular sign-in is blocked, but the owner can still reach/admin/privacy(to cancel) and/admin/billing. - The owner can cancel the deletion at any time during the 30 days — do this the moment they change their mind, not on day 31.
- After the countdown,
leadhub:execute-erasure(daily, 04:00) hard-deletes the tenant's data.
Backups are not erased by the erasure job The erasure job removes live data and rows. Existing backup archives still contain a copy of the data from their date. If that matters for your compliance position, prune old backups after an erasure per your own policy — see Weekly & monthly reviews.
Who can do what
| Action | Who |
|---|---|
| Export own workspace data | Workspace owner (Admin → Data & Privacy) |
| Schedule / cancel workspace deletion | Workspace owner |
| See deleted-workspace flows run | Super admin (nightly job output, log) |
| Run purge/erasure commands manually | Developer / super admin with shell access |
A suspended workspace (billing) is a different state from pending_deletion: suspension redirects users to /admin/subscription-required; pending deletion lets the owner reach only the privacy and billing pages.
A GDPR request, step by step
- Log the request — date, requester, what they asked for (access / erasure / both).
- Access: run the export (above), deliver it securely, note the delivery in your records.
- Erasure: if the request is for the whole workspace, schedule deletion and tell the requester the date the data will go. If it is for an individual contact within a workspace, handle it as a data change in the CRM (delete/anonymise the contact record) and record what you did.
- Verify: after the countdown passes, confirm the tenant is gone (the erasure job ran; check the daily job's log line and that the workspace no longer appears).
- Close the loop: reply to the requester with what was done and when.
Common mistakes
| Mistake | Why it hurts | Do this instead |
|---|---|---|
| Erasing before the requester is sure | Irreversible | Explain the 30-day countdown is available — use it |
| Forgetting backup archives after erasure | Data still exists in old zips | Prune backups per policy after an erasure |
| Shortening audit retention to "clean up" | Destroys the record of who did what | Audit retention is your forensics — keep 180 days unless policy says otherwise |
| Emailing exports as plain attachments | The ZIP is not encrypted by the portal | Use a secure transfer method |
| Running purge commands with changed windows so they are never scheduled again | Silent drift between policy and reality | Keep the scheduled defaults and document any override |
Technical details
- Erasure:
TenantErasureService(COOL_OFF_DAYS = 30); fieldsdeletion_requested_at/deletion_scheduled_at; blocked sign-in via the subscription middleware chain (EnforceNotSuspended+EnforceSubscription, state fromTenantSubscriptionState). - Export:
ExportTenantDataJob(JSON in a ZIP; 24-hour link;LH_EXPORT_MAX_BYTEScap, default 2 GB). - Purge:
PurgeAuditLogs(chunked deletes; 180/30/90-day defaults),PurgeExpiredRecordings(audio deleted, row →expired, path nulled). - Billing idempotency records (
processed_billing_events) are kept 90 days because webhook duplicates can arrive late.
Related guides
Was this page helpful?
Thanks — noted. ✦