Data inventory

What the app stores about a learner, where, why, whether it leaves the server, and for how long. It is the record of processing behind the privacy policy, taken from the schema in src/db.rs (migration v25). Not legal advice; the legal decisions are in TODO.md (“Decide the privacy facts”).

src/privacy.rs lists every user table in USER_TABLES: the data export reads them, account deletion removes them, and a test fails when a table is added to the schema without being listed there. Add new tables here too. The export copies only the columns privacy::EXPORTED names for each table; the ones it keeps out on purpose (secrets, codes, links, message text) are in privacy::WITHHELD, and a test fails when a column is in neither (#429).

In the database (SQLite, SKRONGLE_DB)

Every row below is removed when the account is deleted (ON DELETE CASCADE from users(id), directly or through the owning course or theme), and is included in the export unless noted. The exceptions are sms_sends and privacy_actions (privacy::KEPT_ON_DELETE): their rows lose their account and stay (sms_sends until purged). ai_spend_days (privacy::SHARED_TABLES) belongs to no account: it is the server’s daily AI spend, jobs (also shared) is the background job queue, and content_check_worker (also shared) is when the content-check worker last checked in.

TableColumns with personal dataWhyLeaves the server?Kept
usersemail, first_name, last_name, avatar, theme (no longer read since #577: a theme is a course’s setting), active_course, created_at, google_sub, age_confirmed, developer (a developer account, v24; nothing reads it since #298, and its mock_ai flag was dropped in v40), ai_off (“Hide all AI features”, v27), email_verified_at (when the address was confirmed, v29), superuser (the Superuser section, v30), avatar_color (the colour behind the picture, v34), review_quiet (the review of AI changes does not open by itself, v38), phone (the profile’s phone number, E.164) and phone_verified_at (when a texted code confirmed it), two_factor (the second sign-in step in use: ‘’, sms, email or totp; v39), lang (the language the app is shown in, ‘’ = the browser’s; v42), billing_ref (a random reference Stripe’s subscription metadata carries instead of the account id) and stripe_customer (the account’s Stripe customer id; v45), motivator (the motivator worn, ‘’ = the default; v48; since v51 only what a newly opened course starts with), ai_credits (prepaid Skrongle AI credit, micro-dollars, v53), signup_credit_at (when a new account got its one-time Skrongle AI credit, v87, #931), own_keys_fallback (on Plus, “use my keys when my Skrongle AI limit is hit”, v54), must_change_password (1 after the owner set a temporary password, until the account picks its own; v62, #635), phone_opted_out_at (when a text to the number was refused because it replied STOP; cleared when a text gets through or the number changes; v69, #969), suspended_at (when a superuser suspended the account for a breach of the Terms; v70, #967), beta (made through an e-mailed waitlist invitation; v86, #934), delete_at (when an account whose owner asked to delete it is deleted: 14 days after asking, or the end of a cancelled plan’s paid time if later; v88, #970); ai_effort (the effort Generate the course last ran at: low, medium or high; v92, #918); A password account whose address was never confirmed is deleted, with everything in it, when the address is registered again SKRONGLE_UNCONFIRMED_DAYS (default 7) days or more after it was made (#379). password_hash (argon2id; '' for Google-only accounts)the account and profileno; google_sub/email came from Google at sign-inuntil deleted. The export leaves out password_hash
sessionssession_token (the SHA-256 of the cookie’s random 256-bit token; the token itself is never stored, since v36), created_at, expires_atstaying signed inno30 days, purged hourly; deleted on logout. No IP address or user agent is stored. The export leaves out the token
user_progressmastery per lesson, read_atadaptive practicenountil deleted
quiz_historyevery answer: selected_answer (the learner’s text), was_correct, answered_atspaced practice, history pagenountil deleted; answers older than SKRONGLE_RETAIN_HISTORY_DAYS (default 730 days, two years; 0 keeps them) are deleted hourly
journey_progressGuide steps, stars (its xp column is no longer written, #558)the Guidenountil deleted
user_vocabcollected words, review countsvocabulary reviewnountil deleted
lesson_passesseen/passed per lesson, evidence (an agent’s note)one “passed” for every modenountil deleted
game_scoresscores per game boardleaderboards: other learners of the course see the first name and last initial next to the scorenountil deleted
game_activityfinished rounds: durations, counts (its xp column is no longer written, #558)the home page and historynountil deleted; rounds older than SKRONGLE_RETAIN_HISTORY_DAYS (as quiz_history) are deleted hourly, so streaks and the games’ recent bests reach back that far (round counts are kept in game_rounds)
activity_logkind of activity (an answer, a game item, a flashcard, a lesson read…), how many (n), atthe daily goal’s effort points and the streak (src/activity.rs, v47)nountil deleted; rows older than SKRONGLE_RETAIN_HISTORY_DAYS are deleted hourly, as quiz_history
lesson_touchesper lesson: the mode it was last opened or worked on in and when (at)where every mode resumes, and in which mode Continue opens it (v47); Social counts the learners on a lesson in the last hour, anonymously (#501)nountil deleted; one row per lesson touched
lesson_notesper lesson: the learner’s own note (body, plain text up to 500 characters) and when it last changed“My note” on the lesson page (src/notes.rs, v93, #336)nountil deleted; emptying the note deletes its row
game_roundsper course and game: how many rounds the learner finished“rounds played” on the Pictures, Listening, Numbers and Dates pages without counting game_activity (src/games.rs, v64)nountil deleted; one row per course and game
earningsper course: effort points ever logged, daily goals reached, the local day last active, its day_points and the day streakXP, Skrongle Cash and the streak, without reading the whole activity log (src/earnings.rs, v56)nountil deleted; one row per course
motivator_purchasesper course: the Skrongle Cash shop badges bought (item), the price paid and whenthe badges on the SKC hero and the SKC balance, earned less spent (src/motivators/shop.rs, v68)nountil deleted; one row per badge per course
projectscourses opened, when, the level started at, the theme, profile picture and motivator worn in each, and whether its first-visit settings were donethe course pickernountil deleted
user_api_keysprovider, ciphertext (ChaCha20-Poly1305, bound to user and provider), updated_atAI features with the learner’s own keythe decrypted key goes to that provider with each calluntil removed or deleted. The export lists the providers only
subscriptionsStripe customer_id, subscription_id, status, price_id, period end, the plan it pays for (v55)paid planthe customer id is Stripe’s; card details never reach the serveruntil deleted. Deletion is refused while a subscription would keep billing; the Stripe customer itself stays at Stripe
plan_grantsPlus without paying, notegrantsnountil deleted
plan_cancellationsthe plan cancelled, the reason picked (one of four) and the feedback typed, when (v54)why learners leavenountil deleted
ai_usageone row per model call: purpose, provider, model, token counts, cost_micros, credit_micros, key_source (hosted or own), the course and jobhosted AI allowances and credits (src/metering.rs); own-key calls are logged at cost 0no (the call itself went to the provider)until deleted
ai_usage_monthsper calendar month (UTC): the hosted cost_micros, the credit_micros part, hosted_calls and own_calls, the running totals of the learner’s ai_usage rows (v65)the monthly allowance without adding up the month’s calls (src/metering.rs, #624)nountil deleted; one row per month with AI use
blocked_ipscidr (an address or range a superuser blocked), created_at; no account (v70, #967)refusing requests from it (src/moderation.rs)nountil a superuser removes it. Not exported (it is about no account)
ai_spend_daysper UTC day: everyone’s hosted cost_micros, no account (v65)the daily spending cap (#623)nokept; one row per day. Not exported (it is about no one); deleting an account lowers that day’s figure
jobskind, payload_json, state, run and lease times, last_error; no account (v72)background jobs (src/jobs.rs, #872): today only the hourly housekeeping, whose payload is empty (one of them, housekeeping.deletions, deletes the accounts whose delete_at has come, #970)nodone rows deleted after a day, failed ones after 30 days (hourly housekeeping). Not exported (it is about no one)
ai_credit_purchasesStripe Checkout session_id, payment_intent, price_id, amount paid, credit added and refundedcredit packs: applied once, refunds take back their sharethe payment is Stripe’suntil deleted
terms_acceptancesTerms version accepted, accepted_at (no IP address)the record that the learner accepted the Termsnountil deleted; one row per version accepted
terms_announcementsTerms version, mailed_atthat the account was e-mailed about a material change of the Terms 30 days ahead, so it is mailed once (#966)no (the message itself is a mail_outbox row)until deleted; one row per announced version
breach_noticesnotice (a hash of the notice’s subject and body), mailed_atthat the account was e-mailed a breach notice from /superuser, so the same notice is mailed once (#1068)no (the message itself is a mail_outbox row)until deleted; one row per notice
consentsan optional consent’s purpose (ai: AI features), granted_at, withdrawn_at (v82)the record behind the profile’s Privacy and consent card (src/consent.rs, #974): when each consent was given and taken backnountil deleted; a new row each time a consent is given again
mail_outboxto_addr, subject, body_text/body_html, purpose, delivery state, providere-mail the app sends (src/mail.rs)yes: the address and message go to the mail provider that sends itwhile queued, a sign-in or two-factor code (login-code, two-factor-confirm) or a confirmation link (email-verify) is in the body in clear: that is the message being sent. Codes are never in the subject. Once a row is sent or has failed for good its bodies are emptied, and so is the subject of those code- and link-bearing purposes (mail::SECRET_PURPOSES); the sender also empties settled rows when it starts (#361). All of this is skipped in dev mode, whose outbox page shows them. Sent and failed rows deleted after 30 days. Deleting an account deletes its rows, except its address-confirmation e-mails of the last 24 hours, which stay without body or account (and unsent) until that day is over so the per-address cap still counts them (#395). The export has purpose, recipient, state and dates, never subject or body. Rows about no account (user_id NULL) are not in any export. While the job runner sends a row it holds a lease on it (locked_by, the server’s process id and start time; locked_until), cleared once the row is settled; never exported (#873)
sms_outboxto_number, body_text, purpose, delivery state, providertext messages the app sends (src/sms.rs); the send limits count sms_sendsyes: the number and text go to the SMS provider that sends itwhile queued the text, often a code, is in clear; it is emptied once sent or failed for good, and at start for older settled rows (kept in dev mode); sent and failed rows deleted after 30 days. The export has purpose, number, state and dates, never the text; rows about no account are not in any export. The send lease (locked_by, locked_until, #873) is cleared once settled and never exported
sms_sendsnumber_key (SHA-256 of the number; the number itself is not stored), kind (signin or other), created_at, user_idthe send limits (#362): per account, per number and for the servernotwo days, purged hourly. Kept when the account is deleted, with user_id set to NULL, so a new account gets no allowance back
email_verificationsemail the link was sent to, token_hash (SHA-256 of the link’s token; the token itself is never stored), created_at, expires_at, used_atconfirming the account’s e-mail address (src/verify.rs)no (the link itself leaves in a mail_outbox message)a link works for 24 hours, once; unused ones are deleted when a new one is sent or the address changes; every row is deleted 7 days after it was made (hourly housekeeping). The export leaves out token_hash (the link itself is never in it: mail_outbox bodies are not exported)
password_resetsemail the link was sent to, token_hash (SHA-256 of the link’s token; the token itself is never stored), created_at, expires_at, used_atchoosing a new password from /reset (src/reset.rs, v66)no (the link itself leaves in a mail_outbox message)a link works for an hour, once; unused ones are deleted when a new one is sent or one is used; every row is deleted 7 days after it was made (hourly housekeeping). The export leaves out token_hash
totp_keyssecret (the authenticator app’s TOTP secret, encrypted at rest like user_api_keys: ChaCha20-Poly1305 under the server’s master secret, bound to the user; codes are worked out from it, so it can’t be hashed), created_at, confirmed_at, last_step (the last code’s time step, so a code works once)two-factor sign-in with an authenticator app (src/two_factor.rs, v39)no (shown once to the learner while setting the app up)until removed, another method is chosen, or deleted; an unconfirmed set-up is deleted after a day. The export leaves out secret (encrypted or not, it would let anyone make codes) and last_step
phone_verificationsphone the code was texted to, code_hash and salt (SHA-256 of the code; the code itself is never stored), attempts, created_at, expires_atconfirming the profile’s phone numberno (the code itself leaves in an sms_outbox text)10 minutes, five tries; deleted when used, replaced or the number changes, expired rows hourly. The export leaves out code_hash, salt and attempts
two_factor_challengestoken_hash (SHA-256 of the twofa cookie’s token), method, code_hash and salt (SHA-256 of the texted or e-mailed code with the token), attempts, created_at, expires_at, sent_by (email when a texted code went by e-mail because the number replied STOP, v69)a sign-in waiting for its second stepno (the code leaves in a mail_outbox or sms_outbox message)10 minutes, five tries; deleted when used, cancelled or replaced, expired rows hourly. The export leaves out token_hash, code_hash, salt and attempts
two_factor_confirmationsmethod, code_hash and salt (SHA-256 of the code with a random salt), attempts, created_at, expires_ata Google-only account (no password) turning its texted or e-mailed second step off, switching it, or deleting the account: the code sent to prove itno (the code leaves in a mail_outbox or sms_outbox message)10 minutes, five tries; deleted when used or replaced, expired rows hourly. The export leaves out code_hash, salt and attempts
coursestitle, brief (what the learner asked for), notes (pasted material), outline, error, taken_down_at (when a superuser took it down, #967), owner_emailed_at (when a superuser last e-mailed the owner why it was flagged or taken down, #1035)user-made and AI coursesbrief, notes and the course go to the chosen LLM provider while generating or revisinguntil deleted; notes emptied once an AI course is ready (SKRONGLE_KEEP_NOTES=1 keeps them); an AI course whose generation failed is deleted, notes and files with it, after SKRONGLE_FAILED_COURSE_RETENTION_DAYS (30) days without a resume or edit
course_filesthe course’s JSON and SVG filesthe coursesent to the provider when revising or adding a lessonuntil deleted
course_undo, course_undo_filesearlier versions (ten steps)Undonountil deleted, ten steps per course
course_ai_runs, course_ai_run_filesthe files the latest AI run changed, before and after, what the learner left out of it (excluded, v38) and their own text for lines of it (edits, v41)Review AI changesnountil deleted; one run per course, replaced by the next
course_sharesemail (an address the owner typed to share a course with), token_hash (SHA-256 of the invitation link’s token; the token itself is never stored), user_id (the account that opened the link), created_at, opened_atsharing a course with other learners (src/share.rs)the address gets the invitation through the mail provider (a mail_outbox message)until the owner removes the address or deletes the course; a deleted recipient account unlinks the row (the address stays on the owner’s list). In the export of the owner (their shares) and of the recipient (the shares opened by their account). The export leaves out token_hash
course_flagsstate (clear, flagged or overridden), source (which check), matches_json (the listed words found, with the file, field, lesson and the text around them), reason, content_hash (SHA-256 of what was checked), checked_at, override_note, overridden_by and overridden_at (a superuser’s decision), terms_version and ai_version (the rules each check ran under, v78, #1039)the content check of made courses (src/course_flags.rs, v74, #1033)noas long as the course. In the owner’s export, without content_hash and overridden_by
content_scoresper part of a made course (chunk: the lesson file, #2… for a long one’s later parts): lesson (its title), hash (SHA-256 of the text sent), high (the categories it scored high on, v80, #1046), model, input_tokens, cost_usd, review (queued for the AI check’s stage 2, then its verdict: fine or harmful), scanned_at, ai_version (the rules it was scored under, v78, #1039)the AI content check, stages 1 and 2 (src/content_check.rs, v75, #1036; content_check/review.rs, #1037)the lesson’s text goes to TypeSafe (Jev) to be scored; a part that scored high goes to the hosted AI provider for reviewas long as the course. In the owner’s export, without hash
content_category_scoresper part of a made course and category of the AI check (misinformation, violence, self-harm, weapons, firearms, hate, tragedy, adult, minors, sexual crimes, terms): score (Jev’s probability)the AI content check, stage 1 (src/content_check.rs, v80, #1046)no (the text it scores is sent with content_scores’ part)as long as the course. In the owner’s export
content_checkschecked_at (when the content check last read the course’s files)the nightly content check (src/content_check/nightly.rs, v77, #1038): a course edited since is checked again at midnightnoas long as the course. In the owner’s export
content_check_workerthe worker’s name (process id and start time) and seen_at; no account (v79)skrongle content-check checks in every minute (src/content_check/worker.rs, #1041); the course table on /superuser shows itnoone row, overwritten. Not exported (it is about no one)
signup_invitestoken_hash (SHA-256 of the invitation link’s token; the token itself is never stored), created_by (the superuser who made it; empty for the owner link, whose token is also written to the server log at startup while there is no account), created_at, expires_at, used_at, used_by (the account that registered with it), email (for an e-mailed waitlist invitation, the address it is for; v86, #934)sign-up by invitation (SKRONGLE_SIGNUP=invite, src/signup.rs, v61, #634)no (the superuser copies the link and sends it themselves)a link works once, for 7 days (the owner link for a year, replaced at each start, deleted at startup once an account exists); rows are deleted 30 days after they expire, when revoked, or with the account that made them; a deleted invited account unlinks the row. In the export of the maker and of the invited account. The export leaves out token_hash
waitlistemail (lower case), joined_at, confirm_hash (SHA-256 of the confirmation link’s token, until it is used), confirmed_at, invite_id, invited_at, invites_sent, signed_up_at, user_id (the account the address signed up as)the waitlist for a slow release (src/waitlist.rs, v86, #934; hosted only)the address gets the confirmation link and, when invited, the invitation through the mail provideran address never confirmed goes 30 days after it joined; a confirmed one stays as the release record, and goes with the account it signed up as. Someone without an account asks the operator to remove it. In the export of that account, without confirm_hash
release_settingswhether the waitlist is on; no accountSuperuser → Release (#934)noone row, overwritten. Not exported (it is about no one)
privacy_actionssuperuser_id (who acted), action (export, forget, delete-account, sign-out or notice), reason (typed by the superuser), created_atthe log of privacy requests answered, accounts deleted, accounts signed out and breach notices sent from /superuser (src/privacy_requests.rs, v85, #981; src/breach.rs, #1068). It names no learnernokept as a record; when the superuser’s account is deleted its rows stay without superuser_id (privacy::KEPT_ON_DELETE). In the superuser’s own export
content_reportsuser_id (the reporter; empty for a copyright notice sent signed out), course_id and course_title (the course reported), location, name and work (copyright notices), reason, email (the reporter’s address), details, good_faith, created_at, state, forwarded_at, resolved_atreporting a shared course and the copyright form (src/content_reports.rs, v68, #968)the operator gets a copy by mail; Forward to owner mails the course’s owner a copy, with the reporter’s name and address only on a copyright noticekept as a record after the course is gone; deleted with the reporter’s account. In the reporter’s export
user_themes, user_theme_filesthemes the learner madethemesnountil deleted
data_exportsstate (queued, building, ready, downloaded, expired, failed), token_hash (SHA-256 of the mailed download link’s token; the token itself is never stored), file (the file’s name in the exports folder), link_base (the site the link points to), bytes, error, created_at, ready_at, expires_at, downloaded_at“Download my data” built in the background (src/exports.rs, v44, #430)no (the link leaves in a mail_outbox message with purpose data-export)the link works once, for 24 hours; the file is deleted when downloaded, when the link expires (hourly housekeeping) or with the account; rows are deleted 30 days after they were made. The export leaves out token_hash, file, link_base and error

The memo in #74 (written before migrations 15–22) said there was no Stripe code; there is (src/billing.rs, migration 15). It also predates user_themes (v20), course_undo (v21) and age_confirmed (v22).

On disk besides the database

  • Data exports (<SKRONGLE_DB>.exports/, e.g. /var/lib/skrongle/learn.db.exports/): the JSON file of a “Download my data” export, written a page at a time by the export job while it is built (<id>-<random>.json.part), then renamed. It holds what the export holds (privacy::EXPORTED). Deleted when downloaded, when its link expires after 24 hours, or when the account is deleted; files no row names are deleted by the hourly housekeeping.

In memory only

WhatWhyLeaves the server?Kept
AI tutor chats (tutor::Sessions): the transcript, the provider with its decrypted keyAI tutoringthe whole transcript goes to the provider each turn2 hours idle, or a restart; dropped at account deletion
Generation jobs (ai::Jobs): the request incl. notes and the decrypted key, the progress logAI coursesyes, to the provider6 hours after finishing, or a restart; cancelled at account deletion
Timed game sessions (games::Sessions)Word Match timingno30 minutes; dropped at account deletion
Rate-limit counters (after #121): per address or account id, also learning writes per account (#381)password guessing, scripted floodsnominutes to an hour
Profile trips to Google (oauth::StepUps): the state, the account id and what for (link, confirm); accounts that just confirmed with Googlelinking Google, a Google-only account proving it is its owner (#360, #376)no10 minutes, used once, or a restart

In the browser

  • Cookies: sid, the session (HttpOnly, SameSite=Lax, 30 days, Secure with SKRONGLE_SECURE_COOKIES=1); oauth, the Google sign-in state (10 minutes); twofa, a sign-in waiting for its two-factor code (10 minutes). All strictly necessary. The cookie notice lists them all for learners, with signup, signup_invite, lang and tz. No analytics, no third-party scripts, fonts or stylesheets: every asset is served by the app. A theme may name its pictures by https:// URL, which the browser then fetches from that host.
  • localStorage: the voice and audio switch (speech:*), game and listening settings (numbers:, dates:, listening:), sprint-sound, vocab-side, term. Never sent to the server.
  • Read aloud uses the browser’s speech synthesis; some browsers’ online voices send the text to the browser’s vendor.

Leaving the server

ToWhatWhen
Anthropic, OpenAI or Google (Gemini), or a custom OpenAI-compatible base URLthe learner’s key, the system prompt, their notes/brief/feedback, course files, tutor transcripts, their first name (tutor prompt)only when the learner uses an AI feature with their key. The forms say so (data-testid="ai-disclosure")
The hosted AI provider (Skrongle AI’s key, SKRONGLE_AI_PROVIDER)the parts of a made course that Jev scored high, with the course title, languages and lesson list, other lessons the model asks for, and a superuser’s override note; no account detailsthe AI content check’s stage 2 (#1037, hosted only): only for parts that scored high
Googlethe OpenID Connect sign-in“Continue with Google”
The mail provider(s) in SKRONGLE_MAIL_PROVIDERS (e.g. Amazon SES)the recipient’s address, the subject and the messageeach e-mail the app sends; none while unset (messages are only logged)
Resend (a resend kind in SKRONGLE_MAIL_PROVIDERS)the recipient’s address, the subject and the message, over its HTTPS APIeach e-mail that provider sends
Postmark (a postmark kind in SKRONGLE_MAIL_PROVIDERS)the recipient’s address, the subject and the message, over its HTTPS API (plus the purpose as its tag and the outbox id as metadata)each e-mail that provider sends
Brevo (a brevo kind in SKRONGLE_MAIL_PROVIDERS)the sender and recipient addresses, the subject, the text and HTML body, and the message’s purpose as a tag, over its HTTPS APIeach e-mail that provider sends
The SMS provider(s) in SKRONGLE_SMS_PROVIDERS (e.g. Twilio, or the operator’s webhook)the phone number and the texteach text the app sends: the code confirming the profile’s phone number, and sign-in codes with text-message two-factor sign-in; none while unset (texts are only logged)
Stripethe account id as client_reference_id, e-mail for CheckoutSubscribe / Manage subscription
TypeSafe (Jev, TYPESAFE_API_KEY)the text of a made course, a lesson at a time, with the course and lesson titles; no account detailsthe AI content check (#1036, hosted only): when an AI generation or revision finishes, before a share, and each night at midnight for courses edited since their last check (#1038), for the lessons that changed
Hostingeverything above, at restalways: [PROVIDER/REGION] (TODO.md)

Logs

tracing to stdout (journald under systemd). Lines name users by id, never by name or address (sign-in, registration, profile, keys, courses, games, tutor errors, export, deletion). Model errors are scrubbed of the key (ai::redact_key, tested). Mail lines name the outbox id and purpose, never the address, subject or text (also when no provider is set and mail is only logged, #361); provider errors quoting an address are scrubbed (mail::scrub). Text message lines name the outbox id, purpose and the number’s last two digits; digit runs in provider errors are scrubbed (sms::scrub). Generation progress lines carry titles and counts (“Building from your material (1200 characters)”), never the notes. The app logs no IP addresses (#121’s rate-limit warnings do log the address). On the hosted site Caddy writes no access log, and the journal keeps no line older than 31 days: scripts/deploy-aws.sh installs a journald drop-in with MaxFileSec=1day and MaxRetentionSec=30day (#972). On Skrongle mini the operator sets the journal’s retention and the proxy’s access log, which records IPs.

Backups

A deleted account stays in backups until they expire. On the hosted site no backup is kept longer than 31 days (#972; the nightly copies are #964). On Skrongle mini the operator makes the backups and decides how long a deleted account stays in them.

Deleting an account deletes its rows; nothing is kept in anonymized form. So after a deletion nothing personal stays on the hosted site longer than 31 days, the limit the privacy notice gives.