Capability pack
Community
Text from strangers, and the machinery that keeps it from becoming a liability: one moderated substrate behind both forum threads and page comments, an admin queue, rate limits per person rather than per site, and notifications so a reply reaches the person it answers.
Capabilities
7
All of them are built.
Watched in production
0
7 have never been watched against a real provider.
Provider variables for a fresh project
5
Including the packs it sits on. 3 unset here, each running its documented fallback.
Needed regardless of providers
- A Postgres database — threads, comments, reports, and the moderation queue.
What is in it
Each row links to where it lives, and carries the state it has in THIS deployment.
Categories, threads, pinning, locking, and pagination — with every reply reusing the comment system, so plain text, reports, the flag threshold, and one admin queue cover the forum without a second implementation. Sorting is pinned first then last activity, updated by every reply.
ForumCategory and Thread rows; replies are Comment rows on a thread instead of a post
Members comment under posts and reply one level deep; bodies are stored as typed and rendered as text with links recognized at render time. Own edits for fifteen minutes, soft deletes, per-user rate limits, a verified-email requirement behind a flag. Three reports flag a comment and notify admins; the queue hides, restores, or hides everything by an author and disables them.
Comment and CommentReport rows, plain text rendered as text, a flag threshold, an admin queue
- Ready
Written in the same transaction as the event they announce; mark-read scoped to the reader in the WHERE clause; the badge polls every 30s only while the tab is visible.
Notification table + a polling bell
A visitor confirms by a link only their inbox received; the screen never says whether an address is listed. Issues are Markdown through the pages sanitizer, tested on the admin first. Sending writes one outbox row per recipient in a transaction and drains in batches with retries, so a provider outage delays and never loses. Every issue carries a signed one-click unsubscribe and a List-Unsubscribe header.
Needs
RESEND_API_KEY. Without it: Confirmations and issues print to the console; the outbox still drains.Subscriber, NewsletterIssue, and MailOutbox rows; signed unsubscribe links; a batch drainer with backoff
- Ready
Weighted tsvector columns kept current by a trigger per table — posts, products, pages and threads. Generated columns came first and could not stay: Prisma cannot model one, read the generation expression as a default, and emitted a DROP DEFAULT that the next unrelated migration died on. A plain column a trigger maintains is invisible to that diff. A bound-parameter raw query reads them; highlights are rendered as text, never HTML. Threads are indexed by title alone, so a search cannot surface the body of a reply moderation has hidden.
Postgres tsvector + GIN across four tables, no service
Rate limiting
ReadyKeyed per caller, so an abuser is throttled and not the site. Durable by default: one atomic upsert per hit holds the limit across serverless instances; RATE_LIMIT_STORE=memory keeps the per-process limiter, which is also the fallback if the database call fails.
Postgres fixed windows (atomic upsert), or in-memory
Bot protection
FallbackSign-up, forgot-password, and contact carry a hidden field a person never fills and a script always does. With Turnstile keys set, an invisible challenge is verified server-side too; the CSP admits the widget by derivation. One generic message for every failure.
Needs
NEXT_PUBLIC_TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEY. Without it: The honeypot field runs alone, which catches naive scripts.Honeypot always; Cloudflare Turnstile when configured
First steps in a fork
- 1Create the forum categories at /admin — the seed makes a starting set.
- 2Decide who may post: the flags in src/lib/flags/rules.ts gate writing.
- 3Watch the moderation queue at /admin/comments for the first week; the defaults are deliberately strict.
What to delete
- The shop and the game.
Provider variables a fresh project would set
AUTH_GOOGLE_IDAUTH_GOOGLE_SECRETNEXT_PUBLIC_TURNSTILE_SITE_KEYRESEND_API_KEYTURNSTILE_SECRET_KEY
Shaded green: set on this deployment.
The full inventory, with the tests that cover each capability and what has actually been watched working.
Capability inventory