Salta ai contenuti

Guest data rights and retention

Questi contenuti non sono ancora disponibili nella tua lingua.

How long GuestCraft keeps a guest’s personal data, how an operator erases it or gives the guest a copy, and how a legal hold protects it during a claim. The decisions and their reasons are in ADR 0052.

Settings → Privacy (grant workspaces.configure) holds one period per purpose:

Purpose Counts from Generic default
Guest profiles without stays (guest_profile) the profile’s last change kept until a period is set
Personal data on departed stays (stay_personal_data) the stay’s departure kept until a period is set
Booking revision archives (booking_revision_archive) the revision’s receipt kept until a period is set
Audit details (audit_pseudonymization) the entry kept until a period is set
Exports (exports) the export 7 days
Maintenance ticket photos (maintenance_photos) the ticket’s closing kept until a period is set
Message content (message_content) the message 730 days, set in Settings → Messages

The analysis sets no period for personal data, so GuestCraft deletes none of it automatically until the workspace or its country pack sets a period. Choose the workspace’s data-protection jurisdiction: when an installed pack has a data.retention rule for that country, its periods replace the generic ones, and the screen shows the pack, its ruleset version and its sources. A period set on the screen replaces both. Data that a country module collects only for an authority follows the pack’s rule alone; the screen shows it read-only. The core collects none.

Technical periods have no setting: events and webhook delivery logs 30 days, merge tombstones’ personal data 90 days, synchronization records 30 days; outboxes are emptied once sent.

Every hour (Docker worker) or at the start of each budgeted drain (Vercel), each live workspace whose run is due gets one privacy.retention job. One run changes at most 100 rows per store and queues the next run when a batch was full; it skips every guest under a legal hold and records privacy.retention_applied with counts when something changed. It:

  • anonymizes profiles without stays past their period, and the stays past theirs (notes, cancellation reasons, OTA contact snapshots, rooms’ free-text reasons and children’s ages), then the profile once all its stays are anonymized;
  • deletes booking revision payloads whose receipt is final; quarantined revisions keep theirs;
  • pseudonymizes audit details past the audit period;
  • deletes the photos of maintenance tickets closed longer ago than the photo period, the stored file first and then its row (maintenance_photos in the counts); a ticket reported from the stay of a guest under a legal hold keeps its photos;
  • expires exports and removes their archives, and ends as failed an export still queued after 24 hours (its job never completed);
  • anonymizes merge tombstones 90 days after the merge, keeping identifiers and pointers;
  • deletes events older than 30 days without a pending webhook delivery, processed ARI changes, settled ARI sends and finished history imports older than 30 days (keeping each property’s latest for the health and history screens), and webhook signals older than 30 days.

Anonymizing a profile clears its contacts and notes and replaces its name with -; the profile keeps its identifier, merge pointers, stays, folios and consent decisions. Contacts entered again later are personal data again, and retention applies to them. The same holds for data written later into a stay’s rooms (override and open-balance reasons, children’s ages), its housekeeping tasks and the maintenance tickets reported from them, and into a profile’s consent notes or contact reviews: retention counts again from that write.

On the guest profile, Personal data → Erase (grant guests.erase, unrestricted members, typed ERASE confirmation). The erasure covers the live profile and every profile merged into it. It waits while a stay has not ended or a legal hold is active. It anonymizes the profiles, the stays’ personal fields, the booking revision payloads, consent notes, rejected contact values, merge snapshots, the free-text audit details, the Messages content of booking conversations, notes on the stays’ housekeeping tasks and on maintenance tickets reported from them, photos of those tickets, the sent or failed webhook deliveries whose stored body names the guest or a stay, and the guest’s prepared exports.

A stay’s quarantined booking revision (Channex sent a version GuestCraft could not apply) loses its payload, so it can no longer be retried. The erasure resolves it: its receipt becomes stale with the reason “personal data erased; handle the booking in the channel’s extranet”, the alert on Channels closes, the result lists it under quarantined revisions and the audit records channex.revision_stale. Handle that booking in the channel’s extranet. A newer revision from Channex is imported as usual.

What stays, and why: folios, postings and payments (accounting records), with the online payment records of the stay: provider payments and refunds, the card summary (brand, last four digits, expiry) of a payment, a saved card or a moved OTA card, and a saved card’s consent evidence (the provider’s identifiers are identifiers only; an OTA booking’s own card facts from Channex are cleared with the stay’s personal data); fiscal documents of a country module (GuestCraft’s core issues none); consent decisions with their time, actor and source (evidence); audit entries’ actions, actors, subjects and times; identifiers and merge links; booking identifiers and receipts (so Channex revisions are not imported again); conversations without a booking (no trusted guest link; message retention removes them). Messages that arrive after the erasure are stored normally.

Which operation to use: a guest’s erasure request (the right to erasure) is Personal data → Erase, which includes the Messages content. Settings → Message retention → Erase a guest’s message content (messages.erase) removes only the content of the guest’s booking conversations and keeps the profile and stays: use it when a conversation must go but the rest of the record stays, for example content sent by mistake.

GuestCraft cannot erase the copies held by Channex and the OTAs. Channex has no erase operation: ask Channex and each OTA directly. The confirmation and the result say so.

Known gap: purging erased content from phones is a postponed mobile task. Until then a phone keeps its cached reads for at most 72 hours (removed at sign-out and revocation), and its next read returns the anonymized data.

Personal data → Prepare export (grant guests.export) queues a privacy.export job. It writes one ZIP: guest-data.json (every catalog table about the guest’s identities and stays, with public identifiers, the decrypted booking revisions, the Messages transcript, the audit references of the guest’s identities, stays, rooms and folios, and the list of omitted columns with their reasons) and guest-data.csv (section, record, field, value). Message attachments are links to files Channex holds. A member holding the grant downloads the archive once; it is deleted at download and expires after the export period. An archive over 200 MB fails; nothing is truncated.

The one free-text audit detail (the reason for checking out with an open balance) is replaced by pseudonym:v<version>:<hex>, a SHA-256 digest keyed per workspace, after the audit period or at the guest’s erasure. The database computes it; the key never leaves it, and only guestcraft.pseudonymize_details may write the digest. To rotate a workspace’s key, the database owner runs select guestcraft.rotate_pseudonym_key('<workspace uuid>');: new digests use the new version and older ones stay valid.

Personal data → Place hold (grant guests.legal_hold) with a reason; Release, with another. One hold is active at a time and released holds stay in the history. While active, neither erasure nor retention touches the guest’s records.

All paths are under /api/v1, with GuestCraft-Workspace-Id, and need a browser session: API keys and organization tokens never hold these grants.

Operation Permission Result
GET /guest_data_retention workspaces.configure Settings, defaults with their source, effective periods, technical periods
PUT /guest_data_retention workspaces.configure Saved settings; If-Match required
GET /guests/{id}/legal_holds guests.erase, guests.export or guests.legal_hold Holds, newest first
POST /guests/{id}/legal_holds guests.legal_hold The new hold; 409 guest.legal_hold_active when one is active
POST /guests/{id}/legal_holds/{hold_id}/release guests.legal_hold The released hold
POST /guests/{id}/erase guests.erase The completed request, the new guest version, retained and upstream; If-Match required
POST /guests/{id}/data_exports guests.export A queued request
GET /guest_data_requests guests.erase or guests.export Requests, newest first; guest_id filters to one guest’s chain
GET /guest_data_requests/{id} guests.erase or guests.export One request
POST /guest_data_requests/{id}/download guests.export The ZIP, once

The OpenAPI descriptions list each operation’s problems. Erasure answers 409 guest.erasure_blocked with reason active_stay (and the reservations) or legal_hold, and 409 guest.absorbed with the survivor for a merged profile. A download answers 409 privacy.export_unavailable with reason not_ready, downloaded, expired, erased or failed.

Every column of a table with workspace_id has an entry in packages/db/src/personal-data.ts: a personal column with its purpose, erasure rule and access rule, or the list of other columns. The PostgreSQL catalog test fails until a new column is classified. A module whose data a column rule cannot reach (encrypted content, files) implements PersonalDataModule of @guestcraft/core/privacy and is composed in packages/server.