Plain-language policy
Privacy
Last updated: 18 October 2026
TsundereSQL runs in your browser or downloaded app. Your gameplay save stays on your device. The standard release has two game-data network features: an anonymous progress ping that is on by default, with one off switch in Settings, and optional AI tutor or judgment-grading requests that run only when you use those features. The public website is described separately below.
What stays on your device
The game stores progress, XP, streaks, relationship state, unlocked art, settings, and your saved game in local storage on your device. SQL executes locally in DuckDB running as WebAssembly. Your query is not sent elsewhere for execution. If you choose to use the AI tutor, the current query is included in that request as described below. Clearing the game's local data deletes the save. We cannot back it up or recover it.
AI tutor and judgment grading
These features call our tutor proxy only when you use them. A tutor request contains:
- fixed safety instructions and the selected instructor's persona,
- the current problem title, prompt, and table and column schemas,
- your current SQL, up to five recent queries you ran or submitted on that problem, and a short summary of what they returned (row counts, column names, up to three rows of the practice data, or the error or grader result), and
- the messages in that tutor conversation.
We include these recent queries only so the tutor can see what you tried and answer you better. The game keeps them in memory for the current session, not in your save. They are sent only when you ask the tutor something, and we do not store them or use them for anything else.
A judgment-grading request instead contains fixed grader instructions, the scenario, its authored grading rubric, and the free-text answer you submit. Neither request reads your name, email, full save, or data from other sites.
Our proxy forwards these requests to OpenRouter and a language-model provider to generate the response. Before forwarding, it replaces common API-key patterns, email addresses, and checksum-valid payment-card numbers with redaction labels, and asks OpenRouter to route only to providers it identifies as not collecting request data. This is a safety net, not a guarantee that every kind of personal information can be detected, so do not type personal or confidential information into either feature. Generated links are not made clickable, and responses containing write, administrative, file, network, or extension SQL are replaced before display. Read the OpenRouter privacy policy. The proxy uses your IP address for short-lived rate limiting; it does not attach a game account or save identifier to the request.
Anonymous progress stats (on by default, one switch to stop them)
Settings has one switch, Share anonymous progress stats, that is on by default. It exists to answer one question: where do players stop learning SQL. While it is on, the game sends a small ping at these moments, and nowhere else:
- session_start: the game starts once per session, using a same-day, next-day, 2-7 day, 8-30 day, or over-30-day return bucket computed on your device.
- session_surface: the game starts once per session with one coarse surface label: itch, direct web, Windows desktop, macOS desktop, other desktop, or development. The URL, browser, OS version, and user-agent string are not sent.
- referrer_bucket: which kind of place a session arrived from, as one word: steam, itch, youtube, x, reddit, discord, search, direct, or other. Your browser works this out from the referring address and sends only the word. The address itself, including the site name and any path or search terms in it, never leaves your device.
- edition: which build you are playing: free, demo, or steam.
- build_id: which upload of the game this is, once per session: a letter for the edition and the day the build was made, like f260925. Everyone on that build sends the same label.
- viewport_bucket: how wide the window is, as phone, tablet, or desktop.
- reduced_motion: whether your system asks for reduced motion: on or off.
- db_boot_bucket: how long the local SQL engine took to become usable, timed from the moment the game first asks for it: under 2 seconds, 2 to 5, 5 to 10, 10 to 30, over 30, or failed.
- first_launch: the first time the game is ever opened on this device: yes.
- session_length_bucket: how long a sitting lasted, when you close or leave the tab: under 2 minutes, 2 to 10, 10 to 30, 30 to 60, or over 60.
- solves_in_session_bucket: how many exercises that sitting solved: 0, 1, 2 to 4, 5 to 9, or 10 or more.
- module_started: you open a curriculum module for the first time on that device, using only the fixed module name.
- module_complete: you finish a module, using the number of modules finished, 0 to 14.
- you pass a solve-count milestone, problem_solved_bucket (1, 5, 15, 35, 60, 93, 150, 200, 224 solves), once each.
- streak_bucket: you reach a daily-streak length of 2, 7, 14, or 30 days, once each.
- problem_struggle: you reach a third unsuccessful graded submit on an exercise, using only the fixed exercise name once. Never your SQL, answer, error text, or exact attempt count.
- first_error_kind: what the first failure of a sitting looked like, as one of syntax, runtime, timeout, or empty_result. The error message itself is never sent.
- first_attempt_result: how your first Run or Submit on each exercise went, once per exercise per save: the fixed exercise name, whether it was a Run or a Submit, one of ok, parser_error, binder_error, catalog_error, wrong_rows, wrong_columns, timeout, or other, for the three error kinds which beginner slip it looked like (words typed instead of column names, a missing comma, quoted names, or other), and whether you had opened Help on that exercise first. Never your SQL, anything you typed, the error text, or how long it took.
- hint_shown: which automatic hint appeared: typo, case_mismatch, alias, rows_few, rows_many, nullif, or other.
- hint_then_solved: the same seven labels, sent when an exercise is passed after that kind of hint appeared on it. It is the only way we can tell whether a hint helped.
- explain_opened: you opened the query-plan panel: yes.
- review_done: you cleared a spaced-repetition review: yes.
- placement_done: you finish the placement exam, using the module it placed you out of or none.
- placement_skipped: you declined the placement exam: yes.
- stuck_answer: if you answer the one-tap “where did it get hard?” card after the Module 1 boss, the module and the button you pressed: fine, reading, writing, errors, or too_long. Skipping it sends nothing. It is asked once, and only while these stats are on. If you pick too_long you may also type an optional note of up to 140 characters; it is sent, in the same message, only if you type something, and is stored with just the date and that answer, after links, email addresses and phone numbers are replaced with [link] or [contact], and deleted after about thirteen months like the daily totals.
- feature_used: the first time each surface is opened in a sitting: tutor, judgment, gallery, wardrobe, gacha, phone, memories, lore, save_slots, backlog, skip_read, or settings_open. Drills is not on that list and cannot be: opening Drills switches this counting off for the rest of the sitting, however you got there.
- setting_changed: which default you turned off or changed: audio_off, voices_off, spicy_off, romance_off, analytics_off, reduced_motion_on, or language. The analytics_off one is sent before the switch takes effect, and is the last of these progress pings your browser ever sends. It does not stop the separate tutor-proxy counts described below, which count our own server's work and never start in your browser at all.
- route_started and ending_reached: you start a character route or reach an ending, using which one.
- scene_seen_bucket: you reach 1, 5, 15, 40, or 100 story scenes seen, once each. Which scenes is not sent.
- cg_unlocked_bucket: you reach 1, 5, 15, or 30 unlocked artworks, once each. Which artworks is not sent.
- scene_skipped: you used skip-read in a sitting: yes.
- instructor_unlocked: you reach a later instructor: aimi or nora.
- error_boundary: the game crashed, and which part was on screen: workspace, vn, dashboard, gallery, or other. No message, stack trace, or component name is sent.
- db_reset: the local SQL connection had to be rebuilt: timeout, wedged, or manual.
- asset_missing_bucket: a file failed to load, and what kind it was: sprite, cg, bg, or audio. Never the filename.
- save_migrated: your save was upgraded to a newer format: ok or failed.
- outbound_click: you pressed a link that leaves the game, and which one: kofi, steam_wishlist, itch_page, discord, book_vol1, book_vol2, book_vol3, or privacy.
- if you click a visible companion-product vote link, the fixed choice you press (companion_interest: calendar or fan pack). The public website records it once per browser; the local one-vote guard is not sent to the server.
Each analytics ping has exactly two fields: the event name from the list above, and its number or name, plus only the optional stuck_answer note described above. That is the entire message. There is no identifier of any kind in it: no account, no install ID, no device ID, no session ID, no cookie, and no random number standing in for one. Apart from anything you choose to type in that note, it contains no SQL, answer, email, name, or save data.
One thing about that is worth stating exactly, because "no two pings can be linked" would otherwise be too strong. The two end-of-sitting counts, session_length_bucket and solves_in_session_bucket, travel in a single request: your browser works both out at the same instant, as the tab closes, and sending them separately would just be two messages racing a closing tab. So for that one request, and only while it is in flight, those two counts are visibly from the same sitting. Nothing that records it is kept: each one is added to its own daily total and the request is gone. Nothing else is ever sent together — the server refuses a message carrying more than those two — and no request ever carries anything that says which browser or which sitting it came from.
Your browser remembers locally which milestones it has already reported. The server keeps daily aggregate counters, not per-player rows. The analytics application writes no IP address, user agent, or timestamp finer than the date. Daily totals are kept for about thirteen months and then deleted automatically.
Separately from this switch, our tutor proxy counts its own work as plain daily totals: tutor_call with the value chat or judgment (which of the two features the request was, worked out from the fixed instructions our own code put in it, never from anything you typed), and tutor_reject with the value cap, safety, origin, or input (why a request was refused). That is the whole list. Those totals contain nothing from the request and nothing about who made it, and because they count work our server did rather than anything your browser chose to send, they are kept whether the switch above is on or off.
You can turn the switch off in Settings at any time. Turning it off also deletes the local record of reported milestones. The game explains the counting once after the placement exam or first finished module and offers a one-click off button. A previous decision to keep it off remains off.
Steam demo: anonymous counts (on by default, one click to stop them)
The demo sends anonymous counts by default, and nothing until it has told you so: its first screen (or the demo home, for a save from an older demo) says so, with a Turn off button, and the demo home has the same switch. Turning it off stops sending, deletes the local record of what was sent, and stays off.
While on, the demo sends: that a sitting started (once per launch); pressing Start on the first screen; which demo questions you opened, submitted and solved; hints opened, by question and hint number; Run and Submit results by kind (accepted, wrong, ran, syntax, runtime, guard, timeout, oom); Drills opened and a per-sitting drill-solve bucket (1, 5, 15); reaching the end; seeing and pressing Wishlist. Each count is two fields, tagged with the demo build, your platform (Windows or Mac) and where you came from (almost always "direct"). No identifier, SQL, answer or error text. Daily totals only, kept about thirteen months.
Website, update list, and support contact
The public website does not currently require an account. If an email update-list form is offered and you submit it, the site stores your lowercased email address, signup timestamp, and browser user-agent string in Cloudflare's storage so we can deduplicate signups and send product updates. If you email support, your address and message are handled by the relevant mail providers and kept as needed to answer you. You can ask us to remove a waitlist entry or support correspondence at hello@tsunderesql.com.
What we do not do
- No TsundereSQL gameplay account is required.
- No ad trackers, third-party analytics SDK, or advertising cookies are added by the game.
- No analytics profiles are built because the progress pings have no identifier.
- We do not sell personal data. AI request content is shared only to provide the feature described above.
Children
After the first-run adult confirmation, romance and suggestive content are on by default and can be switched off at any time in Settings. Suggestive content is intended for adults: the game asks you to confirm on a content screen before anything is shown, and declining turns it off. The SQL course itself contains no such content.
Contact
Privacy questions or removal requests: hello@tsunderesql.com.