Skip to content

Repository files navigation

dsa-forum

A same-origin DSA community forum built with TypeScript, Fastify, React, and PostgreSQL. The MVP supports anonymous browsing, DSA identity handoff, topics, replies, author editing with immutable revision history, rate limits, emergency moderation, private in-app notifications, and private follows with an activity feed.

Requirements

  • Node.js 22+
  • pnpm 10.33
  • Docker with Compose (for the local database and integration gate)

Quick start

pnpm install
docker compose up --build

Open http://localhost:8080. The example Compose configuration supports anonymous browsing. Replace its placeholder Foundation URL and handoff key before testing DSA sign-in.

Run make help for wrappers around common dependency, Compose, build, and verification commands. Port overrides can be passed directly, for example make stack-up APP_PORT=18080 POSTGRES_PORT=55438.

For hot-reload development, start PostgreSQL and both development servers with:

make dev

make dev binds all services to loopback. Its preferred ports are PostgreSQL 5432, backend 8080, and Vite http://127.0.0.1:5173; if another app owns a default, it selects the next free ports and prints the resulting URLs. If this checkout's Compose stack is running, it stops those verified containers without removing them or their data before starting the hot-reload environment. Explicit POSTGRES_PORT, DEV_BACKEND_PORT, and DEV_WEB_PORT overrides fail closed if occupied (for example, make dev DEV_WEB_PORT=15173). Ctrl-C stops both hot servers but leaves this checkout's PostgreSQL 17 container and named volume for reuse. Run make dev-stop to stop only that database container; it does not remove the container or volume.

Configuration

See .env.example for all variables.

  • APP_ORIGIN is the exact canonical public origin. Production requires HTTPS.
  • FOUNDATION_BASE_URL is the pinned HTTPS DSA identity service origin.
  • HANDOFF_ENCRYPTION_KEY is exactly 32 random bytes encoded as base64.
  • MODERATOR_DIDS is a required, comma-separated allowlist of valid DIDs.
  • TRUSTED_PROXY_CIDRS is required in production and must list only the TLS proxies allowed to supply forwarding headers. Keep the application port private behind those proxies.
  • Development authentication is available only on a non-production loopback origin. Enable it with DEV_AUTH_ENABLED=true and set one server-selected DEV_AUTH_DID; that DID cannot be a moderator.
  • Member profiles have an optional display name, plain-text bio, and up to three canonical HTTPS links. Profile links are unverified user-generated destinations: the forum never fetches, resolves, previews, or otherwise endorses them.

The application enforces a single running backend instance with a PostgreSQL advisory lock because rate limits are process-local. A second instance fails startup, and loss of the lock connection stops the server. Distributed or multi-instance deployment requires replacing the limiter and singleton model.

Verification

pnpm lint
pnpm typecheck
pnpm test
pnpm build
pnpm test:postgres
pnpm test:e2e

test:postgres builds twice, runs the built migrator against a fresh PostgreSQL 17 database, verifies the seed and migrations, executes PostgreSQL integration tests, and tears down its Compose resources. It uses host port 55437 by default; override with POSTGRES_PORT if needed.

test:e2e builds the runtime image in an isolated Compose project, starts the full stack on ports 18080/15432 by default, and runs a Chromium plus axe accessibility smoke test. It seeds three disposable hashed sessions directly in its isolated PostgreSQL database, verifies reply/reaction inbox delivery and private follow/activity and Following-list concealment and restoration, and fails on browser console/page/request errors or browser requests outside the app origin. Install its browser once with pnpm exec playwright install chromium. Override the ports with E2E_APP_PORT and E2E_POSTGRES_PORT.

Both integration scripts use dedicated Compose project names and remove only their own disposable volumes.

MVP behavior

  • Anonymous category, topic, and cursor-paginated post browsing
  • Anonymous full-text search over current visible topic titles and post bodies, grouped by topic and ranked with title matches boosted; search results use best-effort cursor pagination while edits or moderation can change rank or visibility
  • Identity-only DSA PKCE handoff and revocable server-side sessions
  • Transactional topic/first-post creation and replies
  • Author-only title and post-body editing with public, cursor-paginated, append-only revision history
  • Edits do not change topic activity ordering. Hidden content is not editable, and its revision history is available only to configured moderators.
  • Plain-text titles and posts; no rich text or attachments
  • Per-IP, session, and user write limits plus login limits
  • Deployment-allowlisted moderation: hide, lock, pin, and suspend
  • Append-only moderation audits and public activity that excludes hidden posts
  • Fixed post reactions: Solidarity, Useful, and Insightful. A signed-in, non-suspended member may select each kind once on a visible post, including any combination of the three. Visible posts expose public counts and the current viewer's selections; signed-out and suspended viewers are read-only, and hidden content cannot be reacted to.
  • Reaction PUT and DELETE operations are idempotent. Counts are derived from reaction rows (not cached); a newly selected reaction by another member creates one private notification and reactions do not affect ranking.
  • Reactions have an independent fixed-window budget of 60 requests/minute per IP and 40 requests/minute per session and user. The single-instance/proxy constraints above apply to this in-process budget too.
  • Public DID-linked profiles show the optional name, plain-text bio, and safe external links; profile owners replace their profile using versioned stale-form protection. Configured moderators can hide or restore profiles with an audit trail. Hidden profiles return public 404.
  • Private notifications retain rows for exactly 90 days: cleanup deletes only rows whose creation time is strictly older than the 90-day boundary. A hidden topic or post disappears from its recipient's inbox and unread badge on the next authoritative page/count refresh; restoring it restores its prior notification and unread state. Retention-lag objective: one hourly scheduling interval plus active drain time.
  • Notification actors are presented as display name (DID) when a visible profile name exists, otherwise as their authoritative DID alone.
  • Following is private: only its owner can read outgoing Following data, relationship state, and derived activity. Following never notifies the followed member, and incoming followers remain undisclosed. Hidden followed profiles retain their edges but are excluded from Following rows and counts (as well as relationship state and feed activity) until restored.

Not included: deletion, avatars, private messages, category or role administration, email/push notifications, realtime updates, federation, external search, richer profile moderation, or upstream identity credentials. There is no public graph, recommendations, materialized counters, follower list/count, or incoming follower disclosure. Search does not index revision history, fetch or preview external content, provide saved searches, or notify users. Arbitrary emoji reactions, reaction ranking, and cached reaction counters remain deferred.

Operational recovery

If all moderators become unavailable or suspended, update MODERATOR_DIDS with a fresh trusted DID, restart the single backend instance, authenticate as that identity, and restore the affected account. Configuration remains the break-glass authority; there is no role-management UI in the MVP.

Back up PostgreSQL before deployment and before applying new migrations. Run only one application instance, deploy the new image before startup migration, and retain the previous image and database backup for rollback. Loss of the singleton lock intentionally forces a bounded nonzero shutdown.

About

Community forum application

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages