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