7a0c190ebe
Written while scoping #18. CONTEXT.md pins the ubiquitous language (Series, Reader, Bookmark, Progress, Latest Chapter, Poll) that the schema split and the ordering rule are argued in; the four ADRs record the decisions that follow from it, starting with Postgres over SQLite. Refs #18
45 lines
2.6 KiB
Markdown
45 lines
2.6 KiB
Markdown
# Identity comes from Discord OAuth; we store no passwords and send no email
|
|
|
|
Status: accepted
|
|
|
|
The service is being published to a community that already lives on Discord, and we have
|
|
no transactional email infrastructure. Rather than build email verification and password
|
|
reset to get accounts, Readers sign in with Discord OAuth2 (authorization code grant,
|
|
`identify` + `guilds.members.read`), and guild membership replaces both the invite gate
|
|
and the email-verification step. No password is ever stored and no mail is ever sent.
|
|
|
|
## Considered options
|
|
|
|
**Email + password with invite codes, no verification.** Viable and dependency-free:
|
|
an invite code proves community membership, which is what email verification was
|
|
standing in for anyway. Rejected because it still requires password hashing, a manual
|
|
admin-driven reset path, and a credential store — all of which Discord removes.
|
|
|
|
**Email + password with a transactional provider** (Resend, Brevo). Rejected as
|
|
premature: it builds verification and self-serve reset before anyone has asked for them,
|
|
and adds deliverability as an operational concern.
|
|
|
|
**Discord OAuth.** Chosen. It is less code than either alternative — no hashing, no
|
|
reset flow, no invite table — and the authorization question ("is this person in my
|
|
community?") is answered by the same call that answers the authentication question.
|
|
|
|
## Consequences
|
|
|
|
- **Availability is now coupled to Discord.** If Discord's OAuth endpoint is down,
|
|
nobody can start a new session. Existing sessions are unaffected, which bounds the
|
|
blast radius.
|
|
- **Identity is a Discord snowflake.** Migrating off Discord later means re-identifying
|
|
every Reader, because we hold no other credential for them. This is the lock-in the
|
|
decision buys, and it is the reason this ADR exists.
|
|
- **`guilds.members.read` is checked at login, not continuously.** Someone who leaves
|
|
the guild keeps their session until it expires. Acceptable; revocation is a session
|
|
delete, not an architectural change.
|
|
- **The userscripts cannot use OAuth.** They run in an isolated world on third-party
|
|
pages with no redirect surface, so they keep a bearer token — now issued per Reader by
|
|
the backend rather than a single shared `API_TOKEN` literal. OAuth gates the web UI;
|
|
the web UI is where a Reader obtains their personal userscript.
|
|
- `WEB_PASSWORD` disappears, and with it the session HMAC key derivation
|
|
(`sha256(API_TOKEN | WEB_PASSWORD | …)`), which needs a replacement secret.
|
|
- Seeding the first Reader during migration requires knowing the owner's Discord user
|
|
ID up front — a stable snowflake, copied from the Discord client.
|