0003. Separate learners from the ways they log in¶
Status: Accepted, 2026-09-19 (#208); extended to browser logins 2026-09-20 (#219)
Context¶
A learner's record is addressed by a StudentID. The bot used to derive it from the Telegram chat id (TelegramStudentID.encode(chat_id)), which tied "who is this learner" to "which chat is this". That worked while Telegram was the only way in, but left nowhere for a second login to point: logging in to the web app with the same Telegram account, or a future email login, couldn't reach the learner a chat already had.
When web login was added, its cookie first carried the StudentID itself (student:{student_id}).
Decision¶
Keep two concepts apart:
- a learner is a
StudentID, an opaque random id that addresses the record; - a login is a provider (
telegram,browser) plus that provider's identifier, and is resolved to a learner byStudentIdentityStore.
In code, a learner is a StudentID and a login is a (provider, identifier) pair; earlier discussions call them account and credential. Every frontend resolves a login before touching a record. A logged-in browser holds a random browser token, linked to the learner, not the StudentID (#219). Anonymous web visitors aren't logins and stay out of the store.
Alternatives considered¶
- Derive ids per provider (the old scheme). Simple, but one learner can never have two logins.
- Introduce a separate
AccountID.StudentIDalready is the account; a second id type would be two names for one value. - Put the
StudentIDin the web cookie. One lookup fewer, but a leaked cookie reveals the permanent id, and logging one browser out would mean changing the learner's id. - Count anonymous visitors as logins. Every page view would become a learner, making the store's size meaningless.
Consequences¶
- A learner who uses the bot and then logs in on the web lands on the same record.
- New ways to log in are a new provider, not a new id scheme.
- Every request costs one more Redis lookup (observed, not discussed in #208).
- Switching schemes orphaned the records created under the old one; there were no real users to migrate yet.