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
| Concept | What it is |
|---|---|
| Data subject | The person the data is about. Always a Person record — Solidlio has no separate contact identity. |
| Export request | A request for a copy of your personal data. Produces a downloadable JSON or CSV file. |
| Erasure request | A request to erase your personal data. Carries a 30-day cooling-off period during which you can cancel. |
| Cooling-off period | 30 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 policy | The published, model-by-model contract of what is deleted, what is anonymised and what is retained. Readable by any signed-in person. |
| Anonymisation | The record survives; the identifiers on it are removed. Used where a record has an independent purpose (a paid invoice, an audit entry). |
| Consent record | A PrivacyConsent row: categories, purpose, legal basis, the exact text agreed to, policy version, IP, timestamp and region. |
| Tenant purge | Deletion of every stored object owned by an organization or account. Offboarding, not a data-subject right. |
| Region | CA, 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`.
| Action | CUSTOMER | power user | organization administrator | MSP_TECH | MSP administrator | platform 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
- Go to Privacy in the portal sidebar (
/user/privacy). - 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.
- Select Request my data.
- The request is queued. A background job sweeps pending requests every five minutes, builds the file, stores it, and emails you a download link.
- 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.
- 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
- Go to Privacy → Erase my personal data.
- Select See exactly what is erased and what is kept to read the full model-by-model policy before deciding.
- Select Request erasure, optionally give a reason (up to 500 characters), and confirm.
- The request is created with a 30-day cooling-off period. Your account keeps working normally during it.
- 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.
| Section | Contents |
|---|---|
meta | Export id, subject id, format, region, requested/generated timestamps, scope notice. |
profile | Name, email, phone, mobile, department, timezone, locale, avatar, SSO provider, MFA enabled, skill level, verification and login timestamps. |
organizationMemberships | Organization, role, primary flag, active flag, accepted date. |
accountMemberships | Account, role, accepted date. |
roles | Assigned roles, scope, assignment and expiry dates. |
tickets | Tickets you created, requested or were assigned — scalar fields only. |
ticketComments | Comments you wrote, with the ticket id. |
timeEntries | Date, duration, description, billable and approved flags. |
notifications | Type, title, message, link, read state. |
notificationPreference | Your channel settings, quiet hours and timezone. |
sessions | Device, browser, OS, IP, city/region/country, activity and expiry timestamps. |
loginHistory | Up to 5,000 sign-ins: success, method, IP, user agent, city, country. |
auditLogs | Up to 5,000 of your own audit entries: action, entity, IP, user agent, region. |
consents | Privacy consents: categories, basis, purpose, version, given/revoked/expiry. |
emailConsents | CASL email consents matched by person id and by your address. |
privacyRequests | Your 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.changesandNotification.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)
| Group | Models |
|---|---|
| Credentials | PasswordResetToken, MagicLink, EmailVerification |
| Sessions | UserSession, LoginHistory |
| Devices | PushSubscription, NotificationDevice |
| Third-party access | CalendarSync, CalendarSyncLog, CalendarWebhookSubscription, Integration (user-scoped only), TeamsUserMapping, ScimGroupMember |
| Personal content | Notification, NotificationPreference, SavedView, TimeTimer, ShoppingCart, SolConversation (messages and attachments cascade), AIInteraction |
| Email addresses | EmailConsent, EmailSuppression |
Organization- and account-scoped integrations are tenant infrastructure and are left alone.
Anonymised (row survives, identity severed)
| Model | What happens |
|---|---|
Person | Email 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. |
CalendarAttendee | personId nulled — the attendance belongs to someone else’s meeting. |
AIUsage | personId nulled; the token counts a tenant was billed for are kept. |
SavedPaymentMethod | personId nulled and the instrument deactivated. |
PrivacyConsent | IP and user agent cleared; the consent record kept as evidence that consent was given and when. |
AuditLog | Rows 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. |
OrganizationMembership | Deactivated, not deleted. |
PersonRole | Deactivated, so no permission survives. |
PrivacyExportRequest | Download token, URL, storage key and expiry scrubbed — this revokes every outstanding download link. |
Retained, with reasons
| Records | Why |
|---|---|
| Invoices, invoice lines, payments, expenses | Financial records under statutory retention. Identity is severed via the anonymised Person. |
| Tickets, comments, attachments, time entries | Tenant-owned service records; a ticket thread is other participants’ data too. |
| Projects, change requests, quotes | Tenant-owned business records. |
AuditLog, PlatformAuditLog | The audit trail, including the record of this erasure. |
ApiKey | Organization infrastructure. Revoking a tenant’s integrations because an employee left is that tenant’s availability decision, not a privacy one. |
PrivacyErasureRequest | The proof that the erasure was requested and carried out. |
TokenBlacklist | Written 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
exportscontainer 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.
Consent
Recording consent
Storing the text and version is what makes the record evidential later.
Withdrawing consent
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
EmailConsentcheck 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
overdueRequestsandfailedRequestsper region. - The dashboard shows a red banner when either is non-zero.
- Request lists carry
dueAtand a precomputedisOverdue. - The export job sweeps in
dueAtorder, 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
| Setting | Value |
|---|---|
| Schedule | */5 * * * * — every 5 minutes |
| Batch size | 25 requests per run |
| Timeout | 10 minutes |
| Retry attempts | 2 (job-level), 3 max claim attempts per request |
| Stale reclaim | A PROCESSING claim older than 30 minutes is returned to PENDING |
| Link lifetime | 7 days |
Each run, in order:
- Reclaim stalled claims. A request
PROCESSINGfor more than 30 minutes goes back toPENDING, unless it has already burned 3 attempts — in which case it is markedFAILEDwith a readable reason rather than looping forever. - Sweep up to 25
PENDINGrequests, ordered bydueAtthencreatedAt. - Claim each with a conditional update.
- Collect, serialise, store under the tenant’s key prefix — the same prefix the erasure route deletes, which is what makes the export erasable.
- Mint a 256-bit download token and a 7-day expiry.
- 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
| Variable | Used by | Effect if unset |
|---|---|---|
AZURE_STORAGE_CONNECTION_STRING | storage | Falls 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 / symptom | Cause and fix |
|---|---|
An export request is already in progress | One 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 pending | You 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 yours | The 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 COMPLETED | Already 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 purged | Deliberate 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 exists | The person row was hard-deleted outside the erasure flow. The request cannot be fulfilled. |
| Marketing email not arriving | Check 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
CrossBorderAttempttable and its dashboard tab report attempts; the regional routing that blocks them is a separate mechanism. PrivacyConsentenforcement 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 acceptCA,USandEU. The platform stats dashboard aggregatesCAandUSonly.