Skip to content
All packs

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.
Sits on top ofAccounts10 inherited capabilities, counted once in the number above.

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

  • 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

  • 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

    Ready

    Keyed 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

    Fallback

    Sign-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

  1. 1Create the forum categories at /admin — the seed makes a starting set.
  2. 2Decide who may post: the flags in src/lib/flags/rules.ts gate writing.
  3. 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_ID
  • AUTH_GOOGLE_SECRET
  • NEXT_PUBLIC_TURNSTILE_SITE_KEY
  • RESEND_API_KEY
  • TURNSTILE_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