Commit 067dd66a by PLN (Algolia)

decide: comments v1 build plan — schema, endpoints, spam triad, build order

parent c932c879
# Comments & reactions v1 — build plan (ratified 2026-11-01)
Owner decisions live in `notes.md` (the decide-SPA export); architecture in
`index.html`. This file is the build order. Service home: **nechapi**
(`~/Work/Perso/nechapi`, skill `nechapi-ship`) — container tier on erable,
served at api.nech.pl behind the gateway.
## Service: `comments` (nechapi container tier)
SQLite file (one, WAL mode) + nightly dump into erable's existing backup
regime. No captcha, ever. Antispam = layered friction + hold-for-approval:
honeypot field + min-submit-time (hidden timestamp, reject < 3s) + per-IP rate
limit (edge, nginx or gateway) + default-on hold.
### Schema (v1)
```sql
posts(slug TEXT PRIMARY KEY); -- whitelisted blog slugs
comments(id INTEGER PK, post_slug TEXT REFERENCES posts,
parent_id INTEGER NULL REFERENCES comments(id), -- threading (v1)
author_name TEXT NOT NULL, author_email TEXT NULL,-- email never displayed
body_md TEXT NOT NULL, -- rendered server-side, sanitized
score INTEGER DEFAULT 0, -- up/down votes net
status TEXT DEFAULT 'hold' -- hold|approved|rejected
ip TEXT, created_at TEXT);
comment_votes(comment_id, ip, value 1/-1, PK(comment_id, ip));
reactions(post_slug, ip, emoji TEXT, created_at, PK(post_slug, ip, emoji, day));
```
Whitelist rule: a post slug must exist in `posts` (synced from the www build
or on-demand via `PUT /posts/<slug>`) — no comments on phantom URLs.
### Endpoints
- `GET /v1/www/posts/<slug>/comments` — approved threads (nested by
`parent_id`), long-cache (`Cache-Control: public, max-age=60, stale-while-
revalidate=300`), ETag.
- `POST /v1/www/posts/<slug>/comments` — hold-queue insert; returns 202 + the
honeypot/min-time/ratelimit triad ahead of it.
- `POST /v1/www/comments/<id>/vote` (+1/-1, one per IP per comment).
- `GET /v1/www/posts/<slug>/reactions` / `POST` — curated emoji set only
(owner-provided list in service config; reject others), 1 per IP/emoji/day.
- `GET /v1/www/admin/queue` + `POST /v1/www/admin/comments/<id>/approve|reject`
— token-gated (long token, 1Password-held per secrets doctrine).
- **Email on hold**: existing erable SMTP → mail with signed one-click links
(`/v1/www/admin/comments/<id>/decide?token=<hmac>`), per the owner's
"click easy links to accept/reject".
### Security invariants
- Email never returned by any GET (name only). Body markdown rendered and
sanitized server-side (allowlist tags; no links in v1 bodies until spam
track record exists).
- Admin + decide links: constant-time token compare, HMAC-signed, single-use.
- Rate limit per IP at POSTs; vote/reaction endpoints idempotent per PK.
## Blog island (www repo)
- Client island `CommentsSection` under `PostArticle` (same register as the
embed facades): fetch approved threads on load, render threaded with vote +
reaction affordances, noscript → honest note ("comments load with JS").
- Endpoint base from `NEXT_PUBLIC_*` env; dev points at localhost instance.
- Enhancement pass (after v1 ships): bake top/last comments into the static
HTML at build as the no-JS seed (owner's plan-B nicety).
- The emoji picker renders the curated set from a config shipped by the
service (`GET /v1/www/reactions/meta`), never hardcoded in the blog.
## Build order
1. nechapi: scaffold `comments` service on the golden base (skill
`nechapi-ship`), schema + migrations, GET/POST comments with hold queue.
2. Admin page + token auth + SMTP decide links.
3. Votes + reactions endpoints + curated-emoji config.
4. www: `CommentsSection` island + env wiring; POST form with honeypot
triad; thread rendering + votes + reactions UI.
5. E2E probe (Playwright, evidence-first): post → hold → email link →
approve → visible; spam triad rejects; rate limit trips.
6. Deploy www + service together; watch for 1st real spam wave before
relaxing anything.
## Acceptance (v1 done when)
- A comment posted from the blog lands in hold, reaches erable's mail, and
one click approves it; it then renders threaded on the post without a
redeploy.
- Reactions count per curated emoji, one per IP/emoji/day, no moderation.
- No captcha anywhere; zero unauthorized HTML lands on a page (hold queue).
- Backups: nightly dump verified restorable once.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment