Tourney, secured end to end

Security comes before any feature or deadline in how this app is built. This is what actually protects an account, a password, and the credentials behind them - not a policy statement, the real mechanisms, condensed from Tourney's own in-app security documentation.

5 rate-limited actions
sign-in, registration, password reset, password change, entry submit/edit - each capped per visitor
scrypt + salt
passwords are never stored - not even the admin can read one back
5 security headers
CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy - on every response
Zero secrets in source
supplied at runtime, scanned on every push, rotated on a documented schedule

Injection is prevented at the source, not patched after

Anything a visitor submits - a bracket name, a search term, any form field - is sent to the database separately from the instruction that uses it, never assembled into the query text itself. That separation, applied uniformly including the bulk-loading stored procedures, is what actually stops SQL injection; running something as a stored procedure isn't what provides the protection, the parameterization is.

The same principle covers AI-generated content: text the model produces is filtered before anyone sees it. Only a small set of formatting tags survives, and every HTML attribute is stripped, so a generated response can't introduce active code into a page - the same class of protection, applied to cross-site scripting instead of SQL.

The database and the app server aren't reachable from outside

Only the reverse proxy in front of the site has a port open to the world. The database has no network path in from outside the host at all, and the application server is reachable only through that proxy, never directly - there's nothing to find or connect to behind it, whatever an attacker tries.

Account and session protection

Passwords and reset tokens are never stored - only proven

Passwords go through scrypt, a deliberately slow, memory-hard one-way function, combined with a random per-account salt. What's stored is the output of that function, not the password and not a reversible encryption of it - there's no key and no procedure that turns it back. Signing in re-runs the same function on what you typed and compares the two results, so the site can confirm a password is correct without ever knowing what it is. An administrator can reset an account (replacing the stored value) but can't look one up.

The random salt means two accounts that happen to share a password produce completely different stored values, so a match can't be detected and precomputed cracking tables don't apply. Password-reset links follow the same rule: the link contains a single-use, time-limited random token, and only a hash of that token is stored - the email is the only usable copy, and reading the database directly yields nothing redeemable.

Browser security headers, enforced on every response

Every response carries instructions telling the browser how to defend itself, independent of the application's own code:

Least privilege, secrets, and the gate before anything ships

The AI worker that writes the daily recap gets a database account restricted to one table and, within it, only the columns it must write - participant names, emails, and phone numbers are outside its reach entirely, enforced by the database, not by convention (full detail, plus the mTLS handshake that authenticates that connection, is on the main engineering page).

Reaching a real player is a separate, additional gate on top of all of the above: the same five-check CI/CD pipeline (secrets scan, tests, lint, security-lint, dependency audit) covered on Engineering Platform, plus a human sign-off that only the exact commit just tested can be promoted - detail on both on the main engineering page, §06.