docs: record the domain model and the Postgres/OAuth ADRs
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
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user