Security
What we do to protect your account and your data, and what we don't. Each statement below names the automated tests that check it and the day they last passed. We say nothing here that we can't show.
Your password and your account
- Passwords are stored as scrypt hashes, never as the password. A reset, a change or a new sign-up all store the same kind.
Checked 2026-10-23 by test:
test_pages.Passwords.test_every_way_to_set_a_password_stores_a_scrypt_hash - Every form carries a token tied to your session, and the server refuses any POST without it. The only exceptions are the upload API (it needs your upload token instead), the one-click unsubscribe link and the email service's bounce report (a secret in its address).
Checked 2026-10-23 by test:
test_pages.Csrf.test_every_post_route_refuses_a_post_without_the_token - Changing or resetting your password logs your other browsers out.
Checked 2026-10-23 by tests:
test_accounts.ForgotTests.test_reset_link_lasts_an_hour_works_once_and_ends_other_sessions,test_accounts.SecurityTests.test_password_change_ends_every_other_session - A login lasts 30 days from when you log in (12 hours for an admin), however often you use it. Log out ends it in that browser; Settings → Security → Log out everywhere ends every login to your account at once, including a copy of the cookie someone else may hold.
Checked 2026-10-23 by tests:
test_fix_login.Sessions.test_a_login_lasts_thirty_days_from_when_it_was_made,test_fix_login.Sessions.test_an_admin_login_lasts_twelve_hours,test_fix_login.Sessions.test_a_copied_cookie_dies_with_log_out_everywhere - Pages that show a secret (Settings with your upload token, the two-factor pages) tell browsers and anything in between not to store them.
Checked 2026-10-23 by test:
test_fix_login.NoStore.test_pages_with_secrets_are_not_stored - Admins must log in with a code from an authenticator app as well as their password. Every admin page refuses an admin who hasn't. A password reset asks for the code too before it logs you in. Turning it on needs your password. A code works once; the ten recovery codes are stored only as hashes and each works once; the app's key is stored sealed, so a copy of the database alone can't read it.
Checked 2026-10-23 by tests:
test_twofa.AdminRule.test_every_admin_route,test_twofa.Login.test_replayed_code_is_refused,test_twofa.Login.test_a_recovery_code_works_once,test_twofa.HalfLoggedIn.test_nothing_but_the_code_page,test_twofa.Sealed.test_round_trip_bound_to_account_and_key,test_twofa.Setup.test_setup_flow,test_fix_login.ResetAndTwoFactor.test_a_forgot_link_and_an_admin_link_both_ask_for_the_code,test_fix_login.ResetAndTwoFactor.test_every_login_path_goes_through_the_code - Any player may turn on two-factor login in Settings → Security (admins must). With it on, every way of logging in asks for the code: the login form, a password reset link, a verify link, and a browser you have logged in with before; a login that never passed the code is no login at all. Turning it on needs your password, turning it off your password and a code (or, if you lost both phone and recovery codes, your password and a one-time link we email to your account's address when you ask), and either way your other browsers are logged out and we email you. The app's key is stored sealed and the recovery codes only as hashes, and turning it off deletes both.
Checked 2026-10-23 by tests:
test_twofa_players.Enrol.test_a_player_enrols_logs_out_and_logs_in_with_the_code,test_twofa.Setup.test_setup_flow,test_twofa_players.EveryWayIn.test_the_password_form_by_handle,test_twofa_players.EveryWayIn.test_the_password_form_by_address,test_twofa_players.EveryWayIn.test_a_reset_link,test_twofa_players.EveryWayIn.test_a_verify_link,test_twofa_players.EveryWayIn.test_the_known_device_cookie_never_skips_the_code,test_twofa_players.EveryWayIn.test_a_session_that_never_passed_the_code_is_no_login,test_twofa_players.EveryWayIn.test_every_place_a_session_is_made_is_a_known_one,test_twofa_players.TurnOff.test_refused_without_the_password_or_a_code,test_twofa_players.TurnOff.test_the_password_and_a_code_turn_it_off_end_other_sessions_and_email_once,test_twofa.Cli.test_twofa_off_mails_a_link_that_needs_the_password,test_twofa_players.Cli.test_twofa_off_for_a_player_mails_a_link - An upload token can be replaced or revoked in Settings → Security, and the old one stops working at once.
Checked 2026-10-23 by tests:
test_accounts.SecurityTests.test_new_token_stops_the_old_one_at_once,test_accounts.SecurityTests.test_revoke_stops_uploads_until_a_new_token - A new account must verify its email before it can upload, and until then it is listed nowhere.
Checked 2026-10-23 by tests:
test_accounts.VerificationTests.test_unverified_cannot_upload_until_verified,test_accounts.VerificationTests.test_unverified_is_listed_nowhere
Links we email you
- Verify, reset, change-of-email and undo links are stored only as SHA-256 hashes. A copy of the database can't be used to follow one.
Checked 2026-10-23 by test:
test_accounts.HashesOnlyTests.test_no_link_token_is_in_the_database_file - Each link works once and for a limited time: verify 24 hours, reset 1 hour (an admin's 24 hours), change of email 24 hours, undo 7 days.
Checked 2026-10-23 by tests:
test_accounts.VerificationTests.test_verify_link_is_single_use_and_lasts_24_hours,test_accounts.ForgotTests.test_reset_link_lasts_an_hour_works_once_and_ends_other_sessions,test_accounts.ForgotTests.test_admin_links_still_last_24_hours,test_accounts.ChangeEmailTests.test_change_link_expires,test_accounts.ChangeEmailTests.test_undo_link_lasts_seven_days - After a change of email, the undo link sent to your old address keeps working for its 7 days even if a later change's undo is pressed first.
Checked 2026-10-23 by tests:
test_accounts.ChangeEmailTests.test_the_later_undo_pressed_first_leaves_the_earlier_one_live,test_accounts.ChangeEmailTests.test_two_quick_changes_cannot_defeat_the_first_undo - The forgotten-password page gives the same answer whether or not an address has an account.
Checked 2026-10-23 by test:
test_accounts.ForgotTests.test_same_answer_for_any_address - Link tokens are kept out of the server's logs: the address is cut off at the secret.
Checked 2026-10-23 by tests:
test_accounts_review.AccessLogTests.test_no_secret_reaches_the_log_and_the_log_stays,test_accounts_review.ErrorLogTests.test_flask_and_gunicorn_error_lines_lose_the_token - The Delete my data page gives the same answer whether or not an address has an account; its link works once, for 1 hour, and opening it deletes nothing until you press the button.
Checked 2026-10-23 by tests:
test_deletion.DeleteMyData.test_the_same_answer_for_a_member_a_stranger_an_unverified_account_and_a_sample,test_deletion.DeleteMyData.test_get_asks_and_deletes_nothing_post_deletes_once,test_deletion.DeleteMyData.test_only_the_sha256_is_stored_and_the_link_lasts_an_hour
Limits on guessing and abuse
- Sign-up, forgotten-password and change-of-email forms, which send an email to an address someone typed, check Cloudflare's human test on the server first. A missing or forged answer is refused.
Checked 2026-10-23 by tests:
test_accounts.TurnstileTests.test_every_form_refuses_a_forged_missing_or_unchecked_token,test_accounts_review.TurnstileActionTests.test_each_page_declares_the_action_its_endpoint_expects - Logging in is stopped after 10 wrong tries from one address in 15 minutes, even for the right password. Nobody can lock you out of your account: after 10 wrong tries at it from anywhere, a login to it also needs the human check, unless it comes from a browser you have logged in with before (a cookie that works for your account only). A password reset always works and clears the count. The counts are kept in the database, so every server process shares them, and a refused login looks the same whatever the reason.
Checked 2026-10-23 by tests:
test_pages.Throttles.test_login_stops_after_the_limit,test_hardening.TwoInstances.test_login_throttle_per_ip,test_hardening.TwoInstances.test_login_throttle_per_account,test_fix_login.Lockout.test_owner_from_a_known_browser_logs_in_with_no_check,test_fix_login.Lockout.test_owner_from_a_new_browser_needs_the_check_and_gets_in_with_it,test_fix_login.Lockout.test_a_cookie_for_one_account_does_not_help_another,test_fix_login.Lockout.test_a_forged_or_altered_cookie_is_ignored,test_fix_login.Lockout.test_a_cookie_dies_with_its_account_even_when_the_id_is_reused,test_fix_login.Lockout.test_a_reset_clears_the_count,test_fix_login.Lockout.test_a_hot_account_and_no_account_answer_alike - A login takes as long whether or not the account exists: with no account, the password is still checked against a dummy hash.
Checked 2026-10-23 by test:
test_fix_login.DummyHash.test_no_account_still_checks_a_hash - At most 5 new accounts an hour from one address, and 100 verify emails a day across the whole site.
Checked 2026-10-23 by tests:
test_accounts_review.SignupLimitTests.test_five_sign_ups_an_hour_from_one_ip,test_accounts_review.SignupLimitTests.test_a_site_wide_cap_on_verify_emails - The address these limits count is the one the request comes from. The visitor's address that Cloudflare adds is believed only on requests that arrive through Cloudflare's tunnel; anyone else who sends one is counted by their own address. The home server itself, and the other programs running on it in their own containers, arrive the way the tunnel does and are believed too.
Checked 2026-10-23 by tests:
test_move.Forwarded.test_an_untrusted_peer_is_counted_by_its_own_address,test_move.Forwarded.test_a_trusted_peer_is_counted_by_the_header,test_move.Forwarded.test_sign_ups_count_the_peer_not_a_forged_header,test_move.Gateway.test_an_untrusted_peer_loses_every_forwarding_header - One password-reset link at a time: while the last one is unused and less than 1 hour old, asking again sends nothing and counts against no limit, so a stranger can't use up yours. Past that, at most 5 a day to one account from one address and 15 from all addresses together. A browser you have logged in with before gets a new link even then, up to 5 a day of its own, and an address that reported us as spam or unsubscribed gets one a day whoever asks.
Checked 2026-10-23 by tests:
test_accounts.ForgotTests.test_five_a_day_per_account_the_sixth_silent,test_accounts.ForgotTests.test_a_stranger_cannot_use_up_the_owners_resets_but_a_flood_stops,test_accounts.ForgotTests.test_one_live_link_a_repeat_sends_nothing_and_counts_nothing,test_accounts.ForgotTests.test_a_known_browser_has_its_own_five_a_day_and_the_one_a_day_for_a_marked_address,test_accounts.ForgotTests.test_limits_hold_across_two_app_instances - Nothing says whether an address has an account: signing up with a member's address gets the usual answer (and the member a note instead of a link), and an invite to a member gets the same answer as any other, with no email sent. Both take the same time either way: every email is sent after the answer. Each player may try 10 invites a day.
Checked 2026-10-23 by tests:
test_fix_login.SignupOnAMembersAddress.test_a_member_and_a_stranger_see_the_same,test_fix_login.SameTime.test_an_invite,test_fix_login.SameTime.test_a_second_sign_up_and_send_it_again,test_fix_login.SameTime.test_the_verify_cap_answers_alike,test_invites.Invites.test_a_member_and_a_stranger_look_the_same,test_invites.Invites.test_ten_tries_a_day_per_inviter_refusals_included - At most 50 feedback emails a day reach us from the whole site (every message is still kept), and a player is in at most 3 challenges waiting or under way at a time.
Checked 2026-10-24 by tests:
test_feedback.Feedback.test_at_most_fifty_emails_a_day_site_wide_the_rest_stored,test_teams.Throwing.test_a_player_is_in_at_most_three_at_once - Each upload token may upload at most 120 times an hour (the collector's hooks upload at most every 2 minutes while you work, plus once as each Claude Code session starts and ends, and whenever you run it by hand), and a day of over 50.0B tokens on one machine is refused.
Checked 2026-10-23 by tests:
test_hardening.Uploads.test_121st_in_an_hour_is_refused,test_hardening.Uploads.test_over_50b_for_one_machine_day_is_refused,test_hardening.Uploads.test_the_collector_prints_the_message - An upload holds at most one entry for each day since Claude Code existed (24 February 2025), none before, and an account has at most 20 machines, adding at most 5 a day; a malformed upload is refused with a message, never a crash.
Checked 2026-10-23 by tests:
test_fix_ingest.Sizes.test_at_most_every_day_since_the_floor_in_one_upload,test_fix_ingest.Sizes.test_two_uploads_racing_for_the_last_place_make_one_machine,test_fix_ingest.Floor.test_days_before_claude_code_are_skipped_not_refused,test_fix_ingest.Floor.test_stored_ancient_rows_are_ignored_by_every_page_not_deleted,test_fix_ingest.Sizes.test_at_most_twenty_machines,test_fix_ingest.Sizes.test_at_most_five_new_machines_a_day,test_fix_ingest.Crafted.test_a_list_body_is_a_400,test_fix_ingest.Crafted.test_a_count_over_the_day_cap_is_a_400_and_nothing_is_saved - Your current password may be guessed wrong 10 times an hour in Settings → Security; after that even the right one waits.
Checked 2026-10-23 by test:
test_accounts_review.GuessTests.test_ten_wrong_then_even_the_right_one_waits
Your data
- What each privacy row hides is left out of the page itself, not hidden with styling: the numbers are not in the HTML for a viewer who may not see them.
Checked 2026-10-23 by tests:
test_privacy.ProfileItems.test_tokens,test_privacy.ProfileItems.test_models,test_privacy.ProfileItems.test_hours,test_privacy.ProfileItems.test_plan,test_privacy.ProfileItems.test_machines,test_privacy.LiveRow.test_every_level_and_viewer - Project names, which older versions of the collector sent, were deleted from the database file itself, not just hidden.
Checked 2026-10-23 by test:
test_projects_removed.Migration.test_deleted_names_are_gone_from_the_file - Deleting your account (Settings → Security, or the Delete my data page's emailed link) removes every row about you from every table at once, and the deleted bytes are wiped from the database file, not just unlinked. A test fails if a table is added that the deletion doesn't know.
Checked 2026-10-23 by tests:
test_deletion.EveryTable.test_nothing_of_the_account_is_left_in_any_table,test_deletion.EveryTable.test_the_check_can_fail_a_table_no_rule_knows_turns_it_red,test_deletion.Bytes.test_old_copies_in_free_space_go_too,test_deletion.Bytes.test_secure_delete_zeroes_them_when_the_vacuum_cant_run - A deletion survives a restore: each one is written to a list beside the database before it is done (the account number, when it was made and its random seed, nothing personal), and every start deletes again any account on the list that a restored snapshot brought back. If the list can't be written, the deletion is refused and you are told.
Checked 2026-10-23 by tests:
test_ledger.Ledger.test_a_restore_deletes_it_again_and_spares_a_newcomer_on_the_same_id,test_ledger.Ledger.test_settings_delete_writes_a_line_with_nothing_personal,test_ledger.Ledger.test_an_unwritable_ledger_refuses_the_deletion_out_loud,test_ledger.Ledger.test_three_workers_start_at_once_replay_once_and_again_changes_nothing - A deleted account can survive in our backups for up to 6 months. A restore from the hourly, daily or weekly snapshots replays the deletions: before the site answers again, it replays that list and deletes the account again. If the home server itself were lost and rebuilt from the second copy, deletions from the last day before the loss could come back, and we would delete them again on request.
Checked 2026-10-23 by tests:
test_ledger.Ledger.test_a_restore_deletes_it_again_and_spares_a_newcomer_on_the_same_id,test_ops_admin.RestoreCheck.test_the_ledger_deletes_again_and_spares_the_newcomer,test_ops_admin.RestoreCheck.test_an_account_the_replay_leaves_is_a_fail - A new account that later gets the same account number sees nothing of a deleted one.
Checked 2026-10-23 by tests:
test_deletion.IdReuse.test_another_process_deletes_and_the_next_sign_up_on_the_id_sees_none_of_it,test_deletion.IdReuse.test_each_cache_misses_for_a_newcomer_with_the_same_id_and_the_same_rows - Your picture is published without what was hidden inside it: where and when a photo was taken, the camera and any comments are removed when you upload it, and were removed from pictures already here.
Checked 2026-10-23 by tests:
test_fix_avatars.Strip.test_each_type_loses_its_metadata_and_keeps_its_picture,test_fix_avatars.Upload.test_an_upload_is_published_without_its_metadata,test_fix_avatars.StartUp.test_files_already_on_disk_are_stripped_once - The server's request log holds no IP address, and it is kept in at most 30 MB, the oldest lines overwritten first.
Checked 2026-10-23 by tests:
test_fix_pages.RequestLog.test_the_dockerfile_format_has_no_address_and_none_gets_through_any_format,test_fix_pages.RequestLog.test_compose_rotates_it_as_data_says - No profile or monthly recap is indexed by search engines, whatever its privacy rows say.
Checked 2026-10-23 by tests:
test_fix_pages.Pages.test_no_profile_or_recap_is_indexed_and_no_logged_in_page,test_privacy.ProfileItems.test_noindex - Search engines may index only the public pages (the front page, How it works, Updates, Your data, Security, Privacy, Terms, the leaderboard and the map), and only as seen by a visitor who is logged out. Every other page, and every page for a logged-in player, is marked noindex in the page and in the response headers; a page added later is noindex until it is put on the list.
Checked 2026-10-23 by tests:
test_indexing.Pages.test_each_page_is_listed_with_a_canonical_link_or_noindex_in_header_and_meta,test_indexing.Pages.test_a_logged_in_view_of_a_public_page_is_noindex,test_indexing.Pages.test_the_list_is_what_the_test_says,test_indexing.RobotsAndSitemap.test_sitemap_lists_exactly_the_public_pages - Google Analytics, where it is switched on, loads only on those public pages, only for a visitor who is not logged in, and only after they press Accept. No other page carries the banner or lets the Content-Security-Policy admit Google's scripts.
Checked 2026-10-23 by tests:
test_consent.Consent.test_google_may_run_only_where_the_page_is_public_and_the_viewer_logged_out,test_consent.Consent.test_an_account_waiting_for_its_email_and_a_player_get_no_banner,test_consent.Script.test_everything_is_denied_before_anything_loads,test_consent.Script.test_google_is_requested_in_one_place_and_only_by_load - Settings → Security → Download my data gives you what we hold on you, table by table, and your picture, as one file, after your password (and a code, with two-factor on), at most 5 times a day. What it leaves out is named in the file with its reason, and a test fails if a column is added that the file neither gives nor names. Your password's hash, your upload token, your two-factor key and the hashes of your links and codes are never in it, and other players appear in it by handle only, where you could already see them.
Checked 2026-10-23 by tests:
test_export.SchemaWalk.test_every_column_goes_out_or_is_named_with_its_reason,test_export.SchemaWalk.test_a_column_added_later_fails_it,test_export.SchemaWalk.test_the_secrets_the_plan_names_are_never_exported,test_export.File.test_a_report_about_you_leaves_no_trace,test_export.File.test_no_secret_is_in_any_byte_of_the_file,test_export.File.test_other_players_by_handle_and_only_where_you_could_see_them,test_export.Asking.test_a_wrong_password_makes_no_file_and_counts_toward_the_guesses,test_export.Asking.test_two_factor_on_needs_a_code_once,test_export.Asking.test_the_sixth_file_in_a_day_is_refused - The page /data lists every field the collector sends and every column we store. A test fails when either changes without the page.
Checked 2026-10-23 by tests:
test_pages.Upload.test_every_field_the_collector_sends_is_listed,test_pages.Upload.test_everything_the_page_lists_is_really_sent,test_pages.Schema.test_every_table_and_column_is_listed
The site itself
- Every response carries security headers: a Content-Security-Policy, HSTS over HTTPS, no framing by other sites, no MIME sniffing, a strict referrer policy and camera, microphone and location switched off.
Checked 2026-10-23 by tests:
test_hardening.Headers.test_on_every_response,test_hardening.Headers.test_hsts_only_over_https,test_hardening.Headers.test_policy - No page runs inline script, so the Content-Security-Policy can forbid it; a machine's name in a confirm box is shown as text and can't become code.
Checked 2026-10-23 by tests:
test_hardening.NoInlineScript.test_no_page_has_inline_script,test_hardening.NoInlineScript.test_machine_name_cannot_break_out_of_its_confirm - /.well-known/security.txt says how to report a problem, and its expiry date is never in the past.
Checked 2026-10-23 by test:
test_hardening.SecurityTxt.test_valid_and_never_stale
The limits, said plainly
- No third-party audit. Nobody outside has tested this site. We don't hold Cyber Essentials or any other certificate, and we have no professional indemnity insurance.
- It runs on a home server in the UK, not in a data centre.
- The numbers are self-reported. A script on your computer sends them, so anyone can fake theirs. Nothing here stops that, and the game doesn't pretend otherwise.
- Your upload token is stored as it is, not as a hash, because the server has to recognise it on every upload. Anyone who could read the database could use it. You can replace or revoke it at any time in Settings → Security.
- Backups. The server keeps snapshots up to 8 weeks, and a nightly copy on a second machine up to 6 months (checked 23 October 2026). A deleted account can survive in those for up to 6 months. A restore from the hourly, daily or weekly snapshots replays the deletions; if the home server itself were lost and rebuilt from the second copy, deletions from the last day before the loss could come back, and we would delete them again on request (see "Your data" above).
- This list is what we can show today. Anything we add later is added to the list above with its test.
Found a problem?
Please email [email protected], and give us a little time to fix it before you tell anyone. Our contact details for security reports are also in security.txt. See Tally Digital's security page too.