User guide

Privacy & Data Rights

Looking for what it does rather than how to use it? Read the Privacy and data rights overview .

This guide covers the data-subject request mechanisms in Solidlio: getting a copy of your personal data, having it erased, and managing consent. It is written for three readers — the person exercising a right, the platform administrator who carries out erasures, and the implementer who needs to know exactly what happens to which records.

Nothing here is legal advice. It describes the mechanisms the product provides so that you and your counsel can decide how they map onto your obligations.


What it is

Solidlio lets any signed-in person request a machine-readable copy of the personal data held about them, withdraw consents, and request erasure of their personal data. Platform administrators work the resulting queue, and every request and action is recorded in the audit log.

The data subject files their own request; a platform administrator carries out an erasure. Neither can do it alone.


Concepts

ConceptWhat it is
Data subjectThe person the data is about. Always a Person record — Solidlio has no separate contact identity.
Export requestA request for a copy of your personal data. Produces a downloadable JSON or CSV file.
Erasure requestA request to erase your personal data. Carries a 30-day cooling-off period during which you can cancel.
Cooling-off period30 days from the erasure request, during which the subject may still cancel. Distinct from the statutory deadline.
Statutory deadline (dueAt)30 days from the request, stored on the row. Requests past it are flagged as overdue on the platform queue.
Erasure policyThe published, model-by-model contract of what is deleted, what is anonymised and what is retained. Readable by any signed-in person.
AnonymisationThe record survives; the identifiers on it are removed. Used where a record has an independent purpose (a paid invoice, an audit entry).
Consent recordA PrivacyConsent row: categories, purpose, legal basis, the exact text agreed to, policy version, IP, timestamp and region.
Tenant purgeDeletion of every stored object owned by an organization or account. Offboarding, not a data-subject right.
RegionCA, US or EU. Recorded on every request and consent, and used to group the platform compliance queue.

Roles and permissions

The hierarchy is platform administrator > MSP administrator > MSP technician > organization administrator > power user > CUSTOMER`.

ActionCUSTOMERpower userorganization administratorMSP_TECHMSP administratorplatform administrator
Request an export of your own data
Download your own export
Download someone else’s export
Request erasure of your own data
Cancel your own erasure request
Record / withdraw your own consent
Read the erasure policy
View the platform compliance queue
Close a cooling-off window (release)
Execute an erasure
Purge a tenant’s stored objects

Two points are deliberate and worth understanding.

The subject’s own routes carry no role floor. A person exercising a data right is frequently a CUSTOMER. Any floor at organization administrator or above would block exactly the people the feature exists for.

No tenant administrator can execute an erasure. A Person can hold memberships in several organizations across several MSP accounts, so there is no single tenant whose administrator has authority over that identity. Letting an MSP administrator fire it would let one tenant destroy a record another tenant depends on. Executing is restricted to platform administrators.

Platform administrators cannot download a subject’s export. The download route authorises against the request row’s own dataSubjectId, not against a staff session. A right-of-access artefact is a complete dump of one person’s personal data; a staff-readable copy would be an unlogged route to everyone’s data. Staff who need to act on a request use the audited erasure route instead.


Walkthrough — get a copy of your data

  1. Go to Privacy in the portal sidebar (/user/privacy).
  2. Under Get a copy of your data, choose a format:
    • JSON — complete and machine-readable. Use this for portability.
    • CSV — one multi-section table, openable in a spreadsheet.
  3. Select Request my data.
  4. The request is queued. A background job sweeps pending requests every five minutes, builds the file, stores it, and emails you a download link.
  5. The page polls while a request is in flight, so the status updates without a reload. When it completes, a Download button appears in the table.
  6. The emailed link carries a 256-bit single-purpose token and works without a Solidlio session. From inside the app, your session identifies you instead.

Download links stay live for 7 days. After that the link returns 410 Gone and you request a new copy.

Only one export can be in flight at a time; requesting a second while one is PENDING or PROCESSING returns 409.


Walkthrough — request erasure

  1. Go to Privacy → Erase my personal data.
  2. Select See exactly what is erased and what is kept to read the full model-by-model policy before deciding.
  3. Select Request erasure, optionally give a reason (up to 500 characters), and confirm.
  4. The request is created with a 30-day cooling-off period. Your account keeps working normally during it.
  5. To change your mind, return to the same page and select Cancel this request any time before the cooling-off period ends.

After the cooling-off period elapses, a platform administrator carries out the erasure. You cannot cancel after that point — the cancel route returns 409 with Cooling off period has passed. Request cannot be cancelled.


Walkthrough — carry out an erasure (platform administrators)

Go to Platform → Privacy → Erasure Requests (/platform/privacy, “Erasure Requests” tab). Erasure is two separate actions.

Step 1 — Close cooling-off

Select Close cooling-off on the request. This is administrative and deletes nothing: it sets scheduledFor to now and leaves the request SCHEDULED, which is the state the executor accepts.

If the subject’s 30-day window is still open, the UI asks you to confirm that you are cutting it short, and the API requires waiveCoolingOff: true. Without it the request returns 400. Both the waiver and whether the window was still active are written to the platform audit log under privacy.erasure.release.

Step 2 — Execute erasure

Select Execute erasure. The confirmation dialog states what will be deleted and what will be retained. This step is irreversible.

The button only appears once the cooling-off window has actually elapsed, because the executor rejects it otherwise.

On success the response carries a coverage report: rows affected per model, storage objects deleted, and access tokens blacklisted. The same report is stored on the request row in coverage and written to the audit log.


What the export contains

Every query in the collector is keyed on the data subject’s own person id.

SectionContents
metaExport id, subject id, format, region, requested/generated timestamps, scope notice.
profileName, email, phone, mobile, department, timezone, locale, avatar, SSO provider, MFA enabled, skill level, verification and login timestamps.
organizationMembershipsOrganization, role, primary flag, active flag, accepted date.
accountMembershipsAccount, role, accepted date.
rolesAssigned roles, scope, assignment and expiry dates.
ticketsTickets you created, requested or were assigned — scalar fields only.
ticketCommentsComments you wrote, with the ticket id.
timeEntriesDate, duration, description, billable and approved flags.
notificationsType, title, message, link, read state.
notificationPreferenceYour channel settings, quiet hours and timezone.
sessionsDevice, browser, OS, IP, city/region/country, activity and expiry timestamps.
loginHistoryUp to 5,000 sign-ins: success, method, IP, user agent, city, country.
auditLogsUp to 5,000 of your own audit entries: action, entity, IP, user agent, region.
consentsPrivacy consents: categories, basis, purpose, version, given/revoked/expiry.
emailConsentsCASL email consents matched by person id and by your address.
privacyRequestsYour own export and erasure requests, including statutory deadlines.

Deliberate omissions

These are excluded on purpose, and each would be a defect if included:

  • Other people’s words. A reply someone else wrote on your ticket is their personal data. The export includes your own comments, never the thread around them.
  • Counterparty identifiers on shared records — the other creator, assignee or requester on a ticket you touched.
  • AuditLog.changes and Notification.data — free-form JSON that routinely names other people and other records.
  • Credentials. Password hash, MFA secret, MFA recovery codes and the SSO subject identifiers. An export is a file that gets emailed around.

What erasure actually does

The contract has 38 entries: 22 erased, 9 anonymised, 7 retained.

The design, in one paragraph

Person is referenced by roughly a hundred foreign keys. Deleting it would either cascade away a tenant’s business records and financial books, or fail outright on restrict-mode relations. The executor’s job is therefore to handle every table that holds an independent copy of personal data — an address, an IP, a device token, an OAuth refresh token, free text the subject wrote — because severing the Person row does nothing to those.

Erased (rows deleted)

GroupModels
CredentialsPasswordResetToken, MagicLink, EmailVerification
SessionsUserSession, LoginHistory
DevicesPushSubscription, NotificationDevice
Third-party accessCalendarSync, CalendarSyncLog, CalendarWebhookSubscription, Integration (user-scoped only), TeamsUserMapping, ScimGroupMember
Personal contentNotification, NotificationPreference, SavedView, TimeTimer, ShoppingCart, SolConversation (messages and attachments cascade), AIInteraction
Email addressesEmailConsent, EmailSuppression

Organization- and account-scoped integrations are tenant infrastructure and are left alone.

Anonymised (row survives, identity severed)

ModelWhat happens
PersonEmail becomes erased-{id}@erased.invalid; name becomes “Erased User”; phone, mobile, department, avatar, Stripe customer id and hourly rate cleared; password hash, MFA secret, recovery codes and all SSO ids nulled; account deactivated.
CalendarAttendeepersonId nulled — the attendance belongs to someone else’s meeting.
AIUsagepersonId nulled; the token counts a tenant was billed for are kept.
SavedPaymentMethodpersonId nulled and the instrument deactivated.
PrivacyConsentIP and user agent cleared; the consent record kept as evidence that consent was given and when.
AuditLogRows kept; IP, user agent and the denormalised actorEmail on the subject’s own rows cleared. actorRole is kept — a role name is not personal data.
OrganizationMembershipDeactivated, not deleted.
PersonRoleDeactivated, so no permission survives.
PrivacyExportRequestDownload token, URL, storage key and expiry scrubbed — this revokes every outstanding download link.

Retained, with reasons

RecordsWhy
Invoices, invoice lines, payments, expensesFinancial records under statutory retention. Identity is severed via the anonymised Person.
Tickets, comments, attachments, time entriesTenant-owned service records; a ticket thread is other participants’ data too.
Projects, change requests, quotesTenant-owned business records.
AuditLog, PlatformAuditLogThe audit trail, including the record of this erasure.
ApiKeyOrganization infrastructure. Revoking a tenant’s integrations because an employee left is that tenant’s availability decision, not a privacy one.
PrivacyErasureRequestThe proof that the erasure was requested and carried out.
TokenBlacklistWritten by the erasure to invalidate outstanding tokens; deleting it would un-revoke them.

Beyond the database

  • Object storage. Every object under the subject’s key prefix in the exports container is deleted, and the count is reported.

Erasure and audit immutability

These two obligations genuinely conflict, and the product resolves it explicitly rather than pretending it does not.

An audit log a subject can delete is not an audit log: it would let a person erase the evidence of their own actions, and it would destroy the record of the erasure itself.

The resolution: audit rows are retained, and the identity in them is severed the same way as everywhere else. AuditLog.personId points at the anonymised Person, so the log keeps saying “this actor did this” while no longer naming a human. The free-text ipAddress and userAgent columns on the subject’s own audit rows are cleared, because those are independent copies of personal data that severing the Person row does not reach — including actorEmail, a denormalised copy of the address stamped on the row at write time, which is overwritten with the same erased-{id}@erased.invalid sentinel used on the Person row. actorRole is kept: a role name is not personal data, and it is the part of the trail that explains why the action was permitted.

What survives is the action, the entity and the timestamp — enough to reconstruct the history, not enough to re-identify.

The same reasoning applies to financial records: the invoice survives, the person named on it does not.


Storing the text and version is what makes the record evidential later.

Withdrawal is enforced, not just recorded. The email service consults PrivacyConsent before every non-transactional send:

  • No marketing consent record at all — this gate stays silent and the CASL EmailConsent check remains the authority. Absence is not refusal.
  • Latest marketing consent is revoked or expired — the send is blocked, even if a CASL consent is still active. An explicit choice by the subject wins over an older record.
  • A live marketing consent — passes through to the CASL check, which must still succeed on its own.

“Latest” is by givenAt, so opting back in after a withdrawal works.

Withdrawal never affects transactional mail — TICKET, INVOICE and AUTH categories always send. That includes the response to a data-subject request itself, which is dispatched with skipComplianceCheck so it reaches even a suppressed address.


The statutory clock

Both request models carry dueAt, set to 30 days after creation and stored on the row rather than recomputed, so every reader agrees on one deadline.

dueAt is not the same thing as an erasure’s scheduledFor. scheduledFor is the subject’s 30-day window to change their mind; dueAt is the deadline for Solidlio to respond.

Where it shows up:

  • Platform stats report overdueRequests and failedRequests per region.
  • The dashboard shows a red banner when either is non-zero.
  • Request lists carry dueAt and a precomputed isOverdue.
  • The export job sweeps in dueAt order, so a request near its deadline is never queued behind a fresher one.

Migration 20260728000000 backfills dueAt from createdAt + 30 days for existing rows, so an old request shows as already overdue rather than being given a fresh month.


Request lifecycles

Export

PENDING ──claim──> PROCESSING ──success──> COMPLETED ──7 days──> (link expires, 410)
   ▲                    │
   │                    └──throw──> FAILED ──admin retry──> PENDING
   │                    │
   └──stale reclaim─────┘  (PROCESSING with no progress for 30 min)

Every hop is reachable: PENDING and PROCESSING from the job, COMPLETED and FAILED from the job, PENDING again from Retry on the platform dashboard — which accepts both FAILED and stalled PROCESSING.

Erasure

SCHEDULED ──subject cancels (within 30d)──> CANCELLED

    ├──cooling-off elapses OR admin "Close cooling-off"──> SCHEDULED (scheduledFor in past)
    │                                                            │
    │                                                    admin "Execute erasure"
    │                                                            │
    │                                                            ▼
    └────────────────────────────────────────────────> IN_PROGRESS ──> COMPLETED

                                                                 └──throw──> FAILED

                                                            re-executable ◀─────┘

FAILED and IN_PROGRESS are both re-executable.


How the export job works

SettingValue
Schedule*/5 * * * * — every 5 minutes
Batch size25 requests per run
Timeout10 minutes
Retry attempts2 (job-level), 3 max claim attempts per request
Stale reclaimA PROCESSING claim older than 30 minutes is returned to PENDING
Link lifetime7 days

Each run, in order:

  1. Reclaim stalled claims. A request PROCESSING for more than 30 minutes goes back to PENDING, unless it has already burned 3 attempts — in which case it is marked FAILED with a readable reason rather than looping forever.
  2. Sweep up to 25 PENDING requests, ordered by dueAt then createdAt.
  3. Claim each with a conditional update.
  4. Collect, serialise, store under the tenant’s key prefix — the same prefix the erasure route deletes, which is what makes the export erasable.
  5. Mint a 256-bit download token and a 7-day expiry.
  6. Email the subject the link.

Failure is loud. Storage unreachable, or anything else, marks the request FAILED with the reason and increments the job’s error count. It is never left silently PENDING, and never marked COMPLETED with no bytes behind it. A failed notification email is counted separately — the artefact still exists and the subject can reach it in-app, but the run does not report itself clean.


Tenant purge

This is offboarding, not a data-subject right.

Platform administrators only, and it requires a confirm field equal to the id of the tenant being purged. That is not ceremony: the body is otherwise three optional strings, and a mistyped organizationId would irreversibly delete a different tenant’s objects. The prefix builder fails closed on a blank id or a path separator, so a malformed scope cannot widen into “delete the container”.


Configuration

VariableUsed byEffect if unset
AZURE_STORAGE_CONNECTION_STRINGstorageFalls back to local disk (SOLIDLIO_STORAGE_DIR, default /tmp/solidlio-storage). If set but broken, the package throws and the request is marked FAILED.

Plan tiers

Data-subject rights are not plan-gated. There is no requirePlan, planGate or entitlement check on any privacy request, on any tier. A statutory right that a billing tier can switch off would not be a right.


Troubleshooting

Message / symptomCause and fix
An export request is already in progressOne export at a time. Wait for the current one, or retry it from the platform queue if it is stuck.
An erasure request is already pendingYou already have a PENDING, SCHEDULED or IN_PROGRESS request. Cancel it first if you want to start over.
Export is not ready (status: pending)The job has not run yet. It sweeps every 5 minutes.
This export link has expired. Request a new export to download your data again. (410)Links live 7 days. Request a new export.
404 on a download you believe is yoursThe route returns a uniform 404 for both “no such export” and “not yours”, so a prober cannot confirm another person’s request exists. Check you are signed in as the subject.
Cooling off period has not elapsed. The subject can still cancel this request. (409)Use Close cooling-off first — and only with a deliberate waiver if the window is still open.
Cooling off period has passed. Request cannot be cancelled. (409)Past the 30 days, cancellation is closed.
Executing a data erasure requires platform administrator access (403)Expected for every tenant role including MSP administrator. See §3.
Cannot release a request with status COMPLETEDAlready executed. Nothing further to do.
This request is still inside the subject's 30-day cooling-off period. (400)Pass waiveCoolingOff: true, or wait for the window to close.
Can only retry failed or stalled export requests (status: COMPLETED)Completed exports are not retried. Ask the subject to request a fresh one.
confirm must equal the id of the tenant being purgedDeliberate guard on the tenant purge. Supply the exact tenant id.
Abandoned after 3 attempts — each claim stalled without completing.The job claimed the request three times without finishing. Investigate storage or job health before retrying.
Data subject … no longer existsThe person row was hard-deleted outside the erasure flow. The request cannot be fulfilled.
Marketing email not arrivingCheck for a withdrawn PrivacyConsent (§10) as well as CASL consent and the suppression list.

Limits and known behaviour

  • One export at a time per person; one open erasure request at a time.
  • Exports are point-in-time. A file generated on Tuesday does not reflect Wednesday’s changes.
  • Export caps. Audit log and login history are each capped at the 5,000 most recent rows.
  • CSV is a flattened rendering. It is sectioned rather than one rectangle. JSON is the complete form; use it for portability.
  • Erasure requires a platform administrator. It cannot be fully self-service, and a tenant administrator cannot carry it out. Plan operationally for this.
  • Erasure is not instant. 30-day cooling-off, then an administrator action.
  • Audit rows and financial records survive by design (§9). If your legal position requires their deletion, the product does not do that.
  • The tenant purge deletes stored objects only — blobs under the tenant’s key prefix. It does not delete database rows.
  • Cross-border attempt tracking is a record, not a control. The CrossBorderAttempt table and its dashboard tab report attempts; the regional routing that blocks them is a separate mechanism.
  • PrivacyConsent enforcement covers email. Withdrawal is checked on every non-transactional send. It is not currently consulted by any other processing surface.
  • Regions. PrivacyConsent, export and erasure requests accept CA, US and EU. The platform stats dashboard aggregates CA and US only.

Questions this guide did not answer?

Ask us. You will get a reply from someone who uses the product every day.

Book a demo Contact us

A 30-minute walkthrough against your own workflow. No slides.