DankChess

Privacy

One page counter, one move relay, one direct line to your opponent.

DankChess is a static site. There is no account to make, nothing to upload, and no database behind it. Exactly three things leave your browser, and on an ordinary visit only the first of them ever does. That one happens on every page: a hit to a page counter. The other two happen only if you press one specific button — Play a friend on the board — and both of them have worked since 5 August 2026: your moves travel through a relay on this same domain to reach the other person, and anything you type in the chat box goes straight to their browser instead of through us, which costs one lookup at a public STUN server to work out the route. This page explains all three, and says where we stop being able to prove things to you.

Every claim below was checked against this site’s own source on 31 July 2026. Every statement about whether online play is running was re-checked against the live service on 5 August 2026, the day the relay was switched on.

The counter

Every page here — the home page, the board at /play/, and this page too — loads one script from a host that is not this one: stats.dankdev.com. It is a self-hosted GoatCounter instance, run by DankDev, the same person who runs this site. It is not Google Analytics and there is no ad network involved: stats.dankdev.com is the only third-party host any page on this site contacts. That had no exception at all until online play was switched on; it now has exactly one, spelled out four screens below — a chat channel makes one lookup at a public STUN server, and nothing else on the site ever reaches it.

These are the two lines, copied verbatim out of the page you are reading:

<script>window.goatcounter={path:function(p){return location.host+p}};</script> <script data-goatcounter="https://stats.dankdev.com/count" async src="https://stats.dankdev.com/count.js"></script>

What that tells you, without any guesswork:

  • The counter’s code is fetched from https://stats.dankdev.com/count.js, and the hit it records is sent to https://stats.dankdev.com/count.
  • The first line is the only customisation this site makes. It prefixes the hostname to the recorded path, so your visit to this page is filed as dankchess.com/legal/privacy/ rather than /legal/privacy/. That is because one counter instance covers several DankDev sites and the paths would otherwise collide.
  • The script is marked async and nothing on any page reads it back. If your browser or content blocker refuses to load it, the counter simply does not happen and every page, including the chess board and the computer opponent, works exactly as before. We would rather say that than pretend you have a cookie banner to click.
  • Because your browser makes that request to another hostname, the request carries what every HTTP request carries: your IP address, your User-Agent string, and the page you arrived from. That is a property of the web, not a choice made in this page’s markup, but you should know it happens.

Where our proof runs out

count.js is not a file in this site’s repository — it is served by that other host at page load. So we cannot show you, line by line, what it transmits, and we are not going to describe a measurement we did not make. GoatCounter’s published design is cookieless and keeps no per-visitor identifier, and we have no reason to doubt it, but treat that as the vendor’s documentation, inherited, not verified here. Everything in the next section is different: it is measured in this repository, and you can check it yourself from the page source with Ctrl-U.

What this site itself does — measured, not asserted

Searching every HTML and JavaScript file this site ships (excluding the vendored chess rules engine, which is a pinned third-party copy) gives the following counts. These count places the code actually calls the thing, not the number of times the word appears — our source comments mention localStorage in order to say it is never used, and a comment is not a call. Zero means the feature is not reached from anywhere in the site.

What we looked forHitsWhat that means for you
document.cookie0No page on this site sets a cookie.
localStorage / sessionStorage / indexedDB0Nothing is written to your device’s storage.
serviceWorker0There is no service worker, so nothing of this site is installed in the background or kept alive after you close the tab.
fetch / XMLHttpRequest / sendBeacon0Nothing on this site sends a request by any of the three ordinary means. No page phones home on load, no page uploads a position, and nothing is posted anywhere when you move a piece on your own board. This row is not a claim that the site never opens a connection — see the next row, which is the one exception and is a WebSocket rather than a fetch.
WebSocket1One socket, opened in one place: html/play/online.js, and only after you press Create a game link or open a link someone sent you. It aims at /relay on dankchess.com itself — no third-party host — and that relay has been running since 20:42 UTC on 5 August 2026, so it carries your moves to the other player. The row would still read 1 on a day nobody connected, because the row counts code, and the code is there. Nothing opens it on page load, and the other two modes (Two players and vs Computer) never open it at all.
RTCPeerConnection1One peer connection, built in one place: html/play/chat.js, and only after two browsers have actually paired for an online game. This is the chat channel, and it is a genuinely different path from the row above — a message you type goes browser-to-browser and does not pass through our relay, while every move does. Two things this costs you, stated here because they are measurements and not marketing. (1) The offer and answer that set the channel up have to reach the other tab somehow, and the only thing the two tabs share at that moment is the relay socket, so the relay sees that these two IP addresses paired and when — it does not and cannot see a message, because a message never travels on it. (2) A direct connection means each player’s browser learns the other’s IP address. That is inherent to peer-to-peer and there is no way to have one without it; it is a fair trade with a friend you already sent a link to, and a bad one with a stranger, which is why the link is not public anywhere. Same naive-grep caveat as the socket: new RTCPeerConnection( gets zero hits, because the constructor is read into a local so the tests can pass a fake in. Search for RTCPeerConnection and you will find it. That handshake rides the relay, and the relay runs, so chat works today for any pair whose networks allow a direct channel; a pair whose networks do not gets no chat and plays on without it. Nobody moderates what is said either, because nobody outside those two browsers can see it.
stun: URLs1stun:stun.l.google.com:19302, Google’s free public STUN server, named once in html/play/chat.js and contacted only while a chat channel is being set up. A STUN server does exactly one thing: it tells your browser what its own address looks like from the outside, so the two browsers can find each other. It carries no messages and no moves. There is deliberately no TURN server — a TURN server is the kind that would relay your messages through a third party’s machine, and adding one would quietly undo the whole point of this row. If no direct path can be found, there is simply no chat for that pair and the game carries on; that is the designed outcome, not a bug.
<form>0There is nothing to submit: no sign-up, no log-in, no email box, no comments, no uploads, no user-generated content of any kind.
new Worker(…)1new Worker('./ai-worker.js') on the board page. That is the computer opponent. It is a file served from this domain, it runs inside your browser on a background thread, and it talks to nothing outside the tab.

The relay, in detail

Until 31 July 2026 the board page carried a line saying online play was not available yet, and this table said the site made no network requests of any kind. Both sentences are retired, for different reasons, so be clear which is which. The table sentence went because shipped code now contains a socket call site, which is exactly what the row above counts — the table counts code, not connections. The board-page sentence went because the relay was switched on at 20:42 UTC on 5 August 2026 and online play works. That second one you can check without taking our word for it: ask https://dankchess.com/relay for an ordinary web page and it answers 426 Upgrade Required — 62 bytes of plain text reading this endpoint speaks WebSocket only. It is a better check than the one this paragraph used to offer, because an absent endpoint can only tell you it is absent, while a 426 tells you the service is there and what it insists on speaking.

So read the next paragraph as an account of something now running, not a forecast. The code is relay/server.js in this site’s repository, written and reviewable, and it is the same file the live process executes — there is no build step and no second copy that could differ from what you read.

Starting or joining an online game opens a WebSocket from your browser to wss://dankchess.com/relay. On the far end is a small Node process that does one thing — it pairs two sockets by a random 12-character game id and copies each byte one sends to the other. It does not decode the frames, so it does not know they are chess. It keeps the pairing in memory in that one process and writes nothing to disk: no database, no file, no queue and no backup of your game. Close either tab and that relay session is gone; the relay cannot bring it back, we cannot bring it back for you, and nothing re-pairs the two of you automatically. Your own board is a separate thing and survives: it rides in your tab’s session-history entry, never on our server, and the board page can hand you a resume link that starts a fresh game from the same position and the same colours — a new pairing you two open yourselves, not a session we restored. Restarting that process does the same thing to every relay session open at that moment, and that is the honest price of keeping no state: on our side there is nothing left to reconnect to. What it records is operational and payload-free — an event name and a count of open games and sockets, so hosted or closed with a number, never a move.

Two honest caveats

First, a move is data you send us, even though we neither store nor read it. It passes through a machine we run. If you would rather nothing of yours transited our hardware, use Two players or vs Computer instead: those never open the socket, they never will, and they are the whole board with nothing removed. Second, if you grep the source for new WebSocket( you will find zero hits, and that is not us hiding — online.js reads the constructor into a local (const WS = globalThis.WebSocket) so it can be swapped for a fake in the tests. Search for WebSocket instead and you will find it. We would rather tell you where a naive search misleads you than let you find it yourself and wonder what else we skipped.

Chat goes straight to your opponent. Your moves do not.

Until 31 July 2026 this page said chat was a design decision in relay/PROTOCOL.md and nothing more. That sentence is retired: it was accurate when it was written and became wrong the moment html/play/chat.js was added, because the code ships, the two rows above count it, and a page that still called it a plan would be describing a different site than the one you are on.

What is true instead is worth saying plainly. Chat is written, shipped and usable — and it is usable because the relay runs: two browsers have to be introduced to each other before a direct channel can exist, and that introduction rides the same relay that pairs your moves. The sequence is relay, then pairing, then handshake, then chat. If the handshake cannot find a direct route — some networks will not allow one — that pair gets no chat and the game plays on regardless. There is no fallback that quietly routes your messages through us, and there is not going to be one.

Here is the split, and it is the only part of this section you need to remember. Moves go through our relay. Messages do not. A message you type goes over a WebRTC data channel straight to the other player’s browser, and the relay has no chat message type to carry one with. relay/server.js contains no such string literal; the single time the word appears in that file is a comment recording the rule, which is a thing you can check rather than take from us. Three caveats survive that split and are not going to be tidied away, because all three are true. (1) The relay still carries the setup handshake, so it sees which two addresses paired and when, though never what anyone typed. (2) A direct connection means each player’s browser learns the other’s IP address. (3) Because the messages never reach us, we cannot moderate them — we cannot see abuse here, so we cannot act on it. The third is the right answer for a private link you sent to someone you know and the wrong one for pairing strangers, which is why there is no public matchmaking on this site. If the direct channel cannot be made — some networks will not allow one — there is no chat for that pair and the game plays on regardless, which is what the board page tells you in one sentence rather than retrying forever.

The Dark / Light button

It sets an attribute on the page and does nothing else. That is also why it forgets your choice when you come back — remembering would mean storing something, and the table above is the whole story.

Fonts and the rules engine come from here

The seven .woff2 faces under /fonts/ and the chess.js rules engine under /play/vendor/ are served from dankchess.com. No Google Fonts, no CDN, no embedded video, no social widgets. The relay described above is on dankchess.com too, so it adds no host. On an ordinary visit, stats.dankdev.com is the only hostname other than this one that anything is contacted at. That sentence used to have no exception and now has exactly one — and since 5 August it is a live exception rather than a theoretical one, so here it is: html/play/chat.js names stun.l.google.com, and your browser contacts it once, but only inside an online game that has already paired two players, and only to work out the route to the other browser. Reading this page, playing the computer, playing someone at the same keyboard, or opening a shared position never reaches it. Other hostnames do appear in the markup — dankdev.com in every footer, and goatcounter.com further up this page — but those are ordinary links, and a link sends nothing until you click it.

The legal bit, in the same plain language

If you are in the EU or UK: the counter is processed under Article 6(1)(f) GDPR, legitimate interests. The interest is knowing roughly how many people reach a page, which is how anyone running a free site decides whether it is worth keeping. What makes that proportionate is the narrowness of it: aggregate page counts, no profile built here, no advertising, no cross-site tracking, and nothing sold or shared with anyone. If you would rather not be counted, blocking one script host is enough, and nothing breaks.

We are not going to quote you a retention period. How long hits are kept is a setting of the counter instance at stats.dankdev.com, not of anything in this site’s code, and we have not measured it — so any number here would be invented. If that matters to you, ask via dankdev.com and you will get the real answer rather than a plausible one. The same route works for asking what a given visit recorded, though be warned: with no identifier stored, there is most likely nothing to look up that is yours specifically.

The relay and the law: the relay has been running since 20:42 UTC on 5 August 2026, so this describes a live process rather than a plan. The same Article 6(1)(f) basis covers passing a move between two browsers, and the interest is simply that the feature cannot exist otherwise. The proportionality argument is different from the counter’s, and rests on three things you can check in relay/server.js: it is opt-in (no socket opens unless you press the button), the payload is never decoded or written down, and there is no retention question to answer because there is no store to retain anything in. What the running process does write down is its own operation and nothing else. There are nine log() call sites in that file, and each one passes an event name — open, hosted, paired, closed, reject, ws-error, client-error, upgrade-rejected, listening — beside counts and short codes: games, sockets, code, klass, port, and the length of a version string. Not one of them is handed a move, a game id or an address. Your IP address does reach that process, as it reaches any server you connect to; it is used to carry the connection and is not recorded against your game.

This page describes the site as it stands on 5 August 2026, the day online play was switched on. If the counter changes or anything else starts leaving your browser, this page changes in the same commit — the WebSocket row and the relay section above exist because that rule was applied to the change that added online play.