No description
  • Go 85%
  • HTML 9.6%
  • CSS 2.5%
  • JavaScript 1.7%
  • PLpgSQL 0.7%
  • Other 0.5%
Find a file
2026-07-12 22:10:43 +02:00
cmd/forum README: document forum setup interactive wizard 2026-07-12 21:59:33 +02:00
deploy Initial commit: neighborhood forum 2026-07-11 20:27:40 +02:00
internal README: document forum setup interactive wizard 2026-07-12 21:59:33 +02:00
scripts add source bundling 2026-07-12 21:51:05 +02:00
.env.example Locate communities by domain + path; drop routing modes 2026-07-12 18:40:28 +02:00
.gitignore add source bundling 2026-07-12 21:51:05 +02:00
Containerfile add source bundling 2026-07-12 21:51:05 +02:00
go.mod README: document forum setup interactive wizard 2026-07-12 21:59:33 +02:00
go.sum Notifications, DMs, ops tooling; make content alerts opt-in per category 2026-07-12 11:47:47 +02:00
LICENSE release under CC0 2026-07-12 22:10:43 +02:00
Makefile README: document forum setup interactive wizard 2026-07-12 21:59:33 +02:00
README.md README: document forum setup interactive wizard 2026-07-12 21:59:33 +02:00
sqlc.yaml Notifications, DMs, ops tooling; make content alerts opt-in per category 2026-07-12 11:47:47 +02:00

forum

Self-hostable discussion board for neighborhood communities — a replacement for Facebook groups. Server-rendered Go application with PostgreSQL as the only infrastructure dependency.

Design goals

  • One static binary + PostgreSQL (with the pgmq extension). Nothing else.
  • Works without JavaScript; JS only progressively enhances. Installable as a PWA.
  • Notifications first: web push and email digests are the core feature — members don't scroll the site all day, the site comes to them.
  • Private, invite-only communities with an invite-token economy (admins grant tokens; 1 invite = 1 token; revoked/expired invites refund).
  • Multi-tenant: one deployment hosts several communities, resolved by URL path (/osiedle/...) or by host. Accounts are global, profiles per community.
  • EU/GDPR by design: data export, account erasure, EXIF stripping, no tracking, one-click unsubscribe, Polish-first UI (pl/en).

Quick start

Requirements: Go 1.26+, PostgreSQL 15+ with the pgmq extension available (e.g. the ghcr.io/pgmq/pg17-pgmq image), SMTP credentials (any EU transactional provider; Mailpit in development).

Fast path — interactive wizard (recommended):

make build
./forum setup            # prompts for DB, community, admin invite in one go
./forum serve            # HTTP server
./forum worker           # notification worker (separate process)

forum setup walks through database configuration, applies migrations, generates VAPID keys, bootstraps the first community and category, and prints an admin invitation link. Start the two processes and you're done.

Manual path:

cp .env.example .env            # set DATABASE_URL, SMTP_*, etc.
make build
./forum migrate                 # apply schema migrations (run on every deploy)
./forum admin gen-vapid         # once; put keys in the environment
./forum serve                   # HTTP server
./forum worker                  # notification worker (separate process)

Bootstrap the first community and its admin:

./forum admin create-community -slug osiedle -name "Nasze Osiedle"
./forum admin create-category -community osiedle -slug ogolne -name "Ogólne"
./forum admin invite -community osiedle -email you@example.eu -role admin
# open the printed accept link, pick a display name — you're in

From there everything happens in the browser: admins grant invite tokens on the Members page, members invite neighbors, moderators handle the report queue.

Architecture

  • cmd/forum — one binary, four modes: serve (HTTP), worker (notifications, digests, maintenance), migrate, admin (operator CLI).
  • internal/web — HTTP controllers, embedded templates and static files. Every mutation is a plain form POST with CSRF; JS (embedded, no build step) only enhances. Strict same-origin CSP.
  • internal/db — the single data-access layer: sqlc-generated queries over pgx, tern migrations (embedded), and hand-written pgmq helpers.
  • Use-case packages (accounts, invites, content, mod, gdpr, media, notify) coordinate DAL calls; operations run in one transaction per request.
  • Transactional outbox: creating a thread/post enqueues an event onto pgmq in the same transaction. The worker fans events out into per-recipient web-push jobs, delivers them (pruning dead subscriptions), and builds daily/weekly email digests directly from content, with per-profile hour/timezone and exactly-once claims. RFC 8058 one-click unsubscribe on every digest.
  • Invite-token economy correctness: conditional decrement (balance never negative), guarded status transitions (no double accept/revoke), and a partial unique index making refunds exactly-once — verified by concurrency tests.
  • Attachments: images are re-encoded (EXIF/GPS stripped), downscaled, thumbnailed, stored on the filesystem per community behind a BlobStore interface (swap for S3 without touching callers).

Development

make test              # unit tests
TEST_DATABASE_URL=postgres://forum:forum@localhost:5432/forum_test make test
                       # + integration tests (needs pgmq-enabled Postgres)
make sqlc              # regenerate the DAL after editing queries/migrations
go run ./scripts/genicons   # regenerate PWA icons

Set DEV=1 for pretty console logs. Without SMTP configured, emails are logged instead of sent — magic-link URLs appear in the log.

Deployment

  • Containerfile — static binary in a distroless image.
  • deploy/podman-pod.yaml — single-host Podman pod: pgmq-enabled Postgres + web + worker (podman kube play).
  • deploy/k8s.yaml — Kubernetes: web/worker Deployments, migrate Job, attachments PVC.

Run migrate on every deploy before starting serve/worker. Back up Postgres (pg_dump/PITR) and the attachments directory.

Legal pages. /privacy and /terms ship with placeholder text that every operator must adapt. To do so, drop Privacy.md and Terms.md in the working directory the serve process starts from; each is rendered as Markdown (sanitized) and replaces the built-in placeholder for that page. Either file is optional — a missing one falls back to the placeholder. The files are read once at startup, so restart serve after editing them.

Operational notes

  • iOS web push requires the PWA to be installed to the home screen — the email digest is the universal fallback channel, which is why it is first-class.
  • Multiple worker processes are safe (pgmq visibility timeouts, guarded digest claims); multiple serve processes are safe behind a load balancer (sessions live in Postgres).
  • /healthz (liveness) and /readyz (DB reachability) are wired for probes.