Written to be read by anyone

Security Practices

Security comes before every other priority in how this app is built — more than any feature or deadline. Here's how this site handles your account, your data, and the credentials behind it — no configuration details, addresses, or account names appear here.

01Injection attacks are prevented

Values you submit — a bracket name, a search term, anything typed into a form — are always sent to the database separately from the instruction that uses them, never assembled into the query text itself. That separation is what stops SQL injection, the class of attack where crafted input is used to run unintended database commands. It applies uniformly across the site, including the bulk-loading operations that run as stored procedures — running something as a stored procedure isn't what provides this protection, the parameterisation is, and it's applied the same way everywhere a query runs.

The same principle applies to generated content: text produced by the language model is filtered before it's shown to anyone. Only a small set of formatting tags survives, and HTML attributes are removed entirely, so generated content cannot introduce active code into a page — the same class of protection, applied to cross-site scripting instead of SQL.

02The database and application server are not directly reachable

Only the reverse proxy in front of this site has a port open to the outside world. The database has no network path in from outside the server at all, and the application server itself is reachable only through that reverse proxy, never directly — so there is nothing to find or connect to behind it, regardless of what an attacker might try.

03Account and session protection

  • Sign-in attempts are rate-limited, so passwords cannot be guessed by brute force.
  • Session cookies are inaccessible to page scripts, restricted to this site, and sent only over an encrypted connection.
  • All form submissions carry an anti-forgery token, so another site cannot cause your browser to act on your behalf.
  • Administrator sessions expire after a period of inactivity.
  • Administrator login supports a second factor. When enabled, a correct password is not enough on its own — a time-based one-time code from an authenticator app is required as a separate step, and the two are never accepted together in one submission.

04User passwords are never stored

Passwords are put through scrypt, a deliberately slow, memory-hard one-way function, together with a random per-account salt. What's stored is the result — not the password, and not an encrypted copy of it. There is no key and no procedure that turns it back into your password, because the information simply isn't there any more.

When you sign in, what you typed is put through the same process and the two results are compared. That's how the site can confirm your password is correct without ever knowing what it is. The administrator can reset an account — replacing the stored value so a new password works — but cannot look one up.

The random salt means two people who happen to choose the same password have completely different stored values, so it isn't possible to tell that they match, and precomputed cracking tables don't apply.

The short version: your password cannot be read by anyone, including the site's own administrator. Your contact details are visible only to the administrator, and every other system that touches this database is restricted to the narrowest slice of it that its job requires.
  • Every connection between your browser and this site is encrypted with a certificate from a public certificate authority — the same kind every trusted site on the web uses — and it renews itself automatically, so that encryption never has a chance to lapse.
  • The one internal connection on this site (the web application talking to the machine that writes the daily recap) is authenticated by its own private certificate system: each machine holds an individual certificate, the exact certificate presented has to match the one issued to it, and anything missing, expired, or unrecognized is refused outright rather than let through.

05Password reset tokens are hashed

A reset link contains a single-use, time-limited random token. Only a hash of that token is stored, so the link that reaches your inbox is the only usable copy. Someone reading the database would find nothing they could redeem.

06Browser security headers are enforced

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

  • Content-Security-Policy — an explicit allowlist of where scripts, styles, images, and fonts are permitted to load from. Even if a script were injected somewhere it shouldn't be, the browser refuses to run it if it isn't on the list — a second layer behind the filtering described above, not a replacement for it.
  • Strict-Transport-Security — once a browser has visited over HTTPS, it is instructed to always use HTTPS for this site afterward, even if a stale link or a typo says otherwise.
  • X-Frame-Options — refuses to let this site be loaded inside another site's page, which is what a clickjacking attempt depends on.
  • X-Content-Type-Options — stops the browser from reinterpreting a file as a different, more dangerous type than the one the server declared.
  • Referrer-Policy — limits what part of the URL a user was just on gets passed along when they click a link to somewhere else.

07Least privilege between systems

The site generates a daily written recap using a language model. The machine running that model has a dedicated database account restricted to a single table of generated text, and within that table only to the specific columns it must write. It cannot read participant records at all — names, email addresses, and phone numbers are outside its reach entirely, enforced by the database rather than by convention.

08The AI worker connection is isolated

Beyond what its database account can reach, the machine that writes the daily recap is boxed in on every other axis too:

  • Its database account only functions from that one machine. A copy of the password is useless anywhere else.
  • Every connection from that machine is authenticated twice over, using mutual TLS — it presents a private certificate and a secret token, and both have to check out before anything is accepted. Knowing one alone is not enough.
  • The connection is outbound only. The model machine accepts no incoming connections, so there is no way in even if the web application itself were compromised.
  • The work it accepts is rate-limited, so no amount of queued requests can monopolise it.

09Secrets are never exposed

  • Secrets are supplied to the application at runtime from the environment. None are committed to source control, and every change is automatically scanned for accidentally committed credentials before it can be deployed.
  • They are not built into the application image, so distributing or rebuilding it never distributes a secret.
  • Every credential is scoped to one purpose and one place, so the consequences of any single one being exposed are bounded and known.
  • There is a documented rotation procedure for each one, and it's exercised — credentials are replaced when there's reason to, not left in place indefinitely.

10CI/CD checks before any code ships

Every change is pushed through the same set of automated CI/CD checks before it can reach players, regardless of who wrote it:

  • Secret scan — every file in the repository is checked against known credential patterns before anything is deployed.
  • Test suite (pytest) — every test runs against a real database, the same kind used in production, not a mocked stand-in.
  • Lint (ruff) — the codebase is checked against a fixed set of code quality and correctness rules before it can merge.
  • Security static analysis (bandit) — the application code is scanned for common vulnerability patterns (SQL injection, unsafe deserialization, and similar) before it can merge.
  • Dependency audit (pip-audit) — every third-party package the deployed app actually uses is checked against known vulnerability databases.

11A human signs off before anything reaches players

Passing the automated checks above is enough to reach the rehearsal environment, where the change runs for real before anyone sees it. Reaching players is a separate, additional step: production can only be promoted from the exact commit that was just tested in rehearsal — never a different or hand-picked one — and that promotion is gated on a specific person's explicit approval. If any automated check is still running or has failed, the promotion refuses to proceed at all.

Found something that looks wrong? Please report it rather than testing it against other people's accounts — it will be taken seriously and fixed.
Curious how a change gets from an idea to something running here, and what stops a mistake before it reaches this page? See How This Gets Built Safely.