08749df050
Swap modernc.org/sqlite for jackc/pgx/v5 with no observable change: same endpoints, same wire format, same updated_at ordering rule. The schema now comes from numbered SQL embedded in the binary and applied on startup, one transaction each, recorded in schema_migrations. That replaces two pieces of SQLite-era machinery, both deleted rather than ported: the column probing (Postgres has ADD COLUMN IF NOT EXISTS, and there is no legacy database left to probe) and the Asura key rewrite, which has run clean on every start for months now that the userscripts strip build hashes before writing. Its regexp survives as latest.asuraBuildHash, where the poller still needs it to scope chapter links to a series whose slug carries a rotating hash. Types get real: favorite is a boolean, chapter numbers double precision, timestamps stay unix-ms bigint. SQLite's null-safe IS NOT becomes IS DISTINCT FROM, which is what implements the rule that only reading progress reorders a list. Inside COALESCE/NULLIF the status and kind parameters need an explicit ::text -- there is no target column to infer from and Postgres refuses to guess. Tests lose their free t.TempDir() database, so Docker is now a hard prerequisite for `go test ./...`: internal/pgtest starts one postgres:17-alpine per test binary and hands each test a database of its own. Also lands CONTEXT.md and the four ADRs written while scoping #18. BREAKING CHANGE: DB_PATH is retired for DATABASE_URL, which is required and has no default. Compose gains a postgres service on an internal network with its own volume; POSTGRES_PASSWORD joins .env. The old bookmarks-data volume is deliberately left undeclared so `docker compose down -v` cannot take the pre-migration database with it. main is not deployable until #25 and #26 land. Closes #20 Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com> Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
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.
|