Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
www
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
1
Issues
1
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
PLN
www
Commits
067dd66a
Commit
067dd66a
authored
Nov 01, 2026
by
PLN (Algolia)
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
decide: comments v1 build plan — schema, endpoints, spam triad, build order
parent
c932c879
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
88 additions
and
0 deletions
+88
-0
plan.md
decisions/2026-11-01-comments-reactions/plan.md
+88
-0
No files found.
decisions/2026-11-01-comments-reactions/plan.md
0 → 100644
View file @
067dd66a
# 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.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment