Eviworx
Docs

Privacy & GDPR

The privacy area bundles all data-subject rights and technical data minimization in one place: data export (Art. 15/20), anonymization (Art. 17), time-based retention and public transparency (privacy policy, imprint). Endpoints under /api/privacy; public parts under /api/public.

🛡️
Features
✓ Data export as JSON (Art. 15/20)
✓ Self-service export (1 per 24 h)
✓ Admin/DPO export for any person
✓ Export in the data subject's language
✓ Irreversible anonymization (Art. 17)
✓ Records stay complete (anonymous placeholder)
✓ Pre-check with a list of blockers
✓ Erasure hold with mandatory reason (legal hold)
✓ Erasure backlog with key figures and filters
✓ Nightly retention periods (retention_purge)
✓ Privacy policy with live periods, imprint

Endpoints

MethodEndpointPermissionResponseDescription
GET/api/privacy/export/meany sign-in (own data only)200, documentSelf-service export, at most 1 per 24 h
GET/api/privacy/export/:userIdusers.dataExport200, documentExport for any person, no quota
GET/api/privacy/users/:id/erasure-checkusers.erase200, check resultPre-check: blockers, hold and archive status
POST/api/privacy/users/:id/eraseusers.erase204, no contentAnonymization (irreversible)
POST/api/privacy/users/:id/erasure-holdusers.erase204, no contentSet/clear erasure hold
GET/api/privacy/erasure-backlogusers.eraseaudit.enterpriseView{ data, pagination }Erasure backlog as a list: overdue archived accounts and stale email contacts, with search, filters and pages
GET/api/privacy/erasure-backlog/statsusers.eraseaudit.enterpriseView{ stats, retention }Key figures of the same backlog (unfiltered) and the periods in force
GET/api/public/privacy-confignone{ retention }Live periods for the privacy policy
GET/api/public/imprintnone200, imprint dataImprint data (works before login)

Every endpoint under /api/privacy requires a signed-in person; an API key is rejected (403). The reason: data-subject rights are decisions that must stay attributable to a human — the audit entry names them. The check and backlog responses come without a wrapper, anonymization and erasure hold answer 204 with no content; an unknown person is 404.

Data Export (Art. 15/20)

The export gathers all of a person's personal data into a machine-readable JSON (Art. 20-ready) — master data, cases (tickets as customer/participant, approvals, absences, handovers, assets/licenses), communication (emails, web notifications, Teams reference) and metadata (activity/audit entries as actor, where already-redacted fields stay redacted). The response consists of a cover sheet (cover) and the data (data).

The cover sheet names the export time (exportedAt), the kind of export (exportType: SELF_SERVICE or ADMIN), the data subject (subject), the legal basis (legalBasis), notes (notes) and per category a count and a cap flag (categories with count and capped). Every source has an upper bound — 5,000 entries, 1,000 for web notifications and 500 for error telemetry. If it is reached, the cover sheet marks the category as capped: a silent truncation would be a false "complete" disclosure. If a source fails, the whole export fails with an error — for the same reason. Third-party free text about the person (an agent's description, say) is deliberately not included and is named on the cover sheet.

PathWhoLimit
GET /export/meany logged-in user, own ID ONLY1 export / 24 h; a failed run does not consume the quota
GET /export/:userIdusers.dataExport (ADMIN/DPO)any person, no 24h limit

The self-service quota counts per person: a second call within 24 hours is refused with 429 and the error code DATA_EXPORT_QUOTA_EXCEEDED; the Retry-After header names the remaining seconds, the body carries no waiting time. The reason for the limit: the export is expensive and would otherwise be a convenient way to siphon off personal data. There is deliberately no reset call — whoever needs disclosure sooner gets it via the administration export (users.dataExport), which has no quota.

Language and file name

The legal basis and notes on the cover sheet are in the language of the DATA SUBJECT, not of the caller: first their profile language, then the system language from the general settings, then English. For the administration export this is deliberately a different language than the caller's interface — the document is a disclosure FOR the data subject. If no system language is set in the general settings, people without a profile language receive the cover sheet in English. The file name stays language-neutral: data-export-<date>.json for one's own export, data-export-<userId>-<date>.json for the administration export.

Both paths are audited (DATA_ACCESS) — with categories, record count and the data subject.

Anonymization (Art. 17)

Art. 17 is implemented as anonymization. The reason: deleting the user outright would leave every reference to them (tickets, comments, history) dangling and destroy the factual ticket history. The record therefore remains as a skeleton (identifier, timestamps, status), and all personal data is irreversibly overwritten in one transaction.

Step 1: Pre-check — what blocks?

Active involvements block erasure until an admin resolves them. A workflow step whose assignee vanishes would otherwise hang; an approval quorum could become unreachable. Art. 17 requires "without undue delay" (practically ≤ 1 month) — leaving room to reassign active cases first. GET /erasure-check returns the blockers with counts and entity links; POST /erase refuses the anonymization while blockers exist.

GET /api/privacy/users/:id/erasure-check
{
  "erasable": false,
  "alreadyAnonymized": false,
  "isArchived": true,
  "erasureHold": false,
  "erasureHoldReason": null,
  "blockers": [
    {
      "type": "openTicketsAssigned",
      "count": 3,
      "refs": [{ "id": "tkt_1", "label": "T-1042", "url": "/tickets/tkt_1" }]
    },
    { "type": "pendingApprovals", "count": 1 }
  ]
}

erasable is true only when no blocker exists, no erasure hold is set and the person has not yet been anonymized. Per blocker the check returns up to five references with a link target — so the list can be worked through, not merely counted; types without their own detail view carry a label only.

The blocker types (must be resolved manually): open tickets as customer or assignee; open incidents/problems/changes/change tasks as assignee; running workflows as initiator or step assignee; PENDING approvals as approver; PENDING handovers; assigned assets, held consumables and active licenses; sole member of an otherwise non-functional approval group; mailbox catch-all target; designated assignee in a workflow template; target of an escalation policy that would otherwise have no recipient. Some things the transaction resolves itself (substitutions, group memberships, mailbox access, manager relations, user-ID lists such as scheduled report recipients).

Step 2: Anonymize

Preconditions (otherwise the call is refused with a specific error code, see error codes): not a protected account (system user, first administrator account), not the caller themselves, no erasure hold, the user is already ARCHIVED (two-step protection; archived users cannot sign in anyway), not yet anonymized and free of blockers. Then ONE transaction overwrites the personal data everywhere:

  • Master data: email → erased+<id>@anonym.invalid, name → "Deleted user", avatar/preferences/2FA secrets → null, anonymizedAt set
  • Name snapshots in all *Activity/timeline tables and on ticket-comment authors (actorName → "Deleted user") — otherwise the name would remain there in plain text
  • Attachment uploader/IP/user agent, license assignment emails, participant emails, Teams reference, email sender fields
  • Absences: reason/rejectedReason → null (sick notes + free text = health-related data, Art. 9)
  • The user's access ends entirely: sessions/tokens are fully revoked (refresh tokens + sid sessions), active WebSockets disconnected, device push registrations removed — and it receives no further notifications (see notification system)
  • Audit events of the actor: targeted content scrub (actor_name/IP/user agent) so no plain text survives there for up to 7 years — the actorId stays as a pseudonym, the trail stays correlatable

Deliberate boundary: Ticket and comment TEXTS are retained (Art. 17(3) — business record); the personal reference falls away via anonymization of foreign keys and snapshots. Personal data WITHIN free text is a manual redaction case. The Microsoft Entra ID sync skips anonymized users (no re-import). Anonymized skeletons are also excluded server-side from all user lists and selection pickers (assignee, customer, sharing, checkout recipient, approver) — only user administration can reveal them via a toggle.

Erasure hold (legal hold)

POST /erasure-hold sets an absolute lock that takes effect BEFORE all other checks — including the automatic 180-day anonymization and the re-application after a backup restore. While the hold is active, the erase menu item is disabled and every erasure attempt fails in a controlled way.

The call is strict per direction: setting it ({"enabled": true}) requires a reason of 1 to 500 characters (e.g. ongoing proceedings, retention obligation) — a decision that must be documented should carry its reason. Releasing it ({"enabled": false}) accepts no reason. Any other shape of the call is a 400. Protected accounts have no erasure lifecycle and therefore no erasure hold either; an already anonymized account can no longer be put on hold. Both directions write an audit entry (ERASURE_HOLD_SET or ERASURE_HOLD_RELEASED), the setting one including the reason.

Error codes

CodeHTTPMeaning
ERASURE_BLOCKED400Active involvements exist; details.blockers names type and count per blocker
ERASURE_HOLD_ACTIVE400Erasure hold set — release it first, then anonymize
ERASURE_REQUIRES_ARCHIVED400The account must be archived before anonymization
USER_ALREADY_ANONYMIZED400Already anonymized — the operation is irreversible and not repeatable
VALIDATION_ERROR400Hold without a reason, release with a reason, unknown value for kind or hold
ERASURE_SELF_FORBIDDEN403Nobody anonymizes their own account
PROTECTED_ACCOUNT403Protected account (system user, first administrator account): neither anonymization nor erasure hold
DATA_EXPORT_QUOTA_EXCEEDED429Second self-service export within 24 h; Retry-After names the remaining time in seconds
NOT_FOUND404The requested person does not exist

Deadline model

CasePeriodMechanic
Erasure request (Art. 17)immediately once blocker-freemanually by admin/DPO
Departed (archived) users without a request180 daysTarget archivedUsers in retention_purge: auto-anonymization if blocker-free and without a hold; otherwise skipped and reported in the job result. The period starts at the LAST CONTACT — counted from whichever is later: the archival or the last email on that account, in either direction (the agent reply counts like the customer mail). An archived account that keeps corresponding is not anonymized; without further contact the period elapses normally. The run's result reports the skipped ones separately (hold, ongoing contact, blocker)
Auto-created email contacts without open cases90 / 180 daysTarget emailContacts in retention_purge: without any business anchor (no ticket as customer, no handover, no ticket participation) → hard delete 90 days after the last contact; with an anchor and only closed cases → auto-archive after 180 days of inactivity, after that the regular anonymization of archived users applies (target archivedUsers). Open tickets, assigned assets, held consumables, active licenses and open approvals stop both stages. The auto-archive also ends the account's access (sessions, live connections, push) and its notifications
Anonymized skeletonsunlimiteddeliberate: a record without personal reference contains no personal data

External handover recipients (technicians/partners with a loaned device, created as an email contact) also run this way: while assets are assigned the blocker list applies; after return archive and anonymize via the 180-day automation. Handover protocols remain available as on-demand PDFs and render the pseudonym after anonymization (note "recipient anonymized").

Erasure backlog

The backlog answers one question: who should long since have been anonymized or cleaned up under the deadline model but has not been — and why? It holds two kinds (field kind): overdue archived accounts (ARCHIVED_OVERDUE) and inactive, automatically created email contacts (EMAIL_CONTACT_STALE). Due dates are computed with the same semantics as the nightly run and from the same live-configured periods, so list, pages, sorting and key figures all see the same rows. In the interface the view sits under "Reporting & Audit → Erasure Backlog".

KindStart of the periodPeriod
ARCHIVED_OVERDUEthe later of archiving and the last email on the accountperiod for archived accounts (default 180 days)
EMAIL_CONTACT_STALEthe later of the last change and the last emailthe period of its stage: with a business anchor (a ticket as customer, a handover or a ticket participation) the archive period (default 180 days), without an anchor the deletion period (default 90 days)

The list comes as { data, pagination } and is sorted by overdueness (the longest-overdue row first). Search with q over name and email, page with page and per (default 25, maximum 100). Two filters narrow it down: kind (ARCHIVED_OVERDUE or EMAIL_CONTACT_STALE) and hold (true or false); any other value is a 400 rather than a silent assumption. Each row carries userId, name, email, kind, the reference date (referenceDate), the days past the period (daysOverdue), the hold status with its reason (erasureHold, erasureHoldReason) and isArchived. The blockers of a row are fetched by the interface via the pre-check of that same person — there is only one source for them.

The key-figure route /erasure-backlog/stats returns { stats, retention }: stats counts archivedOverdue, emailContactsStale, holds and total over the UNFILTERED backlog — so the tiles stay stable while filtering, whereas the list carries its filtered sum in pagination.total; with exactly one tile filter both numbers agree. holds counts only holds inside the backlog, not every hold in the system. retention names the three periods the view computes with (archivedUserRetentionDays, emailContactHardDeleteDays, emailContactArchiveDays). Accounts WITH an erasure hold deliberately appear in the list although the nightly run skips them — making the hold visible is half the point of the view.

Retention & Derivatives

ONE nightly job retention_purge (03:00 UTC) enforces all time-based database retention periods in a single run — isolated per target (a broken target does not stop the others) and with a self-audit event per run. The default periods:

TargetRule (default)
AuditEventtwo-stage — content purge per policy (90d/365d/7y), row delete at the 7-year maximum (see audit page)
ActivityLog180 days
RoleAuditLog365 days
EmailMessageticketless orphans (REJECTED/bounces) older than 90 days; rawHeaders + retry copy older than 30 days nulled
WebNotification / ClientError30 / 90 days
ReportExecutionpreviews 24h, result data nulled after 30 days; export files (CUSTOM_REPORT attachments) older than 30 days soft-deleted
JobExecution / PushSubscriptionold records / inactive subscriptions
notificationDigestItemsorphaned digest buffer items (digest disabled after buffering), default 30 days — regularly items are deleted immediately after delivery
BulkUpdateJob90 days
kbArticlessoft-deleted KB articles (trash) older than 90 days → final purge incl. revisions, tag orphans and attachments (cascade)
emailContactsauto email contacts: anchorless → hard delete 90 days; only closed cases → auto-archive 180 days (see periods table)
WorkflowInstance / UserAbsenceoptional, default OFF — for operators with a stricter form-data/Art. 9 policy

Boundary rule: time-based retention of database rows runs via retention_purge; anything touching files or virus scans (physical deletion, quarantine) stays with the attachment_cleanup job.

Derivatives after anonymization

Every place that renders or stores personal data is assigned to a class — making it traceable what an artifact shows after anonymization:

ClassBehavior
A — live render from DBself-healing: renders anonymously by itself (detail/list views, on-demand handover PDF, global search, report execution)
B — snapshots with retentiona name may appear until the period lapses — accepted for windows ≤ 90 days (web notifications, nulled report results)
C — immutable storestargeted content scrub on anonymization (AuditEvent)
D — filesdecided per file (avatars deleted, user uploads stay with an anonymous uploader, report exports 30-day retention)
E — external/irreversiblenot retrievable — sent emails, Teams/Webex messages, delivered push (Art. 17 boundary, disclosed in the privacy policy)
F — Backupsa restore can revive erased people → re-apply erasures after every restore (see below)

Backup restore: A restore from a dump predating an erasure makes the person visible again. Therefore: before the restore save the list of anonymized accounts from the current database, after the restore re-apply every erasure made since — this is repeatable, already anonymized accounts are left untouched. An erasure hold and existing blockers apply here just as they do on the manual path. The procedure is part of the operations runbook for backups.

Public Transparency

Privacy policy and imprint are reachable at /privacy and /imprint WITHOUT login — required for the three groups who never see the login page: recipients of the Art. 14 info mail to auto-created contacts, external handover recipients on the public confirmation page, and email footer links. The periods in the privacy policy are LIVE: /api/public/privacy-config returns the real, currently configured retention_purge parameters, so an admin change appears without a deployment (static values only as an offline fallback).

The privacy policy discloses the third-party services in use (Cloudflare Turnstile, Microsoft Entra ID/Graph, Teams, Webex, Web-Push, SMTP), telemetry with truncated IP, and the Art. 9 masking implemented as a safeguard for absences (sickness free text is stripped without a special right). An optional Art. 14 info mail to auto-created email contacts can be enabled globally via a notification-type toggle (default OFF).

Permissions

PermissionDescription
users.erasePre-check, anonymization and erasure hold (default: ADMIN; critical action, the permission is checked directly against the database on every call, so a revocation takes effect immediately)
users.dataExportThird-party export for any person (ADMIN/DPO). The self-service export needs no permission, only a valid session.
users.eraseaudit.enterpriseViewReading the erasure backlog and its key figures — both the people acting and pure reviewers (data protection, say) can see it; the actions inside it stay bound to users.erase.
Related Documentation