TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Match Records and Privacy: Where Finished Matches Are Stored and What Is Not Left Behind

Status: Draft (2026-10-26), code-based. Behavior was read from the repository code and design documents as of 2026-10-25. The count of "corrected records" in the production DB is the value read on 2026-10-18. Operational statistics such as user counts are not included.Writing rule: Measured data is the default; unmeasured items are marked as "Unmeasured", and items whose basis cannot be found are marked as "Unconfirmed".

Table of Contents

1. When a Match Ends

A best-of-five match ended 3:1. The camera scoreboard shows a "Match Over" screen, and the game scores (11:7, 9:11, 11:5, 13:11) are displayed side by side. Where is this match right now?

It is only on the phone. As seen in Part 2, the camera scoreboard stores the entire match history in the browser's localStorage (ttplay:cam). Every entry in the history also includes its source. These are tap (screen-tapped score), key (remote control key), adj (score adjustment), and auto (accepted auto-judgment). The server knows nothing about this match.

In this state, there are only three paths chosen by the user to leave something on the server.

Path What Goes to the Server Where It Stays
Record This Match Game scores, match duration, opponent Match records, ratings
Share Live (during match) Score, undo, end events Live match and event columns
Auto Score (during match) Frames (not stored), records of human responses to chips Corrected records (no video)
경기 끝 이력 전체는 휴대폰 localStorage 에만 「이 경기 기록하기」 게임 점수만 넘김 · 손으로 올린 게임은 뺌 Google 로그인 · 표시 이름 구글 실명은 어디에도 안 나감 상대 고르고 「기록하기」 하루 30경기 · 같은 상대 10경기 계정 있는 상대: 확인 대기 상대가 「확인」을 눌러야 함 계정 없는 상대 개인 기록만 7일 안 확인 이의 7일 지남 확인됨 Elo 반영 K 40 · 24 · 16 이의 레이팅 안 움직임 개인 기록 레이팅에 안 들어감 상대 이름은 적은 사람만 순위표: 공개를 켠 사람 중 확인된 경기 3개 이상 프로필은 비공개가 기본 · 레이팅은 1500 에서 시작
Figure: How a Finished Match Becomes Records and Ratings

2. "Record This Match"

What is Transferred

When clicking "Record This Match" in the "Match Over" sheet (backend/static/tt/play/lib/cam-sheets.mjs), the screen temporarily stores the match summary {best_of, games, source:'scorekeeper'} in sessionStorage and moves to the "My Records" screen (/tt/me#match). If not logged in, it goes through the login process. Selecting an opponent on the records screen and clicking "Record" sends the data to the server at that moment (POST /api/tt/matches).

What gets transferred is only the game score. Rally-by-rally history, who scored when and how, and traces of auto-judgments are not transferred. Certain cases cannot be recorded.

Login and Display Names

Writing records requires a Google login. Guests only have read access. Write privileges act as a gateway similar to the site community.

There is an important design choice here: Google account names are never exported anywhere. Google names are usually real names, and if they were exposed as-is to match opponents or leaderboards, people met at the table tennis club would learn your real name. Therefore, no responses or screens in the table tennis section use Google names (backend/app/tt_records.py header "Google names are not exported"). Before setting a display name, players are listed as "Pong Player #ID", and to record matches, a display name (2–20 characters) must be set first. This is because matches appearing only with numbers on the opponent's screen make it impossible to recognize who recorded them.

The nickname field (up to 12 characters) in the camera scoreboard also bears the notice "Nicknames instead of real names".

Ratings Change Only After Opponent Confirmation

If ratings could be increased by solo-recorded wins, the leaderboard would simply become a table of who recorded the most. Therefore, matches against opponents with accounts enter Pending Confirmation, and the rating changes only once when the opponent clicks "Confirm", changing its status to Confirmed. Opponents can also raise an "Objection". If not confirmed within 7 days , it does not enter the ratings and remains only as a personal record. Expiration does not modify rows via batch jobs; instead, it is judged by the timestamp generated upon reading (measured using a single DB clock).

Matches against opponents without accounts (neighborhood friends) can also be recorded. In this case, the opponent's name is visible only to the person who recorded the match , and ratings do not change.

Elo Rating

Ratings use the Elo system derived from chess.

# backend/app/tt_records.py 에서 줄임
def k_factor(rating, rated_games):
    if rated_games < 10:   return 40      # 처음 10경기: 제자리를 빨리 찾게
    if rating >= 2000:     return 16
    return 24

def expected(ra, rb):                     # A 가 이길 기대값
    return 1 / (1 + 10 ** ((rb - ra) / 400))

# 변화량 = K × (실제 결과 − 기대값). 두 사람이 각자 자기 K 를 쓴다.

For example, if both are at 1500 and within their first 10 matches, the expected value is 0.5, the winner gets 40 × 0.5 = +20, and the loser gets −20. With the same K of 40, if a 1600-rated player beats a 1500-rated player, the expected value is about 0.64, so it increases by only about +14.4, and dropping a match decreases it by about −25.6.

Daily Limits and Concurrency

Limit Value Reason
Daily match records 30 Prevent bulk entry
Same opponent per day 10 Slow down rating inflation via collusive trading
Daily practice records 20 —
Match date Within 1 year Narrow the path of bulk-injecting old records to manufacture ratings
Single-game score upper limit 50 Filter out typos (e.g., 110:9)

Limits are counted in the DB. To prevent two requests from the same person from being concurrently counted as the "29th" and passing together, people are lined up using DB advisory locks. When confirming, the match row is locked first, and the two player rows are locked starting from the one with the smaller ID . This is because if Alice and Bob simultaneously confirm different matches involving each other and lock "starting from my row", they would wait for each other's row and fall into a deadlock.

3. Record Screens

Screen What is Visible Who
My Records /tt/me My matches, pending/objections, ratings, practice log Self
Leaderboard /tt/rank Public player rankings, club/this-month filters, this week's risers Everyone
History /tt/me/history Aggregated changes in applications, matches, and scores; CSV download Self
Match Share /tt/m/{코드} A single shared match Link recipients

Historydoes not accumulate new records; instead, it reads and aggregates what each feature has already left behind (backend/app/tt_history.py). Match records, live scoreboard events (recalculated by the server), recording analysis results, auto-score correction records, and rating changes are mixed together in a single line. Since live scoreboards and match records are not linked by foreign keys, they are connected by estimation for matches with "identical end scores and close timestamps". Because misconnections can occur, the screen always displays the source of the flow (live or video). There are no user IDs in URLs, and all queries are bound only to the logged-in user, making it impossible to view another person's history. CSVs are exported with defenses against Excel formula injection.

Match Shareuses a specially generated 11-character code instead of a match ID. This is because match IDs are sequential numbers, and using them in URLs would allow browsing other people's matches one by one. Confirmed matches can have sharing enabled by either player, while personal records can only be shared by the person who recorded them. Matches under pending confirmation or objection are not shared because it hasn't even been determined whether the match "actually took place". On the share screen as well, players who have not made their profiles public appear as "Pong Player", and the names of opponents without accounts are never exposed.

4. Live Sharing: The Server Recalculates Scores

If "Share Live" is turned on during a match, people who receive the viewing link see the scores in real-time (backend/app/tt_watch.py).

The naming rules are the same. Links distributed directly by the recorder display the nickname set for that match, but on the home feedviewed by anyone, owners who have not enabled public profiles are masked as "Pong Player", and opponents are masked as "Opponent Player".

5. Corrected Records: Only With Consent, and Still 0 Count

As mentioned in Part 3, when humans respond to auto-judgment chips ("Correct", corrected to another player, "Invalid"), those responses become corrected records. Let's follow where these go.

What It Contains

The auto-score screen creates one row for each rally responded to by a human and sends it to POST /api/tt/corrections . The row contains the following (backend/app/tt_corrections.py).

. No video footage is included. The server does not insert what the browser sent as-is; instead, it regenerates only recognized structures. There is almost no space for text input, leaving nowhere for profanity, URLs, or scripts to accumulate. The row key includes the logged-in user's ID, preventing anyone from overwriting another person's row. Limits are 30 requests per minute per person, and 2,000 rows per day.

Judgments that passed automatically over time do not leave a correction record, because they were not verified by a human.

Who takes what

Never used for training so far

When reading the operational DB on 2026-10-18, the correction records were 0. Therefore, correction records have not yet been used for training or evaluation even once. All learning models in Part 5 were trained using public data and synthetic images.

There are plans. As seen in Part 5, when retraining the learning-based modules with commercially usable data, correction judgments from people who agreed to learning usage can become ingredients for modules like 「Verification Needed」 correction. This is because a 「human-corrected judgment」 is the correct answer for where the engine was wrong. However, for that to happen, enough correction records must accumulate, and reviewing retention periods and consent notices when using them as training materials comes first.

6. Video Does Not Leave the Device, and Exceptions

The primary principle of the camera scoreboard is camera video does not leave the device(backend/static/tt/play/cam.js Introduction). Recording is also synthesized inside the phone and saved on the phone. Text-to-speech for scores uses the browser's speech synthesis.

The exceptions are two features enabled by the user.

Automatic Scoring. Since the GPU finding the ball is on the server, frames are sent. Instead, the scope was narrowed (Part 1).

Upload Recording for Analysis. This is a separate feature where the user directly selects and uploads a video file. The browser first reduces it to 360p60 500kbps and uploads it, and the original is deleted after 30 days. Analysis workers work only through the web's internal API without access to the DB and storage keys.

7. Nicknames Instead of Real Names

To summarize the privacy design in one line: 「Preventing people met at the table tennis court from knowing my real name」 .

Location Displayed Name
Camera Scoreboard Screen Nickname set for that match (up to 12 chars, 「Nicknames instead of real names」 guide)
Match History / Opponent Screen Display name (2–20 chars). Before setting, 「Table Tennis Player #ID」
Leaderboard / Public Profile Only for those who enabled public visibility. Default is private
Opponent Without an Account Only to the person who logged that match
Live Home Feed Owner who didn't set public: 「Table Tennis Player」, Opponent: 「Opponent Player」
Correction Record Export No name, masked value changing with each export
Google Account Name Never exposed anywhere

The same principle applies to demo videos. Names embedded in the original screen (broadcast subtitles) are left as-is, but text we draw over them is used only as 「Player A/B」.

8. What Tables Exist on the Server

Here is a collection of record-related server-side tables. Each table has a DB migration number, and each migration introduced a feature.

Migration Table What It Stores What It Does Not Store
0036 tt_players Display name (2–20 characters, defaults to "Table Tennis Player #number"), rating, number of matches contributing to the rating, visibility (private by default) Google Name
0036 tt_matches Match duration, game scores, source (manual input, score helper, recorded video analysis), status (pending confirmation, confirmed, disputed, expired, personal record), rating change for both players Rally-by-rally history
0036 tt_practice Practice log (visible only to the user) —
0039 tt_live_matches·tt_live_events Live match and event columns (numbered points, undo, end). Adds an 11-character code for match sharing to the tt_matches in the same migration The scores themselves (recalculated by the server from events), video
0040 tt_corrections Original ruling, corrected ruling, event summary, rationale, consent status Video
0048 tt_videos Analysis queue and results for user-uploaded videos — (originals are deleted after 30 days)

A key takeaway from these tables is that "what you choose not to store is half of the design". Match records do not know about rallies, live matches do not store scores but only events, and corrected records do not include videos. Each table stores only what is strictly necessary for its feature, and the history screen reads and aggregates these tables without creating a new one (Section 3). What is not stored cannot leak either.

9. Conclusion

A word from TT. Today's match is remembered by your phone first. The server is only told "what the score was," and your name is used only as a nickname. Since the opponent needs to say "that's correct" for your score to go up, please make sure to confirm on days you win.

References

  1. A. E. Elo. The Rating of Chessplayers, Past and Present. Arco, 1978. (Original bibliography for Elo rating. The original text was not opened this time; our version can be checked in code backend/app/tt_records.py )
  2. International Table Tennis Federation. ITTF Statutes 2026, Chapter 2: The Laws of Table Tennis, §2.12 (Matches and Games). https://db.ittf.com/document
  3. Code and Design: Repository backend/app/tt_records.py·tt_history.py·tt_watch.py·tt_corrections.py·tt_live.py README, backend/static/tt/play/cam.js·lib/cam-sheets.mjs·lib/camauto.mjs.

Further Reading

Sign in to like

Comments

No comments yet.

Sign in to leave a comment