Data Retention & Deletion
← All legal documentsChange note, 23 Oct 2026 — each commission payment is kept on its own. A commission line used to hold one received amount and one date; each payment received against it is now its own row in
commission_payments: the amount, the day it arrived, where it came from (a lender's statement, typed in, or carried over from before), the statement it came from and who recorded it. It is part of the commission line and is kept and deleted with it: readable by whoever can read the line, erased with the client, the firm's account and the six-year purge by cascade, and listed with the line in the client's subject access export. No change to how long anything is kept.
This describes what Crestwell stores, where, how long, and how it's deleted. It reflects the system's actual behaviour.
What's stored, and its lifecycle
| Data | Where | Retention behaviour |
|---|---|---|
| Meeting audio | meeting-recordings bucket (Supabase, private) |
Deleted automatically after transcription by default (data-minimisation). A firm may instead choose a fixed retention window (7, 30 or 90 days — Settings → Firm profile), after which a daily sweep deletes it automatically; the window is the firm's own documented retention decision as controller, made so transcriptions can be verified or re-run against the recording. The broker can always delete it earlier on demand ("Delete audio now"). Shortening the window applies to recordings the firm is already keeping: none is kept longer than the new window counted from the change, and switching to "Delete after transcription" deletes them in the next nightly sweep; lengthening it applies only to meetings transcribed from then on. A recording that is never transcribed (founder decision, 15 Oct 2026): under 30 seconds long, it is deleted in the next nightly sweep; longer, where transcription failed, the sweep tries again once a night for up to three nights and then keeps it, and the meeting page asks the adviser to retry or delete it; where no speech was heard, it is deleted 7 days after the attempt. An upload that never finished is left for the recording device to complete. Legacy MEETING_RETAIN_AUDIO retains indefinitely and is discouraged. |
| Meeting audio on the adviser's device — a recording while it is being made, and until its upload is confirmed | The adviser's own browser storage (IndexedDB), on that device only | Deleted from the device as soon as the upload is confirmed, or when the adviser discards the recording or deletes the meeting there. A recording that was interrupted (a locked screen, a call) or whose upload failed stays on the device, and the app offers it back to the adviser to recover or discard; nothing deletes it on a timer. Crestwell adds no encryption of its own there: the device's own protection applies. |
| Meeting transcript + AI notes + extracted draft | meetings table (Supabase) |
Part of the advice record (below), and kept on its schedule unless the broker deletes the meeting first. A meeting never linked to a client is outside the schedule and kept until the broker deletes it. |
| Fact-find documents added on the New fact find card (16 Oct 2026) | client-documents bucket (private), with a client_documents row, as client documents (the Client documents row, below) |
Kept as the client's documents, on the retention schedule — not deleted when the fact find is read. Each document is filed into the client's Documents the moment it is added, through the same upload as the Documents page (classified, routed into the checklist, held if it shows sensitive details, and not stored again if it is a byte-identical copy of one already filed to that client). Begin reads them where they are; the files of ID, income and bank documents then go sooner than the advice record, as for any other client document (the ID, income and bank documents row, below). The client record is found by name, or made, when the first document is added. Taking a document back out of the card in the sitting that added it deletes it from Documents too, unless it was already in the client's file. Until 16 Oct 2026 these documents went to the extractions bucket and were deleted when the read ended (next row). |
| Fact-find documents uploaded for extraction — "Upload additional documents" on a saved fact find, and the older full-page fact-find upload | extractions bucket (private) |
Transient — deleted as soon as extraction finishes (and on failure/abandonment). A scheduled sweep is the backstop. |
| Fact-find drafts — a fact find started on the New fact find card and not yet read (16 Oct 2026) | fact_find_drafts table, readable and writable by the adviser who started it only |
The client id, the applicants' names as typed (one, or two on a joint application), whether it is joint, an optional email and phone number, and the ids of the client documents it is to be read from (at most 40). No copy of any document. Deleted when the read is saved as a fact find. A read that fails, or never starts, leaves the draft and its documents in place, so the pack need not be uploaded again. No expiry of its own: kept until the adviser deletes it, which leaves its documents in the client's file, or until the client is erased, by the adviser or under the retention schedule, when it goes by cascade with the client. Also deleted with the adviser's account and when the firm closes its account (user_id and firm_id cascade). |
| Fact-find runs — one read owned by the server, so it finishes and is saved even when the adviser's browser goes: the New fact find card (25 Oct 2026), and the full New fact find page and Upload additional documents on a saved fact find (26 Oct 2026) | fact_find_runs table, server only (row-level security on, no policies) |
Ids (the run, the adviser, the client, the fact find it saved), a status, when a Cancel was asked for, and a fixed broker-facing sentence when a read failed. No client data — no name, no filename, no extracted value. The fact find itself is saved straight into fact_finds, as before, with no second copy. |
| Client documents (portal/checklist uploads) | client-documents bucket (private) |
Kept on the retention schedule: a document that is part of the advice goes with the advice record, and the files of ID, income and bank documents go sooner (the two rows below); the broker can delete any document earlier (with a confirm prompt). A file the adviser adds through the File checker, the Documents page, a client's Documents card or the New fact find card (16 Oct 2026) that is byte-for-byte a copy of one they already filed to that client is not stored again (compared by a SHA-256 of the stored bytes, computed on the server; that comparison keeps no hash, and the only hash kept is the fingerprint of a file the schedule deletes). Extra identical copies already in a folder are deleted only when the adviser chooses Remove extra copies and confirms; one copy of each file is kept, and a copy that was added by a colleague, is held or kept for sensitive details, or carries a different type, hand-set type, verification or applicant stays. Each removal is recorded in audit_events as document.copy_removed, naming the removed and the kept document by id only. A file the adviser adds again under the same name, that reads as the same kind of document, is treated as a newer version: the older one is deleted only when the adviser chooses Remove older versions and confirms, and the newest is kept. Offered only for working documents a case holds one current copy of (the application, memorandum of sale, solicitor details, property schedule and reservation agreement) - never for the IDD, privacy notice, fee agreement, fact find, suitability letters or mortgage illustrations (each a record of what the client was given or signed, so an older one may be the one to keep), ID, payslips, statements or tax documents, for a name a device made up (such as scan0001 or IMG_1234), or for a version the adviser verified; recorded as document.older_version_removed, by id only (28 Sep 2026). |
Client paperwork sent to a client, and their confirmation (22 Oct 2026) — the firm's own initial disclosure document, privacy notice or fee agreement put on the client's portal: which file was sent (its sha-256) or the firm's privacy link, the fee stated, when it was opened, and when the client confirmed they read it or signed it, with their IP address and browser at that moment, and for the fee agreement the name and date they typed, their drawn signature, and when they confirmed the 6-digit code emailed to them before signing (no signing link or token is kept: the fee agreement is signed in the portal; the code itself is stored only as a salted hash in portal_sign_codes, keyed to the item, and goes with it by cascade, a row older than a day being deleted when the next code is sent) |
client_paperwork table, readable by whoever can open the client, written only by the server; the evidence fields are frozen once written |
Part of the advice record, and kept on its schedule: deleted with the client (erasure and the six-year purge, by cascade on client_id). The stamped copy filed on confirmation is a client document (the Client documents row) typed as an IDD, privacy notice or fee agreement, and goes on that row's schedule; deleting it from the client's Documents leaves this record and lets the adviser file it again. |
| Fact finds | fact_finds table |
Part of the advice record (next row), and kept on its schedule. Signatures, attestations and adviser sign-offs go with it. When the client e-signs a fact find, a copy of it exactly as signed is kept with their signature (signature_requests.signed_document, migration 20261014201416_keep_what_the_client_signed; signatures recorded before it have none), and goes only with that signature, so on the same schedule. A fact find never linked to a client is outside the schedule and kept until the broker deletes it. |
| The advice record — the client record, fact finds, suitability letters, file notes, vulnerability reviews, meeting transcripts and notes, portal messages, and the documents that are part of the advice: the mortgage illustration or ESIS, IDD, fee agreement, privacy notice, uploaded suitability letters and fact finds, declarations, decision in principle, application and offer. A document whose type was never identified counts as part of it | Supabase tables, scoped to the adviser who holds the client (a colleague sees a client only where it is shared with them); commission as the firm's commission setting allows; documents' files in the private client-documents bucket |
By default, kept for the mortgage term plus 6 years from completion. With no completion recorded, 6 years from the last activity on the client, which is the latest of a change to the client record or a fact find, a document received, and a portal message. Never less than 3 years after the last activity; where no mortgage term is recorded, 30 years is assumed; and nothing goes in a client's first 12 months on Crestwell, whatever dates an imported record carries. The firm's owner and admins are told in the app 30 days before anything is due, and either can choose Keep for now, which holds that client for another 12 months. The firm can instead choose Keep everything until you delete it: nothing is then deleted automatically, and the owner and admins are told once a year which clients are past the standard dates. When the advice record falls due the whole client is erased, with everything kept against them, commission, protection and tasks included. The broker can delete any of it sooner, except portal messages: since 23 Oct 2026 nobody signed in can delete or change one, apart from marking it read, and a client's thread is deleted only with the client (migration 20261023173201). |
| ID, income and bank documents — proof of identity and address, payslips, P60s, SA302s and tax year overviews, bank and mortgage statements, evidence of deposit, AML checks, credit reports, unsecured debt statements, bonus evidence, memoranda of sale, solicitor details, property schedules and reservation agreements | The file in the private client-documents bucket; the record in the client_documents table |
By default the file is deleted 6 years after completion, with the same 30 days' notice, Keep for now, 12-month floor and firm choice as the advice record; with no completion recorded it goes with the advice record. The document's record stays with the advice record: its type, when it was received, the checks run on it, and a SHA-256 fingerprint of the file taken just before the file was deleted. A document held for sensitive details follows the quarantine window instead (Retention periods, below). |
| Email held at an intake address — an email that could not be placed with a client or authenticated, or that carried a forwarded email that would not open, held for the client's adviser or, if it matched no client, the firm's owners and admins | inbound_messages table |
Closes on its own once the firm's quarantine window (default 14 days) passes with nobody having acted. The sender's address and the subject are then blanked; the date it arrived, how many attachments it had and the outcome are kept, as the record that an email was refused. A held email's attachments are stored only if the adviser accepts it (a forwarded email in it may have been opened in memory first, and nothing of it kept), so there is no file of ours to delete. An email the adviser discards keeps its sender and subject. Our email provider keeps its own copy of every email it receives (see the sub-processor list). |
| Email-in addresses — each client's, the firm's and each adviser's document intake address (22 Oct 2026: the firm's and advisers' are names, and an adviser's carries their first name or a name they chose) | client_intake_aliases table (migrations 068, 069, 20261022171522) |
A client's address is deleted with the client. An adviser's is deleted with their account. The firm's, and any left, are deleted with the firm's data. An address that is replaced or stopped is kept, marked as stopped, so mail sent to it is turned away rather than reaching anyone else; a random one is never handed out again, and a firm's or adviser's name is held back from every other firm for 90 days after it stops, then released only when another firm takes it (the old row stays, marked released, and no longer receives mail). After a firm or adviser renames, the previous address keeps working; only the three most recent previous names do, and a further rename stops the oldest. |
| Rate-limit log | api_requests table |
Operational; short-lived counters. |
| AI cost records — what each paid model or transcription call cost (Crestwell's own record, as controller) | api_job_usage table (migrations 091, and 20260928105700 for the app's own calls); service role only — no firm or adviser can read it |
One row per call: for the app, the firm id, the id of the adviser who asked (none for a portal upload or inbound email) and the feature; for the Partner API, the job, partner and job kind; for both, the model, token counts, audio seconds, retry flag, stop reason, duration and time. No client content, client id or file name. No period set yet. Until one is: kept; the adviser's id is emptied when their account is deleted, and a closed firm's rows are kept with an id that no longer points at anything, because they are our cost history rather than the firm's data. Not in a client's DSAR bundle, because nothing in it is about a client. |
| Public website measurement — page views, engaged time, scroll depth, clicks; the visitor as a daily-rotating keyed hash, never an address. On the website's demo (the demo page, and the demo the early-access page plays in place; from 25 Oct 2026): which demo steps were shown, which workflows were reached to their end and how long the demo was in front of the visitor, grouped by a random number made for that one pass through the demo and tied to the page view it happened on, with no address, country or browser of its own | site_views, site_clicks, site_demo_events tables |
Deleted after 400 days by the nightly /api/cron/site-prune, all three alike. Cookieless, and nothing is stored on the device; the demo plays back captured screens of the application and is part of the website; the application is not measured. |
| Waiting list — the work email, and any optional answers about the firm, of someone who asked for an invite (Crestwell's own list, as controller) | waiting_list table |
Invited, and neither joined Crestwell nor getting updates: deleted 90 days after the invite, or 90 days after they stopped the updates if that is later. Took up the invite and joined Crestwell: deleted 30 days after joining. Getting updates and not joined: kept until they stop them. Still waiting for an invite: kept until they ask to come off the list or the list closes. Someone who asked to be removed stays on it marked as removed, as the record that stops a later invite. |
| Outreach unsubscribe links | None stored | Stateless HMAC tokens — no token table; nothing to retain for the link itself. What an unsubscribe records is kept on the client's record and against their email address in the firm's email suppression list (next row). |
| Email suppression list — each email address a firm's client has said no from: by unsubscribing, in their portal or on their fact find, or through their adviser | Supabase (EU), one list per firm | For each address: a SHA-256 fingerprint of the firm and the trimmed, lower-cased address, how the no arrived, and when. Never the address itself, a name or a client. Checked before an email is drafted and again before it is sent, so the no covers anyone at that address, and an unsubscribe recorded there stops every automated email to it, case emails included. Kept indefinitely, including after the client is erased: it is what stops the person being emailed again if they are added back, and deleting it would make them contactable once more. It narrows to a no to keeping-in-touch email when the adviser switches emails back on at the client's request; it is removed when keeping-in-touch emails are switched back on for that client, by the client in their portal or by the adviser (on the client's confirmed word, where the no was the client's own); and it is deleted when the firm closes its account. |
| In-app notifications | user_notifications table (migration 044), RLS-scoped to the recipient |
Notifications are server-side and may embed a client name and a short excerpt of adviser-entered text. Capped at the most recent 200 per user by an after insert trigger, which does not spare unread rows; deleted with the user's account (user_id cascades from auth.users). The cap is the trg_trim_user_notifications trigger in migration 20260906160600. |
| Team notes — their sections, comments, pasted images and attached files | team_notes, team_note_shares, team_note_blocks, team_note_comments tables (migrations 044, 20260925145744 and 20260925154951), RLS-scoped to the author and whoever the author shares the note with; files in the private note-files bucket (10 MB each; images, PDF, Office, CSV, text — no SVG or HTML) |
Retained until deleted. Anyone the note is shared with can delete a section, image or file; a comment can be deleted by its writer or the note's author; only the author can delete the note, which takes every section, comment and file with it. A section or comment written by a colleague whose account is later deleted stays in the note, unattributed. A comment also sends an in-app notification quoting up to 140 characters of it (see the row above). Deleted with the author's account (author_id cascades from auth.users) — so closing a firm, which deletes its users (/api/firm/close), deletes its team notes at closure, not after the 6-year period. A file is removed from storage when the app deletes it; a file whose row went by cascade (note, author or firm deleted) is queued in note_file_deletions and removed by the daily retention job. Team notes are free text and can hold client personal data, but they are not the advice record (that is the file note) and are not covered by purge_firm_client_data. |
| Team chat — direct messages, group threads and announcements between a firm's users, and files attached to them | conversations, conversation_members, conversation_reads, conversation_messages tables (migrations 055, 058), RLS-scoped to the thread's members; files in the private message-attachments bucket (25 MB each), under the thread's id |
Retained for the life of the firm's account. Nobody signed in can delete a message or a thread (founder decision, 20 Oct 2026; migration 20261020143601): chat is part of the firm's record, so a thread is archived instead. The person who started it can archive it for everyone (it leaves every member's list, can still be read under Archived, takes no new messages, and can be unarchived), and any member can archive it for themselves. A sender can edit the text of their own message; each earlier version is kept, with who changed it and when (conversation_message_edits, migration 20261014221856). A sent message's attachment and client mentions cannot be changed (migration 20261023161143). A member can leave a thread, which deletes nothing anyone said. Deleting a user's account deletes the messages they sent and the threads they started, with every message in those threads (sender_id and created_by cascade from auth.users). Closing the firm's account deletes every thread and every file (/api/firm/close). The daily retention job removes any file no message names any more, once it is a day old: a file uploaded and never sent, and the files left by a deletion. From 14 to 20 Oct 2026 a sender could delete their own message and whoever started a thread could delete it, with its files; before 14 Oct those deletions left the files in storage. Chat is free text and can hold client personal data, but it is not filed to a client, so it is not in a client's erasure or DSAR bundle. Covered by the six-year purge (decided by the founder 20 Oct 2026): when a firm's purge runs, six years after its account closed, every thread goes with the rest of its data, with its members, read markers and messages. Each thread's attached files are removed from message-attachments first; if storage refuses a file, or a thread's folder cannot be listed in full, none of the firm's rows are deleted that day and the purge tries again the next (purgeFirmChat in , which runs before purge_firm_client_data). Before 20 Oct 2026 the purge did not reach chat, so a firm whose subscription simply ended would have kept its chat and the attached files indefinitely. |
| Rendered letters and fact finds — the cache of Word suitability letters and file notes filled from the firm's own template, and of filled PDF fact finds | fact-find-templates bucket (private), under renders/<firm id>/ |
Deleted after a day. The hourly /api/cron/upload-retention sweep deletes every render more than 24 hours old (14 Oct 2026), so one goes within 25 hours. The cache path names the firm, not the client, so erasing a client cannot find their renders; the day is the bound. Closing the firm's account deletes them at once. Before 14 Oct 2026 nothing removed them. |
| Firm templates and branding — the firm's PDF fact-find and document templates, its own Word letter and file-note templates, its logo and letterhead, and each user's profile photo | fact_find_pdf_templates, doc_pdf_templates, firm_templates tables; files in the private fact-find-templates bucket under the firm's id, the public firm-assets bucket under the firm's id, and the public avatars bucket under the user's id |
Retained until the firm (or, for a photo, the user) removes them. Removing a PDF template deletes its file (for the fact-find form, a logo or the letterhead, a deletion storage refuses is not yet caught: the record or link goes and the file stays until the account closes); removing a Word template (Forget) deletes its stored Word file, before the template is dropped, so a refusal changes nothing and can be retried (14 Oct 2026; before that the Word file stayed in storage). A firm's own Word template is often a real letter with a past client's details left in, which is why it is treated as client data here. Closing the firm's account deletes every one of these files. |
| The firm's own client paperwork (22 Oct 2026) — the initial disclosure document, privacy notice and fee agreement a firm uploads to send to its clients, every version | fact-find-templates bucket (private), under <firm id>/paperwork/, with a firm_paperwork_documents row |
The firm's own documents, not a client's. A new upload retires the version before it rather than deleting it, because items already sent were hashed against it and their filed copies were built from it. Every version is deleted when the firm closes (the firm's folder) and its rows by cascade. Crestwell writes and reviews none of their wording. |
File check record — one row per recorded File checker run: each check the client's own adviser runs (Check files, Download check report; the same check repeated with the same result within five minutes counts once), and each automatic check — the hourly sweep (/api/cron/file-check, at :50 past each hour), or the client's result page being opened — whose result changed or that marked a stage gate (application submitted, offer, completed). A colleague the client is shared with can read the record, and a check they run is shown to them, but it is recorded only as an automatic result change, never under their name. Stored: the client, adviser and firm ids; checklist item ids and statuses; the FCA-file-list items that were missing, and those the broker is asked to confirm (both as ids); the version of the rules (a label); finding codes, severities and counts; the name check's counts (whitelisted); how many documents were in the file (held and kept ones left out) and whether there was a fact find; the case stage; who ran the check (nobody, for an automatic one); which run it was compared against (none, for a client's first) — unique indexes stop two writers at once recording the same automatic check twice; when; and a SHA-256 fingerprint of the result. Never a finding's words, a file name, a holder's name, a figure, a special-category finding code, or a held/kept count; and a held document is not counted in a stored result (things to fix, warnings, notes, complete or not), nor a held or kept one in the file's document count, so no held count can be worked out from a run. The File checker list's pointer (client_pack_checks, one row per client) does hold live counts that include held documents, as the result page does; it is overwritten every time the result is worked out and is not in the DSAR bundle |
file_check_runs table, readable by whoever can open the client; written only by the server, with the service key |
Append-only — no update or delete for anyone signed in, the broker included, and no code path edits or removes a run. Kept for the life of the client record and erased with the client by cascade: deleting the client, in-app or on an erasure request, takes every run; closing the firm's account takes them at closure, not after six years (/api/firm/close deletes the firm row, and every client with it); and the six-year closed-firm purge takes them when it deletes clients (purge_firm_client_data, migration 067). Deleting a single document does not change runs already recorded — they hold counts, checklist ids and finding codes, never the document. Deleting a user account deletes the clients that adviser holds, and every run on them (clients.user_id cascades from auth.users); on another adviser's client, a run that named the person is kept with their id emptied (user_id and actor_id are on delete set null). In the DSAR bundle as file-checks.csv: a person's check reads someone at the firm, and no user, firm or actor id and no fingerprint is written into it. |
| Automatic file-check queue — which clients are due an automatic, rules-only re-check (no model call) | file_check_queue table; service role only — no policy or grant for anyone signed in |
Transient. At most one row per client: the client id, why it was queued (a file change or a stage gate) and that stage, every stage gate not yet recorded (so two reached before a sweep are each recorded), and three timestamps — when it was first queued, when it is due, and when something in the file last changed. No content. Written by database triggers when the file changes or the case reaches application submitted, offer or completed; deleted by the hourly sweep (/api/cron/file-check, at :50 past each hour) once that client's check has been stored, each stage gate coming off the row as its run is recorded (file_check_queue_done, service role only) — a failed one stays and is retried at the next hourly run. Nothing is queued for a sample client or a client of a firm already closed; a row queued before its firm closed, or before the client was marked a sample, is dropped by the sweep without a run being recorded. Erased with the client by cascade. Not in the DSAR bundle, because it is transient and service-role only. |
| File checker items marked not needed — the items the broker has marked Don't need it on a client's file, so every check leaves them off the list. Only the fee agreement, the bonus or commission evidence, the credit report and the memorandum of sale can be | file_check_not_needed table, readable by whoever can open the client; written only by the server, with the service key, after the route has proven the caller may act on the client — today only the client's own adviser |
One row per client and item: the client id, the item id (fee_agreement, bonus, credit_report or memo_sale — a check constraint refuses any other), who marked it (marked_by), the firm id, and when. No content. No insert, update or delete for anyone signed in, the broker included. Kept until the broker presses Put it back, which deletes the row, or until a check finds the mark no longer does anything — the item is now in the file, or the fact find says it isn't needed — when the server deletes it and records a file_check.put_back event with no person (28 Sep 2026); the table keeps no history of its own — who marked or put back which item, and when, is in the audit trail (file_check.marked_not_needed, file_check.put_back), and each recorded file check keeps which items were marked when it ran. Erased with the client by cascade: deleting the client, closing the firm's account, and the six-year closed-firm purge. Deleting a user account deletes the clients that adviser holds, and every mark on them; on another adviser's client, a mark is kept with who made it emptied (marked_by is on delete set null). Not in the DSAR bundle as a file of its own: that an item was marked or put back, which item and when, is in activity-log.csv, and each recorded check in file-checks.csv reflects the result; the table holds no client content. |
| File check sign-offs (23 Oct 2026) — an owner's or admin's Reviewed or Needs work on a recorded File checker run, made while the firm's owner has switched on file check supervision, with an optional note of up to 280 characters | file_check_reviews table, readable by whoever can open the client, and by the firm's owner and admins through the server while supervision is on; written only by the server, with the service key |
One row per sign-off: the run, client, firm and reviewer ids, the outcome, whether it was a self-review, the note and when. Append-only — no update or delete for anyone signed in, and no code path edits or removes one; switching supervision off keeps them. Kept for the life of the client record and erased with the client by cascade (client_id and run_id are on delete cascade): deleting the client, in-app or on an erasure request; closing the firm's account; and the six-year closed-firm purge. Deleting the reviewer's account keeps the sign-off with the reviewer emptied (reviewer_id is on delete set null). The note is treated as client content: it is in the client's DSAR bundle (file-check-reviews.csv) and never in the audit trail, a notification or the firm's CSV export. |
| Texts sent to clients — one row per text a firm sends to a client, beside an email (23 Oct 2026) | sms_sends table, readable by the firm's own users; written only by the server, with the service key |
The firm, the client, the kind of text (portal invite or reminder), the last three digits of the mobile, Twilio's message id, the delivery status, and what the text cost the firm. No message body and no full phone number. It is the firm's billing record for extra texts, so it is kept as billing history is: deleted 6 years after the text by the daily retention job. Rows are append-only, bar the delivery status. Erasing the client clears the link to them (the client column is emptied) while the row stays as a count; closing the firm's account deletes every row at closure. In the client's DSAR bundle. |
| Short links in texts (23 Oct 2026) | sms_short_links table; service role only — no policy or grant for anyone signed in |
A fingerprint (SHA-256) of each short link's code, never the code, and which portal link it opens. It opens only while that portal link is live, and never after its own end, at most 180 days after the portal link was made. Deleted once that end has passed by the daily retention job (sweep_client_texts), and with the client or the portal link by cascade before then. |
| "Your texts are used" notices — the record that a firm's owner was told, once in a month, that texts stopped at the plan's allowance or the owner's cap (23 Oct 2026) | sms_allowance_notices table; service role only |
The firm, the start of the month it was for, and why texts stopped. No client and no content. Deleted 90 days after the month it was for by the daily retention job (sweep_client_texts); a month is the longest window it ever has to cover. Deleted with the firm's account. |
| An adviser's mailbox connection (25 Oct 2026) — the Gmail or Outlook mailbox an adviser has connected so their email shows in the Crestwell inbox | mail_connections table, one row per adviser per provider; RLS on with no policy, so only the server reads it |
The connection and nothing else: the connected account's address, the permissions granted, the access and refresh tokens encrypted under a key held outside the database, when they expire, when it was connected, whether it still works, and, if a read or a send failed, Crestwell's own one-line description of the kind of failure — never the provider's message. No email is stored: threads, messages, addresses and attachments are read from the mailbox while the adviser has Crestwell open (from when their dashboard loads, so the inbox is ready before it is opened — changed 26 Oct 2026; until then only while the inbox itself was open), answered marked "do not store", held only in that browser tab's memory and discarded when it closes or the adviser signs out; nothing of them enters this table, a server cache or a log, so there is nothing of them to retain or erase. Kept until the adviser disconnects, which deletes the row, or their account is deleted (user_id cascades from auth.users). Not in a client's DSAR bundle: nothing in it is about a client. |
Deletion / right-to-erasure
- Per-record deletion: brokers can delete clients, documents, fact finds, meetings, file notes, letters, commission entries — each behind a confirm prompt. The exception is a fact find the client has signed, or one an adviser has certified once a signing link has been created for it (live, expired or revoked): it is part of the advice record and can't be deleted on its own. It is erased with its client, or by Crestwell on the firm's documented instruction. A certified fact find that no signing link was ever created for can be deleted by its owner, and the adviser's certification goes with it. A fact find with a finalised or sent suitability letter can't be deleted on its own either, because the letter would go with it: the database refuses it from any app or API caller, and it too is erased with its client (22 Oct 2026). A finalised or sent letter can't be deleted on its own for the same reason. Since 23 Oct 2026 the same holds for a fact find with a finalised file note or a recorded vulnerability review, and for that note or review on its own: deleting the fact find would delete them, and a review's history, with it. Any fact find can be archived instead: it leaves the broker's lists, opens read-only, and is kept whole with its letters until it is unarchived or erased with its client; archived fact finds are still in the client's subject access export. Every fact-find deletion a broker makes is recorded in the audit trail as
fact_find.deleted, and each archive and unarchive asfact_find.archived/fact_find.unarchived. Portal messages, the client's and the adviser's, can't be deleted or reworded on their own at all (23 Oct 2026, migration20261023173201): the thread is part of the advice record and goes only when the client is erased. - Meeting erasure permanently removes the recording, transcript, notes, AI draft, and provenance. Fields already committed into a client's fact-find are kept (they're part of the client record).
- On a data-subject erasure request: the controller (broker) identifies the records and deletes them in-app; Crestwell can assist with full account/firm data export + deletion on documented instruction.
- Closing the account (the owner,
POST /api/firm/close) deletes the firm's stored files first — client documents, meeting audio, chat attachments, templates, rendered letters and fact finds, logos, letterhead and profile photos, by each user's, the firm's and each thread's folder — and only then the records. If storage refuses any file, the closure stops with no record deleted and can be retried; a file that arrives while the closure runs is removed by a last pass, or reported for removal by hand. Team-note files follow through the daily retention job (see Team notes). Until 14 Oct 2026 a closure deleted the records and left every one of these files in storage. - On contract termination: Crestwell returns or deletes the firm's data per the broker DPA (the return/deletion window is under Retention periods, below).
- The email suppression list is not erased with the client. An erasure leaves the client's entry on the firm's email suppression list in place. It holds no address, and it exists so that the client's no outlives their record.
- Clients deleted before 16 September 2026: deleting a client then left some of their records in place, no longer linked to the client: fact finds, meetings, suitability letters, file notes, vulnerability reviews, fact-find confirmations and sign-offs, commission entries, emails sent and received, tasks and appointments. Since then, deleting a client deletes all of these. Records left behind that way are listed for Crestwell to reconcile, and go with the rest of the firm's data six years after it closes at the latest.
Retention periods
Two different decisions live here, and conflating them is the usual mistake:
- While the account is active, the firm is the controller. Crestwell applies a default schedule (the advice record and the ID, income and bank documents, above) that the firm can change to Keep everything until you delete it, and the firm can hold any client. Crestwell also deletes on the firm's instruction at any time.
- After the account closes, nobody is instructing anything, so Crestwell needs a stated default. Those are the periods below.
| What | Period | Why |
|---|---|---|
| Client data after account closure — fact finds, meeting transcripts and notes, client documents, file notes, suitability letters | 6 years from termination, then deleted or anonymised | The Financial Ombudsman's six-year window and the Limitation Act both outlive the subscription. The MCOB minimum is shorter, but a complaint landing in year four against a destroyed advice file helps nobody. The firm may instruct earlier deletion at any time — broker DPA cl. 12. |
| Crestwell account and billing records | 6 years after the account closes | HMRC company-record keeping and contract limitation. Login and profile data goes sooner; only the financial records need the full term. |
| Meeting transcripts and AI notes (account active) | With the advice record: by default the mortgage term plus 6 years from completion, or sooner if the broker deletes them | They are part of the advice file the broker may need to defend, so they follow it rather than a shorter clock of their own. The audio itself is already destroyed after transcription, which is where the real minimisation happens. |
| Return/deletion window on termination | 30 days to choose, 30 days to execute | Broker DPA cl. 12.2. Silence does not trigger deletion — the 6-year default applies instead. |
Temporary extraction uploads (extractions bucket) — "Upload additional documents" on a saved fact find, and the older full-page fact-find upload. Since 16 Oct 2026 not the New fact find card, whose documents are client documents kept on the retention schedule |
60 minutes, enforced | Deleted as soon as extraction finishes; the hourly /api/cron/upload-retention sweep (migration 087) is the backstop for what that delete never sees: a killed request, a request refused at intake, an upload never submitted. The window is an age test evaluated hourly, so such a file normally goes within two hours of upload; a failed sweep alerts and the next run retries (2 Oct 2026). |
Partner API audio uploads (partner-audio bucket) |
4 hours, enforced | Deleted as soon as processing finishes, success or failure. The four hours is the backstop for what that delete never sees — a killed request, a request refused at intake, an upload never submitted, and a file in this bucket sent to a document job (which cleans document storage only) — and it is four rather than four minutes because the window must also cover ingress: a 250 MB recording can still be arriving. Same hourly sweep (bucket created by migration 086), so such a file normally goes within five hours; the window follows the bucket, so any upload routed here gets it (2 Oct 2026). |
| Public website measurement, the website's demo included | 400 days, enforced | Thirteen months of comparison (this month against the same month last year) and a margin; the rows carry no address and cannot be joined across days, and a demo pass's random number is made new for each pass, so holding them longer adds nothing. /api/cron/site-prune deletes older rows nightly, from site_views, site_clicks and site_demo_events. |
Documents held for special-category content (client-documents bucket, quarantined_at set) |
The firm's quarantine window (default 14 days, 1–90), enforced — unless an adviser keeps the document | Added 26 Sep 2026. The daily retention job deletes a held document, row then file, when its window lapses with nobody having acted. An adviser can instead keep it in the client's file ("Keep it in the file", POST /api/documents/[id]/release), which records released_at / released_by; a kept document leaves this purge and follows the client record's retention, like any other client document. The purge deletes a held document's row only while released_at is still empty, and its file only after that row is gone, so a keep made while a run is in progress still holds; a file that then will not delete is reported to ops by its storage key (ids only) for removal by hand. |
| Backup window | 7 days (daily automated backups, Supabase Pro) | Erasure propagates as backups expire, not instantly. Point-in-time recovery is not enabled. |
| Off-site backup (Backblaze, since 23 Oct 2026) | 5 days, enforced, plus 1 day for Backblaze to remove the deleted file: within 7 | The database and every stored file, encrypted. Each night the job forgets snapshots more than 5 days old and prunes everything they alone held; the bucket's lifecycle rule then has Backblaze delete the hidden file a day later. Age-based on purpose, so a gap in backups cannot keep an old copy alive. Logins are not included (runbook). |
Enforcement — what the code actually does
The six-year period is now enforced, not aspirational (migration 067, the
daily retention-purge job). Reviewed 2 Sep 2026:
- An account-closed state with a recorded termination date.
firms.closed_at, set by a database trigger off the billing mirror rather than by application code, so no code path can forget it.canceledandincomplete_expiredare closures;unpaiddeliberately is not — that is recoverable dunning, and treating it as termination would start a deletion clock on a firm whose card merely expired or whose payment merely bounced.incompleteis not a closure either — including one whose first payment is still clearing (subscriptions.payment_processing, 28 Sep 2026; a flag kept with the billing record, on its six-year term). Resubscribing stops the clock, unless the firm was already purged. - A scheduled purge with an audit record. Runs daily at 03:20. The deletion
itself is a SQL function that refuses to touch a firm that is not closed or
not yet due, so a bug in the caller cannot delete a live firm's records.
Each run writes a
retention_purgesrow holding counts per table, never content, which outlives the data it describes. Stored FILES (client documents, and any meeting audio a firm was still holding) are deleted before the rows, because SQL cannot remove Supabase storage objects and a row-only purge would leave the documents themselves in the bucket forever. The list of files is read in full however many there are (14 Oct 2026: before, it stopped at the first 1,000, and the rows of the rest went with their files still in storage). A file that will not delete aborts that firm's purge so it retries, rather than reporting a success that did not happen. - Orphan detection. Any client row without a
firm_idis a row the purge cannot see. The job counts them on every run and alerts if any exist — data we have promised to delete and cannot attribute is a louder problem than any single purge failing. - A tested restore path proving erasure survives a database restore.
The procedure is written, and the scheduled
purges are self-healing across a restore, because both select by elapsed
time and the restore rewinds their own completion stamps. On-request
erasures are now recorded too:
erasure_log(migration 106) captures client deletions via a database trigger and partner job erasures directly, and each one also emits anerasure.recordedevent to Sentry, which is the copy that survives a rewind of the register itself. What remains: the restore has not yet been rehearsed, and erasing a single document, fact find, file note or letter while the client remains is still not in the erasure register (a document's deletion has been on the audit trail since 14 October 2026, which a restore rewinds too). Both are tracked in the runbook.
The firm row, its subscription and its billing history are deliberately kept: those are Crestwell's own records under its own six-year obligation, and they are the anchor the audit row hangs from.
Nothing has aged near six years yet, so the job runs and finds nothing due — which is the correct behaviour, and is why its first real execution must not be the first time anyone has watched it work.
The in-life schedule (the first table's two schedule rows) is worked out once a day, and the database checks again before anything goes. It refuses to erase a client, or to delete an ID, income or bank document's file, unless the firm is open and on the standard schedule, the client has been on Crestwell for 12 months, completion (or, with none, the last activity) was 6 years ago, no hold is in place, and the 30 days' notice for that date has run; erasing a client also needs 3 years since anything last happened on it. A mistake in the daily job cannot delete anything inside those floors.