PRIVACY POLICY
Last updated: 13 July 2026 · Operator: TFCL (TF2 Competitive League)
This Privacy Policy explains what information TFCL Play (play.tfcleague.com) collects from you, how we use it, and your rights regarding that data. By using the site you agree to the practices described here.
01 Who We Are
TFCL Play is a competitive Team Fortress 2 pickup-game platform operated by TFCL. The service is hosted on Cloudflare Pages (edge network) and uses a Cloudflare D1 (SQLite) database. There are no physical servers under our direct control; all compute and storage runs within Cloudflare's infrastructure.
Contact: reach us via the TFCL Discord server.
02 Data We Collect
We collect the minimum data necessary to run a competitive PUG platform:
When you sign in via Steam OpenID we receive and store your:
- Steam ID (64-bit numeric ID)
- Display name (persona name)
- Avatar URLs (medium and full)
- Steam profile URL
We never receive your Steam password, email address, or payment information from Steam. Steam's OpenID login is handled entirely by Steam's servers.
If you choose to link your Discord account via Discord OAuth2 we store your:
- Discord user ID
- Discord username
We request only the identify scope. We do not receive your Discord email, messages, or server list. You can unlink Discord at any time from your profile dashboard.
We store data you generate by using the service:
- PUG participation history (PUGs joined, team, class, result, your voice-mode preference)
- Match results, scores, ELO changes, and logs.tf links
- PUG Points balance, transactions, and reward redemptions
- PayPoints purchase records (order ID, amount, granted points - separate from Premium purchases)
- Referral code and referral relationship (who referred whom)
- Community posts, comments, and votes you submit
- Announcements you've authored (if you're an admin/mod)
- PUG chat messages (stored per-PUG, associated with your user ID)
- Profile customisations you set (name colour, rainbow name, profile frame, custom avatar, bio, banner, background, accent colour theme)
TFCL Premium members can generate bearer API keys to drive parts of TFCL Play from their own website, dashboard, or bot. We store:
- A SHA-256 hash of the key (the raw key is shown to you exactly once at creation and is never persisted)
- An 11-character visible prefix (e.g.
tfcl_pk_8c4f2e) so you can correlate a key you see in a log to a row in our database without ever exposing the full secret - The scope of the key (
premiumoradmin) - An optional user-supplied label (e.g. "Stats dashboard")
- Creation, last-use, and revocation timestamps
- For each authenticated API call: endpoint, method, hashed-IP (see Technical Logs - we use a keyed HMAC-SHA256 for activity_logs and a salt-prefixed SHA-256 for api_key_audit; both are one-way and irreversible, and only the activity_logs HMAC is a true keyed MAC), HTTP status code
Keys automatically stop working within seconds of TFCL Premium lapsing - the lookup joins users.premium_expires_at on every request. Rate-limited at 60 requests/minute per key; overrun returns 429 with a Retry-After header.
Anyone can buy a TFCL Premium plan as a gift, and admins can mint free codes for promotions. We store:
- A SHA-256 hash of the 16-character code (the readable code is shown to the buyer/admin exactly once)
- A 9-character prefix (
XXXX-XXXX) so an admin can spot a code in a list without re-hashing - Which plan the code grants, in days
- Provenance: whether it was paid (with
paypal_order_id+ amount) or admin-minted (with admin user id + optional free-text note) - A 2-year shelf-life expiry; the redeemer user id and timestamp; admin revocation timestamps if applicable
Redeeming a code stacks the granted days on top of whatever premium you already have, using the same arithmetic as a direct self-purchase. Multiple users holding the same code is impossible because code_hash is unique.
Any signed-in user can create a team. We store the team's identity, roster membership, and any optional join password:
- Team name, tag, optional avatar URL, optional bio, public/private flag, region, and current status (
active/ soft-disbanded) - A URL-friendly slug like
151-envision-crewused for pretty team page URLs - Roster rows (one per member) with role (
owner/captain/member), join/leave timestamps - Roster capacity is locked to exactly 13 members per team (enforced server-side and via a DB CHECK constraint)
- For password-protected teams: a PBKDF2-SHA256 derived key (100,000 iterations, 16-byte CSPRNG salt, 32-byte derived key) stored as four
$-separated fields:pbkdf2_sha256, iteration count, hex salt, hex derived-key. Plaintext is never persisted, never returned by any endpoint, and never logged
Rejected join attempts (wrong password) return a generic 409 with no timing oracle - server-side verify uses constant-time hex comparison. Soft-deleted teams and members remain in the database so historical references (e.g. cup rosters, brackets) still resolve cleanly.
Free community tournaments and short-form CUPS (1-3 day events) collect entrant/roster data and an audit trail of all organizer/admin actions:
- Tournament title, description, format, min/max participants, schedule (start/end, check-in window), and any optional contact/rules text
- Per-tournament participant rows: your team label, team size, status (
registered/checked_in/withdrew/ final position), check-in timestamps - CUP-specific fields: per-team PayPal order id and paid-at timestamps, per-seat
cup_playerpayment rows, and (for paid CUPs) a recipient email needed for prize disbursement via PayPal Payouts - An
entry_modetag on everycup_audit.team_registeredrow:free_open/premium_comp/paid_registered. When a registered team was captained by an active TFCL Premium member on a paid CUP, the platform covers the entry fee and a second audit row (team_comped_premium, recordingfunding_mode+entry_fee_cents) is written so we can report on premium-comped entries downstream. No PayPal order id is stored for comped entries. - Bracket metadata (round, bracket position, scheduled time, server reservation id, scores, winner)
- Invite codes (8-char URL-safe alphabet), visibility (
public/private), and any PBKDF2-hashedjoin_password_hashon private tournaments (same envelope and constraints as team join passwords) - An audit table per tournament/CUP (one row per
created,approved,started,completed,cancelled,edited, and per entrant join/withdraw) recording the acting user, the from/to status, and a JSON detail blob - retained indefinitely
"Prize pools SOON" copy you may see on the tournaments page reflects that the paid-tournament flow is intentionally deferred: the schema already accepts entry fees, prize pools, sponsor stripes, etc., but the UI surfaces a "Coming soon" notice and the API rejects any paid-mode field with a typed error so the feature can be enabled later without a schema rebuild.
If you purchase a TFCL Premium subscription, payment is processed entirely by PayPal. We store:
- PayPal order ID (used only to prevent duplicate/replayed purchase credit)
- Plan purchased, amount charged, and currency
- Premium expiry date resulting from the purchase
We never receive or store your PayPal login, card number, billing address, or any other payment instrument details - that information is handled solely by PayPal under its own privacy policy. All PayPal API calls happen server-side; your payment credentials never pass through our servers.
If you provision an on-demand TFCL Server (Free or Premium tier), we store data needed to run and manage that reservation:
- Which tier, region, map, and config preset you selected, and when the reservation was created/expires
- The provisioned server's IP address, connect port, server password, and RCON password (needed so you can connect to and administer your own server)
- Extension count (from the in-game
!extendcommand) and the reserving player's identity, so the in-game!whocommand can show who reserved the server - A per-server random
plugin_secrettoken, generated when the server finishes provisioning and pushed to the game server via RCON, so the in-game!extendcommand can authenticate its outbound request without using your session cookie - For Premium-tier users, an optional free-text schedule label and a future datetime the server should be provisioned at (so we can pre-book up to 48 h ahead)
Server credentials (password/RCON, plugin secret) are only ever returned to you, the reserving player, and to our automated provisioning system. The underlying virtual machine is provided by a third-party infrastructure provider; their own privacy practices apply to anything running inside the VM itself, but no personal data beyond the reservation metadata above is sent to them.
TFCL Premium members and admins may upload custom .bsp map files to be hosted on maps.tfcleague.com. We store:
- The uploaded filename, display name, and file size
- Which user account uploaded the map, and when
- A pre-computed public download URL on the FTP-backed map host
- The map's current status (active / soft-deleted)
Uploaded map files themselves are hosted on our FTP-backed file host (maps.tfcleague.com), not in the database, and are only used to make custom maps available for on-demand servers and lobbies. The file-host's own privacy practices apply to the bytes stored there.
When you log in we create a session cookie (session) that is:
- HTTP-only, Secure, SameSite=Lax
- Valid for 7 days, auto-renewed on active use
- Deleted from our database when you click Log Out
We also use several short-lived support cookies during sign-in flows; none are used for tracking:
discord_oauth_state- 10-minute CSRF nonce during the Discord link flow, deleted immediately after the callback runs.pending_ref- optional referral code copied into a cookie when you first arrive at the site via a friend's referral link; read once on Steam callback and immediately deleted.redirect_after_login- the page you were trying to reach when prompted to log in; cleared the moment you complete the Steam login.discord_redirect- same idea, but for the relay-step between Steam login and Discord linking; 10-minute expiry.
Our activity and error logging system records:
- A one-way hash of your IP address - never the IP itself. Your raw IP (from the
CF-Connecting-IPheader provided by Cloudflare) is immediately run through a keyed HMAC-SHA256 hash and discarded; only the resulting hash is stored. This is irreversible - nobody, including our own site administrators with full database access, can recover your real IP address from it. The hash is still consistent per-IP, so it still lets us notice "these actions came from the same address" for abuse/ban-evasion detection, without ever exposing what that address is. - User-Agent string
- In-lobby actions (join, leave, pick, ready, report) tagged to your user ID
- JavaScript errors encountered while using the site
These logs are used for abuse detection and debugging. They are not sold or shared with third parties. Note: Cloudflare, as our reverse proxy/CDN, inherently sees your real IP address at the network edge in order to route traffic and provide DDoS protection - this is outside our application's control and is covered by Cloudflare's own privacy policy, not this one. Our own database and admin tools never receive or store your raw IP.
Every page on TFCL Play loads the Google Analytics 4 JavaScript snippet with measuring ID G-CJB7C6MXQW. This means when you load any page, a request is sent to googletagmanager.com / google-analytics.com carrying:
- Page URL and referrer URL
- User-Agent string
- Approximate country/city derived server-side by Google from your IP (your raw IP is not stored by us; Google records its own view of the request)
- A hashed client ID generated client-side and stored in a first-party
_ga/_ga_<MEASURE_ID>cookie
We do not send your Steam ID, Discord ID, hashed IP, email, session token, or any other identifier we hold to Google Analytics. The CF-Connecting-IP hash we store in our own logs is independent of anything GA sees. Google’s full data-handling is described in Google’s privacy policy. You can opt out using your browser Do Not Track signal, an ad/content blocker, or Google’s Analytics opt-out browser add-on.
03 How We Use Your Data
04 Third-Party Services
Authentication (OpenID) and player profile data. Steam's privacy policy applies to any data you share with Valve.
Optional account linking (OAuth2 identify scope only) and the TFCL community Discord server.
Site hosting (Pages), edge database (D1), and CDN. Cloudflare processes all traffic to this site and may log request metadata per its own privacy policy.
Game-server provisioning for both PUG lobbies (requested the moment a lobby is created) and the on-demand Servers page runs on a third-party infrastructure provider reachable via our self-hosted relay at srvrelay.tfcleague.com; provisioning is routed to the nearest available VM in your chosen region at request time. Only metadata needed to provision (region, tier, OS snapshot) is sent - no personal account data leaves our database. The provider’s own privacy policy applies to anything running inside the VM itself.
Payment processing for TFCL Premium purchases and PayPoints top-ups. All payment details (card number, PayPal login, billing address) are entered directly on the PayPal checkout flow and never pass through our servers - we only receive the order ID and purchase outcome.
Used when paying out CUP prize pools. Recipient email and amount are submitted server-side and PayPal handles the actual money movement; PayPal’s privacy policy applies to anything PayPal stores on its side.
Match-log retrieval. After a match ends we try to read the stats log from logs.tf using the log ID reported by the game server. Only the log ID is sent to logs.tf - no personal data.
Anonymous site-traffic analytics. A gtag.js snippet is loaded on every page (measuring ID G-CJB7C6MXQW) and reports aggregate, cookie-less page-view and event data to Google. We do not send any personally identifying fields (your Steam ID, Discord ID, hashed IP, email, or session token) to Google Analytics. You can opt out using your browser's Do Not Track signal, an ad/content blocker, or Google's own Analytics opt-out browser add-on.
CDN-delivered fonts and icon libraries. Your browser may send a request to these CDNs when loading the page; their own privacy policies apply.
05 Data Sharing
We do not sell, rent, or trade your personal data to any third party.
Some data is publicly visible by design:
- Your Steam username and avatar on leaderboards, lobby rosters, match history, and your public profile page.
- Your ELO rating, win/loss record, and total games played.
- Community posts and comments you submit.
- Profile customisations (bio, banner, theme) if you set them.
- Public team pages (team name, tag, avatar, bio, roster of public teams, plus your membership in them).
- Public tournament pages (title, format, schedule, participant list, bracket, and final standings for completed events).
The following is not public: your hashed IP (never your real IP - see “Technical Logs” above), Discord ID/username (not displayed to other users), session tokens, raw API keys (only the visible prefix is shown back to you), point transaction history, referral relationships, TFCL Premium / PayPoints purchase/order history, all gift-code values (only the visible prefix is shown to admins), all team join passwords (only a 🔒 PASSWORD badge is shown; a wrong-password attempt is rejected the same way regardless of which character is wrong), all private tournament join passwords, all on-demand server passwords/RCON credentials and plugin secrets (visible only to the reserving player via a session cookie or matching API key), the per-server audit log of API-key usage, and the recipient email stored on a CUP that uses PayPal Payouts (only the organizer setting up the pool ever sees this).
06 Data Retention
- Sessions expire after 7 days of inactivity and are deleted from the database at that time.
- Google Analytics cookies (
_ga,_ga_<MEASURE_ID>) are first-party cookies set in your own browser by the GA4 snippet. They expire after 2 years of inactivity by default and can be cleared or blocked via your browser settings or the Google Analytics opt-out add-on. - User accounts are retained indefinitely to preserve match history and leaderboard integrity. You may request deletion (see below).
- Activity logs are retained for 90 days for abuse investigation purposes, then may be purged.
- API-key audit rows are retained for 90 days alongside activity logs; per-key last-20-call rolling history is regenerated on every call.
- Error logs are retained for 30 days for debugging purposes.
- PUG chat messages are auto-purged 24 hours after they are sent by
purgeOldLobbyMessages()on every lobby read. Note that chat messages are NOT a long-term record. - TFCL Premium and PayPoints purchase records (order ID, plan/price-key, amount/currency, granted days/points) are retained indefinitely as a financial/audit trail; we do not store card or PayPal account details ourselves.
- Gift codes have a 2-year shelf life from the time they were minted/bought; unredeemed codes past that expiry are returned as 410 Gone from the redeem endpoint. Redeemed codes are retained indefinitely so gift history on /premium (“MY GIFT CODES”) and the admin gift-code list keep working.
- Premium API keys are retained until you revoke them. Revoked keys stay in the table with
revoked_atset so the hash can never be reused for a new key; only the live (non-revoked) key_hash is indexed for auth lookups. - Teams: disbanded teams and inactive member rows are soft-deleted (kept indefinitely) so historical references (e.g. CUP rosters, brackets, referrers) still resolve. Slugs are retained permanently.
- Tournament/CUP audit rows are retained indefinitely per the immutable-history requirement of competitive events.
- On-demand server records (region, tier, map/config, connect info) are retained after the server is destroyed for abuse investigation and capacity-planning purposes; the server's IP/password/RCON credentials become invalid the moment the underlying VM is destroyed, so retaining the record poses no ongoing access risk.
07 Your Rights
You have the right to:
- Access - request a summary of the personal data we hold about you.
- Correction - your username and avatar are automatically updated from Steam on every login. If you believe other stored data is incorrect, contact us.
- Deletion - request deletion of your account and associated personal data. Note that anonymised match results (scores, ELO deltas) may be retained for leaderboard integrity without a link to your identity, and that financial records of TFCL Premium purchases may be retained as required for accounting/audit purposes even after account deletion.
- Discord unlink - you can unlink your Discord account at any time from your profile dashboard, which removes your Discord ID and username from our database.
- API-key revoke - on
/premiumyou can revoke a Premium API key at any time; revocation is immediate and the revoked key cannot be re-issued (its SHA-256 hash stays in the table to prevent reuse). - Team / tournament password rotation - team owners can change or clear the join password at any time from
/teams/:slug/manage; we will re-hash via PBKDF2 the next time you save one. - Withdraw consent / stop using the service - you can log out and stop using the site at any time. Log-out deletes your session from our database immediately.
- Opt out of analytics - you can decline Google Analytics tracking via your browser’s Do Not Track signal, an ad/content blocker that filters
googletagmanager.com/google-analytics.com, or by installing Google’s Browser Opt-out Add-on. Clearing your browser cookies also resets the GA client ID.
To exercise any of these rights, contact us on the TFCL Discord.
08 Security
We take reasonable technical precautions to protect your data:
- All traffic is served over HTTPS (enforced by Cloudflare).
- All persistent cookies (session, discord_oauth_state, discord_redirect, redirect_after_login, pending_ref) are HttpOnly + Secure + SameSite=Lax to mitigate XSS and CSRF; the support cookies are also short-lived and never used for tracking.
- Discord OAuth uses a short-lived cryptographic CSRF nonce (state parameter).
- API secrets (Steam API key, Discord client secret, PayPal client secret, infrastructure-provider/relay API keys, RCON passwords, the IP-hashing salt
IP_HASH_SALT) are stored as Cloudflare Worker secrets and are never exposed in responses. - Team join passwords and private-tournament join passwords are stored as PBKDF2-SHA256 envelopes with a per-row CSPRNG salt and 100,000 iterations. Plaintext is never persisted, never returned by any endpoint, never logged. Verify uses constant-time comparison.
- Premium API keys are stored only as SHA-256 hashes; the raw value is shown ONCE on creation. Lookup is hot-path-indexed on non-revoked rows and uses constant-time comparison.
- Gift-code values are stored only as SHA-256 hashes. Only the first 9 chars (
XXXX-XXXX) are surfaced in any list. - Per-server
plugin_secretvalues are random per-reservation, pushed to the game server via RCON once, and authenticate inbound!extendcalls. Concurrent duplicate!extendpresses are deduplicated server-side so only the first counts. - Game-server connect passwords and RCON passwords are stored in the database but are only returned to authenticated lobby participants / the reserving player (or an API key authorised as them), and are freshly generated per-reservation.
- Bearer-key traffic to the on-demand Servers/Maps routes is rate-limited at 60 requests/min/key; overrun returns 429 with a Retry-After header.
- TFCL Premium purchases and PayPoints top-ups are verified server-side directly against PayPal’s API before any Premium time or points are granted; we never handle raw card or PayPal account credentials.
- Map upload requests are bounded to 95 MB (Cloudflare edge request-body limit) and gated by TFCL Premium eligibility.
No system is 100% secure. If you discover a vulnerability please report it responsibly via Discord rather than exploiting it.
09 Children
TFCL Play is not directed at children under 13. Team Fortress 2 itself requires a Steam account, which Valve requires users to be at least 13 to create. We do not knowingly collect data from anyone under 13. If you believe we have done so, please contact us and we will delete the relevant data.
10 Changes to This Policy
We may update this policy when we add new features that affect data collection. The "Last updated" date at the top of the page will reflect any changes. Continued use of the site after a policy update constitutes acceptance of the new terms.
Questions? Find us on Discord