LEGAL

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:

Steam Account Data (mandatory)

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.

Discord Account Data (optional)

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.

Gameplay & Platform Data

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)
Premium API Keys (TFCL Premium feature)

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 (premium or admin)
  • 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.

Premium Gift Codes (paid + admin-minted)

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.

Teams & Team Passwords

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-crew used 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.

Tournaments & Cups

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_player payment rows, and (for paid CUPs) a recipient email needed for prize disbursement via PayPal Payouts
  • An entry_mode tag on every cup_audit.team_registered row: 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, recording funding_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-hashed join_password_hash on 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.

Payment Data (TFCL Premium purchases)

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.

On-Demand Game Servers (Servers page)

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 !extend command) and the reserving player's identity, so the in-game !who command can show who reserved the server
  • A per-server random plugin_secret token, generated when the server finishes provisioning and pushed to the game server via RCON, so the in-game !extend command 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.

Custom Map Uploads (TFCL Premium / admin feature)

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.

Session Data

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.
Technical Logs (automatic)

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-IP header 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.

Site Analytics (Google Analytics 4 - automatic)

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

Provide the service: Identifying you in lobbies, calculating ELO/rankings, displaying your profile, allocating PUG Points, processing Buy Points payments, and enforcing the per-tier server cap & in-game reservation commands.
Safety and moderation: Hashed-IP and activity logs allow admins to spot patterns (e.g. the same address behind multiple accounts) to investigate griefing, ban evasion, or abuse - without our admins ever seeing an actual IP address. The audit trail on tournaments/CUPs gives admins a clear, immutable record of every organizer action for dispute resolution.
Discord integration: Your linked Discord ID is used to move you into team voice channels when a lobby starts.
Leaderboards and statistics: Aggregated match statistics are shown publicly on leaderboards and match history pages.
Referral bonuses: We track who referred whom so we can award PUG Points to both parties once the new player completes their first lobby.
TFCL Premium: Payment/order records let us verify a purchase and grant/extend your Premium expiry once, let you see your own purchase history, validate API-key bearer auth on every request, and let admins/recipients mint or redeem gift codes.
Premium teams & tournaments: Team rosters, pretty team-page slugs, hashed team join passwords, tournament invite codes, private-tournament join passwords, and CUP payment records are used to authenticate participants, render team/tournament surfaces, and let admins run short-form CUP events end-to-end.
On-demand game servers: Reservation records let us enforce capacity limits, auto-expire servers, provide connect info, let the in-game !who/!extend commands work via a per-server plugin_secret, and (for Premium) pre-book a future-dated reservation up to 48 h ahead.
Aggregate site analytics: Page-view and referrer stats from Google Analytics 4 help us understand which pages and features are used so we can prioritise fixes and improvements. No personally identifying data is sent to Google Analytics (see “Site Analytics” above).

04 Third-Party Services

Valve / Steam privacy policy

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.

Cloudflare privacy policy

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.

TFCL Server infrastructure (PUG lobbies + on-demand) 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.

PayPal (Checkout) privacy policy

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.

PayPal (Payouts) privacy policy

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.

Google Analytics 4 privacy policy

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.

Google Fonts / jsDelivr / Font Awesome privacy policy

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_at set 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 /premium you 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_secret values are random per-reservation, pushed to the game server via RCON once, and authenticate inbound !extend calls. Concurrent duplicate !extend presses 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