Security
Pongdang is local-first: an academy's data lives on its own device. This page explains what is protected, how, and what is not. We avoid absolute claims: no software is unbreakable.
Where data lives
- Students, classes, passes, attendance, absences, make-ups and the admin log are stored in the browser's IndexedDB on the academy's device. They are not encrypted at rest by Pongdang; protecting the device itself (screen lock, disk encryption, keeping the OS updated) is the base layer.
- Our server never stores academy records. It serves the app files and briefly holds encrypted QR check-in messages.
Encryption
- Backup files: AES-256-GCM, with a key derived from the 24-character recovery code (about 110 random bits) using PBKDF2-SHA256 (210,000 iterations, salted with the studio ID) and HKDF. Data is compressed and encrypted on the device before it is saved anywhere.
- Key rotation: making a new recovery code replaces the backup key; new backups use the new key. Old backups still need the old code.
- QR self check-in: the student's phone encrypts the check-in to the academy's public key (ECDH P-256, HKDF, AES-256-GCM, padded to hide small size differences). The academy's answer (that student's remaining sessions) is encrypted to a one-time key held only by that phone. The server sees only ciphertext, arrival time and message sizes.
- All cryptography uses the browser's built-in WebCrypto. No custom ciphers.
Admin mode
- Editing attendance, passes, students, classes, settings and backups can be protected with a PIN or passphrase (PBKDF2-SHA256, 200,000 iterations, random salt) and, where the device supports it, a platform authenticator (fingerprint or face via WebAuthn, verified on the device).
- The app locks after a few idle minutes (1 to 30, chosen by the owner). After 5 wrong attempts it waits 1 minute, then doubles the wait up to 1 hour.
- Check-in mode on a shared tablet shows only the QR code and a code entry; leaving it requires the PIN or biometric.
- Every admin change is written to an append-only log inside the studio data (time, device name, what changed). It is included in encrypted backups.
- Limit: the admin lock protects the app's screens from people using the device (students, parents, staff). It is not encryption of the data on the device. Someone with full control of an unlocked device and developer tools can read local data.
Self check-in: what it stops and what it doesn't
- Each check-in deducts from that student's own pass, so the default is one simple printed QR.
- Optional (off by default): a live QR on a tablet that changes every 30 seconds (accepted for about 2.5 minutes), and a one-time location check at check-in (compared on the academy device; coordinates are not stored, only pass or fail). Both raise the bar but are not proof: a photo can be relayed in real time and phone locations can be faked. Flagged check-ins wait for the owner's approval.
- Replays: a message older than 10 minutes, or a repeated one, is ignored. A second check-in for the same class is not counted twice.
Check-in cards, numbers and camera
- A printed card's QR holds only `PD1:` plus the student's random 8-character code. It contains no name or other personal data. Reissuing a card replaces the code, so the old card and the old phone check-in code stop working.
- Kiosk numbers (4 to 6 digits) are chosen by the owner and unique within the academy. They are short by design. After 5 wrong numbers the pad waits 30 seconds, then doubles up to 5 minutes, and the name is shown for confirmation before a session is deducted. Anyone who knows or guesses a number, or holds a card, can check in as that student. Owners can see and undo every check-in.
- The camera is used only in check-in mode, only after a tap. Frames are decoded on the device and never sent anywhere.
Website and server
- Strict Content-Security-Policy (only our own files; no inline scripts; Trusted Types required), no third-party scripts, fonts or analytics, no cookies, no-referrer, frame-ancestors none. The camera is allowed for our own pages only (Permissions-Policy).
- The check-in inbox stores only ciphertext and deletes messages when the academy device collects them, or after 14 days. Answers are deleted when read, or after 1 day. Inputs are strictly validated, request bodies are capped at 4 KB, and per-inbox and global daily quotas keep us inside the free plan. The read secret is stored only as a hash.
- Rate limiting per server instance is best effort; daily quotas are enforced in the database.
Excel export
- Exporting needs the admin PIN again and is written to the admin log. Exported CSV/ZIP files are **not encrypted**: anyone with the file can read names and phone numbers. Cells that could run as spreadsheet formulas are prefixed so they open as text. Students' 8-character check-in codes are not exported.
Free trial and activation
- Anyone can create an academy. The first **10 recorded check-ins** (phone QR, card, kiosk number and the owner's own taps; existing academies count from the 2026-10-06 update) use every feature. After that, recording check-ins and issuing new passes pause until the academy is activated with an access code. Viewing data, encrypted backups and Excel export always keep working: data is never held hostage.
- **Server-side (enforced):** phone QR check-ins go through our Pages Function + D1. The server counts drops per academy inbox; the 11th and later drops are stored as *held* and are not delivered to the academy device until the inbox has a current activation (`activated = 1` and `act_exp > now`). Held drops are delivered (and recorded) right after activation; like all drops they expire after 14 days. The student's phone shows "the academy needs to activate".
- **Access code:** checked only on the server (`POST /api/inbox/activate`), against the Pages secret `ACCESS_CODE_SHA256`. Neither the code nor its hash is in the app, the repository docs or public pages (a test checks this). Attempts need the inbox's read secret and are limited to 5 per academy per day and 500 per day overall. Missing secrets fail closed.
- **Activation token:** on success the server returns an ES256-signed token `{iid, exp}` bound to the academy's inbox id and valid for 14 days (signing key = Pages secret `TOKEN_PRIVATE_JWK`). The app verifies the signature with the public key in `config.js`, so a hand-written token is rejected, and renews it with `POST /api/inbox/renew`, which re-checks the server-side activation flag. If activation is revoked on the server (flag cleared), renewal fails and QR relaying stops when the current token expires. Offline, the app accepts a genuine token for 7 days past its expiry.
- **Client-side (convenience, tamper-evident only):** the local counter is computed from the check-in records and the `license` record (start date, token), which travel inside the AES-GCM-encrypted backups. **Honest limit:** check-ins that never touch the server (kiosk number, card scan, owner taps) are enforced only in the browser. Someone who edits their own browser storage or modifies the app can keep recording those offline; we cannot prevent that without a server round-trip for every check-in. Likewise, an academy that deletes its inbox and registers a new one (a new QR poster) gets a new server-side count. The server limit covers the phone QR flow, which is the main way students check in.
- The server stores per inbox only: a total relayed count, the activation flag and the token expiry. The code itself is not stored and no academy data is sent.
Known limits
- Losing both the device and the recovery code means the data cannot be recovered. We do not hold a copy or a master key.
- Anyone with a backup file and its recovery code can read it. Store the code separately from the backups.
- The server can see metadata: when check-ins arrive, how many and how large.
- If the academy device is offline, QR check-ins wait on the server (up to 14 days) and the phone cannot show remaining sessions until the device collects them.
- Browser storage can be cleared by the user or, rarely, by the browser under storage pressure; the app asks for persistent storage, and regular backups are the safety net.
Reporting
Please report security issues to qkqhqkqh112112@gmail.com. We will reply as soon as we can.