Privacy
What the site collects, where it's stored, and how to request deletion. Updated 2026-07-05.
At a glance
Every data item the site touches, in one table. The sections below give the full detail on each.
| What | Why | Where stored | How long |
|---|---|---|---|
| Email hash (truncated SHA-256, 16 hex chars) | Recognise a repeat visit from the same address and tie uploads to a verified email. No plaintext address is kept. | Account cookie value; folder prefix for your encrypted uploads; the magic-link record in Valkey | Cookie: 7 days rolling, 60-day hard cap. Magic-link record: 10 min. |
| Session cookie (bpm-session) | Anti-abuse: proves you solved a proof-of-work before the tile proxy or plan queue does paid work | Your browser (first-party cookie) | 1 hour |
| Account cookie (bpm-account) | Keeps you signed in after you click the magic link | Your browser (first-party cookie) | 7 days rolling, 60-day hard cap from sign-in |
| Uploaded course (bring your own) | Run the model on your own route. Encrypted in your browser; the key never reaches the server except in memory during a plan. | Ciphertext on Scaleway S3 (Paris); key stays in your browser tab | 7 days of no use, 60 days hard maximum |
| Share-link plan | Let someone open a route you planned. Its own key rides in the URL fragment, so recipients decrypt locally. | Ciphertext on Scaleway S3 (Paris); key in the URL fragment | Expires with the parent upload (7 days / 60 days) |
| Analyzer upload (no donation) | Score a past ride against the model, then hand the result back to your browser | Staged in the cluster's Valkey, redacted on the worker | Up to 5 min, then deleted. Nothing persisted. |
| Donated ride | Improve model calibration. Redacted, GPS endpoints trimmed, keyed by a random browser id (not your email). | Redacted FIT + JSON sidecar on Scaleway S3 (Paris) | Until you ask me to remove it (email the receipt code) |
| Analytics (PostHog EU) | Count visits; if you accept, separate new visitors from returning ones | PostHog EU Cloud (Frankfurt); if accepted, a visitor id in your localStorage | Decline or untouched: nothing on your device. Accept: until you clear localStorage, plus PostHog's own retention. |
| intervals.icu connection | Import your FTP, weight, W′ and power curve so plans and analyzes use your real numbers instead of estimates | OAuth token in your browser's localStorage only. Sent with each request that needs it; the server calls intervals.icu and forgets both token and response. | Until you hit Disconnect (or clear localStorage). Nothing stored server-side. |
| Performance traces (PostHog EU) | Measure server timing under load (request and per-phase durations) so the app stays fast | PostHog EU Cloud (Frankfurt); server-side only, no cookie, no device storage | Structural and timing data only. The querystring is stripped before export: never your route, FTP, weight, CdA, or ride file. PostHog's own retention. |
Who runs this
Solo project, run by me out of Brussels. The legal data controller for GDPR is Dropbars Consulting SRL (my one-person company). Privacy questions to privacy@bikepowermodel.fit. Ask what's on file, fix it, delete it. No justification needed.
Cookies
Two cookies, both first-party, both HttpOnly/Secure/SameSite=Strict. Neither identifies you across sites.
| Name | Purpose | TTL | Consent |
|---|---|---|---|
| bpm-session | Anti-abuse: a signed token proving the caller solved an ALTCHA proof-of-work, bound to your IP so it can't be replayed from elsewhere. | 1 hour | Strictly necessary, no consent required (ePrivacy 5(3)) |
| bpm-account | Signed-in session, only set after you click your magic link. Carries a hash of your email, never the address itself. | 7 days (rolling, 60-day cap) | Strictly necessary for the feature you requested |
Each authenticated request extends your session by 7 days from now; after 60 days from sign-in you're asked to magic-link again, regardless of activity. Opening someone else's shared plan link doesn't count as activity: share endpoints are anonymous and never extend the session.
Technical details: cookie internals
Both cookies are stateless signed tokens of the form <ts>.<tag>.<hmac>. There's no server-side session table: the HMAC is what makes the value unforgeable, and the server re-checks it on every request.
- bpm-session:
<ts>.<sha256(ip)[:32]>.<hmac>. Your IP goes into the token as a 32-hex-char SHA-256 tag rather than in the clear, so the cookie can detect cross-IP replay without carrying a readable IP. - bpm-account:
<ts>.<sha256(email)[:16]>.<hmac>. The signed message is domain-separated (prefixedaccount.) so a session token can't be replayed as an account token. Thetsis your original sign-in time, preserved across refreshes, which is how the 60-day hard cap is enforced.
The truncated hashes are equality tags, not secrets: integrity comes from the HMAC, not from the hash length. Both hashes are unsalted, so they keep a readable IP or email out of the cookie but a party who already knows your address or IP can recompute the same tag.
If you download a Power Guide FIT, analyze a ride, or donate one, the site emails you a one-time magic link to confirm the address. Delivery goes through Scaleway Transactional Email (EU-hosted). Your address is sent to Scaleway's send API and not stored on my side. What the server keeps is a truncated SHA-256 of the lowercased address (sha256(email)[:16], 16 hex chars), not the address itself. That's enough to recognise a return visit from the same email and to attach your uploads to a verified address, without a plaintext email sitting in a database.
Be straight about what that hash does and doesn't do. It de-identifies what's stored: there's no readable email, and the hash isn't linked to your ride content. It is not a strong secret. The hash is unsalted and short, so anyone who already knows or can guess your exact address can compute the same value and check for a match. It protects against a database dump leaking readable emails; it does not make you anonymous to someone who already has your address. Scaleway has its own retention policy on sending logs.
Analytics
PostHog EU Cloud (eu.i.posthog.com, Frankfurt), routed through this site's own origin at /api/m/log so ad blockers don't skew the stats. Two modes, picked by your signal:
- You haven't touched the banner, or clicked Decline → cookieless server-hash pageview. Nothing stored on your device, no cross-session identifier, no IP. Just a "visit happened" count.
- Accepted → regular capture with a visitor id persisted in
localStorageso I can see returning visitors vs new ones. Page paths, click targets, web vitals. Geo-IP off on my side.
The browser's Do-Not-Track header is ignored: it's deprecated (MDN) and orthogonal to cookie consent anyway (a DNT user can still actively click Accept). You see the same banner either way.
You can change your mind later by clearing localStorage.bpm.analytics.consent.
What leaves your browser
Beyond the cookies above, the following reach a server when you use the corresponding feature:
- Planner: your slider values (FTP, weight, CdA, effort) + the route id. No IP logging beyond web-server access logs, which rotate on 7 days.
- Download Power Guide: only the job id and your account cookie. The bytes come back to you; nothing extra is logged.
- Analyzer: the FIT you upload to score a past ride against the model.
It's staged in the cluster's Valkey for at most 5 minutes and redacted on the worker before anything reads it: an allow list keeps only the ride telemetry the model scores (power, heart rate, cadence, GPS, elevation, session totals) and drops every identifying field, including the rider-profile name, the device serial number, and any developer or cloud-id fields the head unit wrote.
The result comes back to your browser and the staged bytes are deleted when the job finishes. Nothing is persisted. Your IP and user-agent are not stored.
- Donation (the "donate this ride" button in the Analyzer): same whitelist redaction, plus the first and last 500 m of the GPS track are trimmed to blur where you started and finished. The redacted FIT and a small JSON sidecar are then stored on Scaleway S3 (Paris) for me to review for model calibration. The submission is keyed by a hash of a random id your browser generates, not your email, so repeat donations from the same browser cluster together without naming you. The email body I get excludes your FTP, weight, height, sex, and CdA (see the operator-notification note below). Your IP and user-agent are not stored. To remove a donation later, keep the receipt code and email privacy@bikepowermodel.fit.
- Operator notification email: when you donate a ride, I get a digest email summarising what came in (donor hash, when, planning fields like effort and intent, ride-level metrics like distance and duration). Health-derived fields you enter (FTP, weight, height, sex, CdA) are deliberately excluded from the email body and stay only inside the auth-gated S3 sidecar.
- Planner: bring your own course. A different posture.
The FIT is parsed in your browser; only GPS + elevation survive (rider name, device serial, HR, power, cadence all stripped client-side). That stripped course is then encrypted in your browser with AES-256-GCM under a key generated for this tab. The ciphertext is what gets stored on S3 (Scaleway, Paris); the key never leaves your browser tab except for the instant during a plan request when the server decrypts, runs the model, and drops both key and plaintext when the response returns.
The server is a blind operator for this flow: no copy of the key, no mirror to a sync service, no recovery path. Sign out, close the tab, lose the laptop: the upload becomes unreadable ciphertext. This is deliberate; it's also why there's no multi-device view of your uploads.
Shared links use a second, per-share key that rides in the URL fragment (browsers don't send fragments to the server), so recipients decrypt locally too. Shares expire when the parent upload expires: 7 days of no use, 60 days hard maximum.
- DEM correction and filenames, on the bring-your-own flow. At upload time the server applies DEM-based elevation correction against a SRTM mirror that lives in my own Scaleway bucket (Paris). No third-party data hosts are involved; the tile lookups never leave Scaleway. Your filename stays in your browser as a label and never leaves the tab; a constant internal name is stored inside the encrypted payload, so even if the decryption key were ever compromised, no filename surfaces alongside the coordinates.
Third parties
Hosting is Scaleway (Paris). Email is Scaleway TEM. Analytics is PostHog EU. Map tiles are proxied through this server from a private Scaleway bucket: your browser never talks to a third- party tile host. No Google Fonts, no ad networks, no Facebook pixel, no Cloudflare, no customer-data brokers.
Your rights
You can request access, correction, or deletion of anything associated with your email hash. Email privacy@bikepowermodel.fit. Single operator, no databases of profiles, so I usually respond within a day. Complaints about data handling can go to the Belgian data protection authority (APD/GBA).
If you're a rider whose name appears in the public training dataset on GitHub (pro_weights.json, rides_inventory.json) and want your row removed or pseudonymised, email the same address: same response, no justification needed.
Delete all my uploads. Wipes every encrypted course you've uploaded and every share link you've created. Signing out does this automatically; this button is the same thing while staying signed in.
Two things this doesn't reach. Donations you sent through the Analyzer are stored without your account on them: to remove one, save the receipt code from the donation panel and email privacy@bikepowermodel.fit. And once a recipient opens a share, the plaintext is in their browser: I can't reach across to delete that.