Privacy Policy
Last updated:
This site is a personal portfolio. It sets no cookies and runs no advertising. Since 18 September 2026 a counted page view carries a visitor number that is the same for your browser all day, different tomorrow, and erased from the row after 30 days; section 2.2 sets out exactly what it is. Everything else stored here is what you deliberately send: an email address if you create an account for the private area, or your name, email address and message if you use the contact form.
1. Who is responsible for your data
The data controller for this website is Stefanos Tzortzoglou, a private individual based in Uppsala, Sweden. This site is a personal portfolio, not a business: nothing is sold here and no company is behind it.
Contact for any privacy question or request: stefanos@tzortzoglou.eu.
Because the controller is established in Sweden, this policy is governed by the EU General Data Protection Regulation (GDPR, Regulation (EU) 2016/679) and the Swedish Data Protection Act (lag (2018:218) med kompletterande bestämmelser till EU:s dataskyddsförordning).
2. If you only browse the site
You do not need an account to read anything on the public pages. You are asked for information only if you choose to send it: by creating an account (section 3) or by using the contact form (section 4). Three things still happen automatically, because that is how the web works:
2.1 Hosting logs
The site is hosted on GitHub Pages (GitHub, Inc., United States). To deliver and protect the pages, GitHub processes technical connection data including your IP address, the requested URL, and your browser's user-agent string. I have no access to these logs and cannot read them.
- Purpose: serving the site and protecting it from abuse.
- Legal basis: legitimate interests, Art. 6(1)(f) GDPR — running a functioning, secure website.
- Transfer outside the EEA: yes, to the United States, under GitHub's Data Protection Agreement and the EU Standard Contractual Clauses. See GitHub's privacy statement.
2.2 Analytics
This section reversed on 18 September 2026. Until that day a counted page view carried no identifier, and this section said so in those words. A counted page view now carries a visitor number, the same for your browser all day and different tomorrow.
If you accept the banner, page views are counted by a small endpoint on api.tzortzoglou.eu, the same application that receives the contact form, running on a virtual private server rented in my own name. No analytics company is involved and nothing is loaded from one: your browser sends one short message to my own server, which sends nothing back.
The number is a 64-bit hash of four things: a secret my server holds, your IP address, your browser's User-Agent string, and which of my two sites you were on. A new secret replaces the old one every 24 hours, and the old one is destroyed the moment the new one is made, so yesterday's numbers cannot be recomputed from anything that still exists. Your IP address and your User-Agent go into the hash and are not written down.
Two page views from the same browser on the same day carry the same number, so they are linked to each other. Tomorrow that browser gets a different number, and the secret that would relink the two days is gone. Anonymity would be a larger claim and I am not making it. Within a single day, one browser's page views can be picked out as a group.
That makes a counted page view pseudonymous data, and under the GDPR pseudonymous data is personal data (Recital 26), so the rights in section 6 apply to it. Consent remains the legal basis for it on this public site, Art. 6(1)(a).
- What a counted page view records: the number, the path of the page (without its query string
or fragment, so anything sitting in this site's own address bar stays in your browser), which of my two sites it
was, the host of the page that linked you here (
www.linkedin.com, not the full address), the time, and four coarse facts about your browser: its name (Firefox, Safari), its operating system (Windows, Android), whether it is a phone, a tablet or a desktop, and which of a handful of width brackets its window falls into. The window width is the one new thing your browser sends, and my server keeps only which bracket it fell into. Since 26 September 2026 it also records the country your IP address belongs to, as a two-letter code (SE,DE). My server looks it up in a country list from DB-IP that it downloads and holds itself, so your address is never sent to DB-IP or anyone else. - What it uses and discards: your IP address and the full User-Agent string. Both are inputs to the hash and neither is written to the page view. The IP address is also where the country comes from; only the two letters are kept.
- What it still does not record: no cookie, no fingerprint read off your hardware, fonts or canvas, and never the full referring URL. The full URL is the one place a search term can ride: your browser hands the referring address over in full, as it does to every site you click through to, and my server reduces it to its host and discards the rest before anything is written. The trimming is real, but it happens on arrival, not before sending.
- What is refused outright: a request whose User-Agent identifies it as a bot or a crawler is not counted at all.
- Where the request goes: to that server, which I run, so unlike the hosting logs in section 2.1, this one connection is to me, not to GitHub. Your IP address arrives with it, because every request carries one, and it is used three times, all in memory: as the key a rate limiter counts against, so that the endpoint cannot be hammered, as one of the four inputs to the hash, and to look up which country it belongs to. It is never written to the page view and never stored.
- If you ask me to find what I hold about you: the answer here used to be that there was nothing to find. Within the current day, a row could in principle be matched to you: someone with your IP address and your User-Agent could recompute the hash while that day's secret still exists and see which rows come out equal. Once the secret is discarded that is no longer possible, and the rows from earlier days are numbers with nothing left to compare them against.
- How long a counted page view is kept: the number is erased from the row 30 days after the page view. The row stays and the counts stay; the field that ties one view to another is emptied. The rest of the row has no scheduled deletion, and if the table ever grows to the point of mattering, rows older than 24 months will be deleted and this paragraph will say so.
- Purpose: understanding, in aggregate, which pages people find useful, and how many separate browsers a day's views came from rather than only how many views there were.
- Legal basis: your consent, Art. 6(1)(a) GDPR. Nothing is counted at all unless you accept it on the banner shown on your first visit, and you can withdraw that consent at any time, as easily as you gave it, from the Cookies & Local Storage page (Art. 7(3)).
- If you decline or ignore the banner: nothing is sent, nothing is counted, and the site works exactly the same.
- Country, which is not here yet: I intend to store a two-letter country code with each page view. The lookup will read a MaxMind database file stored on my own server, and the two letters are all that would be stored. No country is recorded today, and this section will be rewritten before the first one is.
- Before September 2026 this was done by GoatCounter, a third-party service. Nothing is sent there any more; the figures it gathered up to the switchover are frozen, and still readable at steftzor.goatcounter.com.
2.3 Third-party files loaded by your browser
Some files are loaded directly from other providers, not from this domain. When your browser fetches those files, it connects to those providers directly, and they necessarily receive your IP address, your user-agent, and the page you were on. This happens on page load, before you interact with anything. On these public pages that is now one provider on one page: the rendered GitHub statistics images on the Code page. The typeface and every icon are served from this domain. The full list is in the Cookies & Local Storage page.
I do not control what those providers do with that connection data. If you would rather not connect to them, a content-blocking browser extension will prevent the requests; the site remains readable, though those statistics images will not render. The application behind the sign-in loads more from elsewhere than these pages do, including a map, which section 3.3 is about.
2.4 The status page
Added 26 September 2026. Opening /status/ makes your browser ask api.tzortzoglou.eu, my own server, for the latest health checks, and ask again every minute while the page stays open, for the list of recent deploys every two minutes, and once for the data-drift summary. The status badge in the footer of every page asks the same thing once, when you scroll the footer into view, and so does the architecture drawing on /code/, once when the page opens. It is not the page-view counter and needs no consent, because nothing about you is recorded: the request carries your IP address, as every request does, and it is used only in memory, as the key the rate limiter counts against. The answer holds only the state of each service. The checks themselves are made by the server, not by you.
3. If you create an account
Some personal content is shared with specific people through a separate application at app.tzortzoglou.eu, where signing in, registering and the private area all live. Reaching it requires an account, which is approved manually by me, and each account carries a role that decides what it may see. Creating an account is entirely optional — nothing on this public site depends on it.
3.1 What registering stores
When you register, the following is stored:
| Data | Where it is stored | Why |
|---|---|---|
| Email address | Firebase Authentication and Cloud Firestore | To identify your account and let me recognise who is requesting access. |
| Password | Firebase Authentication only, stored as a salted hash | To let you sign in. I never see your password and cannot recover it. |
| First and last name | Cloud Firestore | To address you by name in the application, and so that I can tell approved accounts apart by something more readable than an email address. |
Account status (pending / approved) and creation timestamp; for an account I
invited, which administrator invited it and when the invitation was last sent again |
Cloud Firestore | To run the manual approval step. |
Role (user, superuser or admin) |
Cloud Firestore | To decide which parts of the application the account may see. Only I can set it. |
| Preferences you choose to save: a home location, a home stop and favourite teams (team names) | Cloud Firestore | So the forecast, the departures and the match list open on what you picked, on any device. Each is optional. |
- Purpose: restricting private personal content to people I have approved.
- Legal basis: your consent, Art. 6(1)(a) GDPR, given by ticking the consent box on the registration form. You may withdraw it at any time (see section 6); withdrawal does not affect processing already carried out.
- Processors: Google Ireland Limited (Firebase / Google Cloud) holds the account itself. Data is stored on Google Cloud infrastructure and may be transferred outside the EEA under the EU Standard Contractual Clauses and Google's Data Processing Addendum. The mail this application sends you about the account goes through a second processor, and that has its own section 3.4.
- Retention: until you ask for deletion, or until the account is no longer needed — whichever comes first. There is no scheduled clear-out: a registration I never approve stays where it is until I delete it myself, which I now do with a tool in the admin area that removes the sign-in, the stored document and anything sent from that address in one step, and reports back exactly what it removed.
What is never done with it: your email address is not used for marketing, not added to any mailing list, not sold, not shared with anyone, and not used for profiling or automated decision-making within the meaning of Art. 22 GDPR.
3.2 Page loads inside the application
The signed-in application counts its own page loads, and it does so for everyone who uses it, with no banner and no question. A page load is counted only after the server has confirmed the session behind it: if you are not signed in, nothing is counted and you are sent to the sign-in form instead. Every page load counted here belongs to an approved account holder, or it is not counted. That is a statement about the app's own pages, which is where the counts come from. The endpoint that receives them is open, as a page counter has to be, so a request made by hand, not by a browser, could add a row that no session stands behind.
This changed on 18 September 2026 too. A counted page load used to hold no identifier. It now carries the same visitor number as the public site, computed the same way from the same four inputs, with the application standing in as the second of the two sites. A row here holds the number, the path, that it was the application and not the public site, the referring host, the time, the same four coarse facts about the browser (name, operating system, device class and width bracket), and, since 26 September 2026, the country as two letters. It still does not record which account was signed in, and the counter is never told.
Page loads from one browser on one day carry the same number, so they group together. On the public site that grouping sits inside an audience of strangers. Here it sits inside a handful of approved accounts, which is a much smaller crowd. A day's rows for one browser, read next to the list of people who had access that day, could come down to one person. The number is erased from the row after 30 days, as it is on the public site, and a row is then a path, a time, four coarse browser facts and a country, with nothing joining it to any other row.
- Why you are not asked: the public site's counter measures an audience of strangers, which is something you are entitled to refuse. The application is a service you deliberately signed in to, and counting how often its own pages are opened is part of operating it: knowing what works and what is unused, not measuring an audience. Asking a question I would have to keep working regardless would be the theatre this site avoids elsewhere.
- Legal basis: legitimate interests, Art. 6(1)(f) GDPR, for operating, maintaining and improving a service you are signed in to. This section used to add that the ePrivacy consent rule in Art. 5(3) was not engaged, because nothing is stored on or read from your device. Nothing is stored there. The window width is read from your browser, and Art. 5(3) can reach that, so the claim is narrower than it was and the counting rests on the legitimate interest and your right to object, above and below.
- Object to it: you can, under Art. 21 (see section 6). Filtering your own rows out is not something I can do, because the only thing that would let me find them is the number you would be objecting to. Say so and I will stop counting the application entirely.
3.3 Map tiles inside the application
Two pages in the application draw a map: the race location on /f1/ and the stops on /transit/. The map software is served from my own domain, but the map picture is not. Your browser fetches it a square at a time, as you pan and zoom, straight from tiles.openfreemap.org: OpenFreeMap, a free map-tile service built on OpenStreetMap data. It asks for no account, no key and no payment, which is the only reason your browser can be the one asking, not my server. Your browser already fetches a couple of other files from outside this domain (the sign-in libraries, and on one page an animation), and those are listed on the Cookies & Local Storage page. Tiles are worth their own section because they are not a file. The same script is fetched once and is the same for everybody; which tiles you ask for depends on what you are looking at, and you ask for more of them the longer you look.
Since 18 September 2026 there is a second tile host, on one page and behind a button. The Formula 1 page has a Satellite control that swaps the drawn map for photography, and that imagery comes from tiles.maps.eox.at — EOX IT Services GmbH, who publish a cloud-free composite of the European Space Agency's Sentinel-2 data. It arrives the same way, a square at a time, fetched by your browser. Everything below about what a tile request reveals applies to it exactly as it does to the first host. Nothing is fetched from them unless you press the button. Leave it alone and your browser never contacts them; the departures page has no such button, because photography hides the street names and stop labels that map exists to show.
- What they receive: your IP address, your user-agent, which tiles you asked
for, and the fact that you were on
app.tzortzoglou.euwhen you asked. Those tile numbers are themselves a location: not where you are, but which part of the world the map is showing you and how closely you are looking at it. The address of the page you were on is not sent: this application sets a referrer policy ofstrict-origin-when-cross-origin, so the Referer header on a request reaching them carries the site's name and never the path. That is a narrower statement than it may look, and deliberately so: it is about the header, not about every request this application makes. The counter described in section 3.2, which runs on these very pages, sends a path on purpose, to my own server. And the trimming described in section 2.2 is a different trimming: my server reducing a referring address after it arrives, rather than your browser withholding one before it leaves. Nothing about your account goes with the request: it carries no token, no name and no email address, because it is an ordinary image request and there is nowhere in one to put them. - When: only on those two pages, only once a map is actually drawn there, and only for someone signed in; no page on the public site contacts them at all.
- Why it is not proxied like everything else: every outside service this application takes data from (the weather, the departures, the Formula 1 results, the football and hockey fixtures and tables, the sports headlines) is called by my server, so your browser never meets it. Map tiles are the exception because they are images, a few hundred of them in a session of panning around, and putting my server in front of those would mean running a tile service instead of passing a message along. I chose a map over that, and this paragraph is the price of choosing it.
- A request I have no record of: it goes from your browser to them and never passes through anything I run, so I have no log of it, no count of it, and no way to tell you what they did with it afterwards. What I can tell you is exactly what the code asks for, which is above, and that they are not my processor: there is no contract between us, so whatever they receive they hold as their own controller, under their own terms and wherever their network serves you from, which may be outside the EEA.
- Legal basis: legitimate interests, Art. 6(1)(f) GDPR — the same basis as the rest of the application in section 3.2: showing a departure board or a race weekend on a map is part of the service you signed in to use. Your right to object under Art. 21 applies here too (see section 6).
- If you would rather not: a content-blocking extension, or a browser in strict tracking-protection mode, will stop the requests, and both pages carry on working: the departures and the race details come from my own server and do not depend on the map. A map that cannot load says so instead of showing you an empty rectangle.
3.4 Mail this application sends you
Added 18 September 2026, later than it should have been. Section 3.1 named only Google until today, and mail about your account has been going out through a second processor since this application started sending any. I have now read one delivered copy of each of the five messages below, and every one of them carried the tracking described here. Section 9 says this policy is updated before something new goes live. That did not happen here, and this section is the catch-up.
Five messages exist, and each one is about a single account: an invitation, which I send and you did not ask for; the notice that your registration was approved; a password reset when you ask for one; the confirmation of an address change; and a warning to an address that is about to stop being the one on an account. Nothing else is ever sent to you. Brevo (Sendinblue SAS, France), the same provider named in section 4.2, transmits them, and receives the address each one is going to along with the message itself. Brevo is established in France. Where it or a sub-processor it uses moves data outside the EEA, that happens under Brevo's own data processing agreement and standard contractual clauses, which are the terms this account is held on.
- The link in them is not rewritten. Each message is sent as plain text with no HTML part, because Brevo rewrites links in an HTML body through its click tracker, and three of these messages carry a one-time link: two of them set a password, and the third confirms a change of address. I read a delivered one rather than trusting that. The link arrives character for character as it was sent, and Brevo has turned nothing in the message into a link of its own. Your own mail provider is a different matter, because some scan or open links before you see them, and that happens on their side.
- There is a tracking pixel, and I have not found a way to remove it. Brevo builds an HTML copy of the message even though it was sent as text, and puts two invisible images into that copy, one sized a single pixel and one hidden outright. Both point at r.mail.tzortzoglou.eu, which is worth naming because it looks like mine and is not: it is a subdomain of this site's domain that Brevo operates, for this. If your mail client loads remote images, fetching them tells Brevo the message was opened, along with your IP address, your mail client's user agent and the time. The same address also appears as a line of text at the top of the message, above the greeting, which is how this was noticed at all. Blocking remote images stops the fetch, and nothing is lost by doing it, because the words and the link are already in front of you.
- What the consent setting does, and does not do. This account has Brevo's per-contact tracking consent switched on, does not track contacts whose consent is unknown, and every message is sent marked as one whose recipient has not consented. Brevo documents that combination as what keeps an open from being recorded against the person who opened it. I am repeating their documentation rather than something I can watch from their side, and the same documentation did not stop the pixel being added in the first place. Asking their support to disable transactional tracking on the account is the only route left with this provider, and if that changes this section will say so.
- The delivery log. Brevo keeps a record that it sent you each message. Exactly like the log described in section 4.2, it is not something I can edit, and it expires on their schedule rather than mine. Erasing your account does not reach it; section 6 says so too.
- What it is never used for. Your address is not added to a mailing list, not used for marketing, and not used to send you anything other than the five messages above. There is no list to be on.
4. If you contact me
4.1 If you email me directly
Email sent to stefanos@tzortzoglou.eu is not delivered to a mailbox on this domain. It is relayed by my domain registrar's mail-forwarding service (Spaceship) to a personal Gmail mailbox (Google Ireland Limited), where it is read and answered. Both providers process the message in transit or at rest as part of delivering it.
Its contents, your address, and anything you choose to include are kept for as long as needed to deal with your message and to keep a record of the correspondence.
- Legal basis: legitimate interests, Art. 6(1)(f) GDPR — responding to someone who contacted me.
- Transfer outside the EEA: possible, under those providers' own standard contractual clauses.
- Sensitive information: ordinary email is not end-to-end encrypted. Please do not send anything confidential or sensitive to this address.
4.2 If you use the contact form
The form on the contact page sends what you type to an application I run at api.tzortzoglou.eu, on a virtual private server rented in my own name. Your message is stored there in a database, and a copy is emailed to me.
| What | Where it is stored | Why |
|---|---|---|
| The name, email address and message you type | PostgreSQL database on the server, and my Gmail mailbox | To read your message and reply to it. |
| Your IP address and browser user-agent string | Same database record | To detect and limit automated abuse of the form. |
| The time you gave consent | Same database record | To evidence the consent, as Art. 7(1) GDPR requires. |
- Purpose: receiving and answering messages sent deliberately through the form.
- Legal basis: your consent, Art. 6(1)(a) GDPR, given by ticking the box on the form. The form cannot be sent without it. You may withdraw it at any time (see section 6); withdrawal does not affect processing already carried out.
- Processors: the server is rented from a hosting provider who necessarily stores the data on its hardware. The notification email is transmitted by Brevo (Sendinblue SAS, France) and delivered to a personal Gmail mailbox (Google Ireland Limited).
- Retention: messages are kept for as long as needed to deal with them and to keep a record of the correspondence. Ask, and the stored record is deleted: I have a tool that does it and tells me what it removed. The emailed copy is a separate thing: a copy of your message was sent to my mailbox when it arrived, and is also in Brevo's delivery log. Deleting the database record does not reach either of them. I delete the mailbox copy by hand when you ask; the provider's log is not something I can edit at all, and expires on their own schedule.
- No marketing: an address given through this form is never added to a mailing list, never used for marketing, never shared, and never sold. Nothing you send through the form feeds profiling or automated decision-making within the meaning of Art. 22 GDPR.
- Sensitive information: please do not send anything confidential or sensitive through the form. It is transmitted over HTTPS, but the resulting email is ordinary email and is not end-to-end encrypted.
- Instead of the form: emailing me directly (section 4.1) avoids the server and the database entirely. Both routes reach me.
5. What is stored on your device
No cookies are set by this site. A small amount is written to your browser's local storage: your theme preference, your analytics choice, and — only if you log in — your session token. The full list, and how to change your analytics choice, are on the Cookies & Local Storage page.
6. Your rights
Under the GDPR you have the right to:
- Access — get a copy of the personal data held about you (Art. 15).
- Rectification — have inaccurate data corrected (Art. 16).
- Erasure — have your data deleted (Art. 17).
- Restriction — have processing limited (Art. 18).
- Portability — receive your data in a machine-readable format (Art. 20).
- Object — object to processing based on legitimate interests (Art. 21).
- Withdraw consent — at any time, where processing rests on consent (Art. 7(3)).
To exercise any of these, email stefanos@tzortzoglou.eu. There is no charge, and I will respond within one month. Deleting an account is still something I do, not something a form does. You ask, and I run it: it removes the sign-in, the record about you, and anything you sent through the contact form, and reports back which of those it found. It takes only a short message, and you do not have to give a reason.
One limit is worth stating plainly, because erasure cannot reach past it: anything already sent as mail has left this system. If you used the contact form, a copy of your message was emailed to me when it arrived. If you have an account, the mail this application sent you about it went out through Brevo, who keep a record of having sent it. Deleting the stored records reaches neither those copies nor the provider's delivery log. Section 3.4 and section 4.2 say what happens to each of them.
Deleted records also stay in the nightly backups described in section 7 until those backups expire, seven days at most. They are not restored from there unless the whole database has to be rebuilt, and if it is, I delete them again.
If you think your data has been handled improperly, you can complain to the Swedish supervisory authority, Integritetsskyddsmyndigheten (IMY) — imy.se — or to the authority in your own EU/EEA country.
7. Security
The site is served over HTTPS. Authentication and account data are handled by Firebase, and database rules restrict each account to reading only its own record; account approval can only be performed by me. Passwords are hashed by Firebase and are never visible to me. No system is perfectly secure, but the amount of personal data held here is deliberately kept to the minimum needed to run the approval gate.
Backups. Every night the database on my server is copied in full: the counted page views and contact form messages, plus the server's own operational records. Accounts are held by Firebase, not in this database, so they are not in the copy. The copy goes to a private storage bucket in my own Oracle account, in Oracle's Stockholm data centre, and nowhere else; the only key that can write to it is limited to that one bucket. Each copy is deleted automatically after seven days. It exists so the site can be restored if the main server fails, and is opened only to test that restoring works or to actually restore.
8. Children
This site is not directed at children. Please do not register an account if you are under 16. If you believe a child has registered, email me and I will delete the account.
9. Changes to this policy
If the site starts doing something new with personal data — a newsletter, embedded third-party media, a change of provider — this policy will be updated before that goes live, and the "last updated" date above will change. The contact form in section 4.2 was added this way, and so were the move from GoatCounter to my own counter in section 2.2, the application's own counting in section 3.2, and the maps in section 3.3, which put a new outside host in front of two pages, and so got a section of its own instead of a line in a table.
Questions about this page? Email stefanos@tzortzoglou.eu.