A full-stack forum application built with Go (backend) and vanilla JavaScript (frontend). Users register, log in, create posts, comment, react, and receive notifications. A four-tier moderation system (guest → user → moderator → admin) is enforced entirely server-side.
- Features
- Technology Stack
- Architecture
- Prerequisites
- Installation
- Environment Variables
- Running the Application
- Testing
- Database
- Authentication
- Moderation
- API Overview
- Deployment
- Troubleshooting
- Project Documentation
- Contributors
- Email / password registration and login
- Secure password hashing with bcrypt
- UUID session tokens stored in HttpOnly cookies
- Google and GitHub OAuth login
- Password reset via email link (falls back to stderr log when SMTP is unset)
- Create, edit, and delete posts
- Draft / published / archived states
- Image uploads on posts and comments
- Category tagging (many-to-many)
- Like / dislike reactions (mutually exclusive per user per entity)
- Triggered by: post reactions, comment reactions, new comments on your posts
- Polling-based (no WebSocket required)
- Badge counter, dropdown panel, sound feedback
- Mark as read individually or in bulk
- Public profile: avatar, username, role, bio (≤ 280 chars), join date, stats, published posts
- Owner view: additionally shows email, inline avatar/bio editing, and full activity
- My Activity (posts, comments, liked/disliked posts) merged into the owner's profile page
/activityredirects to the owner's profile
Four roles enforced server-side (lowest → highest):
| Role | Capabilities |
|---|---|
| Guest | View posts, comments, and reactions (read-only) |
| User | Create/edit/delete own posts and comments, react, request moderator role |
| Moderator | Delete any post/comment, report posts to admins |
| Admin | Manage roles, approve/reject moderator requests, receive/reply to reports, manage categories |
Key invariants:
- First registered user is automatically made admin.
- An admin cannot demote themselves.
- The system refuses to remove the last admin (enforced in the persistence layer).
| Component | Technology |
|---|---|
| Backend | Go (standard library) |
| Frontend | Go (template server) + Vanilla JavaScript |
| Database | SQLite (via github.com/mattn/go-sqlite3) |
| Auth | bcrypt (golang.org/x/crypto), UUID sessions (github.com/google/uuid) |
| OAuth | Google, GitHub (standard net/http, no OAuth library) |
| Container | Docker + Docker Compose |
Browser
↓ HTTP (port 3000)
Frontend Server (cmd/frontend)
├── Renders HTML templates (web/templates/)
├── Serves static assets (web/static/)
└── Proxies /api/ →
Backend Server (cmd/backend, port 8080)
├── Middleware: Recoverer → CORS → (optional/required) Auth
├── Router (internal/router/ — all routes under /api/v1)
├── Handlers (internal/handlers/ — request validation, JSON)
├── RBAC (internal/auth/rbac.go — server-side enforcement)
└── DB Layer (internal/db/ — SQL, transactions, migrations)
└── SQLite (data/forum.db or /data/forum.db in Docker)
Layer rules:
internal/db/— nonet/httporencoding/jsonimportsinternal/handlers/— no new inline SQL (useinternal/db/functions)internal/auth/— centralised RBAC, never bypassed
For detailed architecture documentation see architecture.md.
- Go 1.24 or later
make- GCC / CGO toolchain (required for
go-sqlite3) - Docker and Docker Compose (optional — backend only)
git clone <repository-url>
cd forum-moderation
# Download Go module dependencies
make deps # runs: go mod tidy
# (Optional) create a .env file for local configuration
cp .env.example .envCopy .env.example to .env and adjust values. All variables have safe
defaults; none are required for local development.
| Variable | Default | Required | Description |
|---|---|---|---|
APP_ENV |
production |
No | development enables DEBUG logs. Any other value → production (INFO/WARN/ERROR only) |
PORT |
8080 |
No | TCP port the backend HTTP server listens on |
TZ |
UTC |
No | IANA timezone (e.g. Europe/Athens). Invalid values fall back to UTC |
| Variable | Default | Required | Description |
|---|---|---|---|
DB_PATH |
/data/forum.db (Docker) or ./data/forum.db (local) |
No | Path to the SQLite database file. Local default is auto-detected from the presence of ./data/ |
| Variable | Default | Required | Description |
|---|---|---|---|
COOKIE_SECURE |
false |
No | Set true to add the Secure flag to session cookies. Must be true in production (HTTPS) |
FRONTEND_URL |
http://localhost:3000 |
No | Public URL of the frontend — used in password-reset email links and OAuth redirect URIs |
FRONTEND_ORIGIN |
(FRONTEND_URL value) | No | CORS allowed origin. Defaults to FRONTEND_URL when unset |
| Variable | Default | Description |
|---|---|---|
GOOGLE_CLIENT_ID |
— | Google OAuth App client ID |
GOOGLE_CLIENT_SECRET |
— | Google OAuth App client secret |
GOOGLE_REDIRECT_URL |
— | Google OAuth callback (e.g. http://localhost:8080/api/v1/auth/google/callback) |
GITHUB_CLIENT_ID |
— | GitHub OAuth App client ID |
GITHUB_CLIENT_SECRET |
— | GitHub OAuth App client secret |
GITHUB_REDIRECT_URL |
— | GitHub OAuth callback (e.g. http://localhost:8080/api/v1/auth/github/callback) |
| Variable | Default | Description |
|---|---|---|
SMTP_HOST |
— | SMTP server hostname |
SMTP_PORT |
587 |
SMTP port (465 for implicit TLS, 587 for STARTTLS) |
SMTP_USER |
— | SMTP username / sender address |
SMTP_PASS |
— | SMTP password |
SMTP_FROM |
(SMTP_USER) | From address in outgoing emails |
At startup the backend logs its resolved configuration (no secrets are logged):
[INFO ] starting env=production port=8080 db_path=/data/forum.db tz=UTC log_level=info
[INFO ] database ready path=/data/forum.db
[INFO ] server listening addr=http://localhost:8080
The project uses two separate servers that must both be running:
| Server | Port | Command |
|---|---|---|
| Backend API | 8080 | make run-backend |
| Frontend UI | 3000 | make run-frontend |
make run-all # starts backend (8080) + frontend (3000)
# frontend auto-opens http://localhost:3000 in the browserStop both servers:
make stop-allAPP_ENV=development
PORT=8080
TZ=UTC
DB_PATH=./data/forum.db
COOKIE_SECURE=false
FRONTEND_URL=http://localhost:3000make docker-build # build the backend image
make docker-run # run the backend container (detached)
make docker-stop # stop and remove the containerIn production the frontend still needs to run separately (it is a Go binary, not served by the same container as the backend).
Production environment variables:
APP_ENV=production
PORT=8080
TZ=UTC
DB_PATH=/data/forum.db
COOKIE_SECURE=true
FRONTEND_URL=https://forum.example.com
FRONTEND_ORIGIN=https://forum.example.com| Mode | Levels logged | Format |
|---|---|---|
development |
DEBUG, INFO, WARN, ERROR | [LEVEL] message key=value … to stderr |
production |
INFO, WARN, ERROR | [LEVEL] message key=value … to stderr |
make test # go test ./... (all packages)
make vet # go vet ./...
make fmt # gofmt check
make ci # full gate: build → fmt → vet → policy → test- Integration tests (
internal/tests/) — 31 test files exercising every API endpoint throughhttptestwith an in-memory SQLite database. - Unit tests — co-located with source files (
_test.go), covering handlers, DB helpers, RBAC, and image workflow logic. - Config tests (
internal/config/) — default values, env overrides, invalid port, timezone fallback, APP_ENV validation. - Logger tests (
internal/logger/) — level suppression in production, field formatting, output format.
All tests use an in-memory SQLite database; no external services are required.
# Remove the database file to start fresh (the schema is re-applied on next startup)
rm -f data/forum.db- Engine: SQLite (single file, no server required)
- Driver:
github.com/mattn/go-sqlite3(CGO) - Location:
./data/forum.db(local) or/data/forum.db(Docker /DB_PATH) - Initialization: the schema is embedded in the binary (
internal/db/forum_schema.sql). On first run the file is created and the schema applied automatically. - Migrations: idempotent
ALTER TABLE/CREATE TABLE IF NOT EXISTSstatements run at every startup viadb.InitDB— safe to run against existing databases. - Backup: copy the SQLite file while the application is stopped, or use
.backupvia the SQLite CLI. - Pragmas: WAL journal mode, foreign keys enabled, 5 s busy timeout, NORMAL synchronous mode.
| Table | Purpose |
|---|---|
users |
Accounts, roles, avatar, bio |
oauth_users |
OAuth provider links per user |
sessions |
Active session tokens |
posts |
Forum threads (draft / published / archived) |
comments |
Replies to posts |
categories |
Post categories |
post_categories |
Post ↔ category many-to-many |
reactions |
Like / dislike on posts and comments |
notifications |
In-app notifications |
moderator_requests |
User → admin role-upgrade requests |
reports |
Moderator → admin post reports |
post_edits |
Post edit history |
comment_edits |
Comment edit history |
password_resets |
Password-reset tokens |
POST /api/v1/users/register— create account (bcrypt password hash stored)POST /api/v1/users/login— validates credentials, creates session, setsHttpOnly SameSite=LaxcookiePOST /api/v1/users/logout— invalidates session
Sessions are UUID tokens stored server-side. The cookie never contains user data.
POST /api/v1/users/forgot-password— generates a time-limited token and emails a reset linkPOST /api/v1/users/reset-password— validates token and updates password
When SMTP_HOST is not set (development), the reset link is printed to stderr
instead of sent by email.
- Redirect user to
GET /api/v1/auth/{google|github} - Provider redirects back to
GET /api/v1/auth/{google|github}/callback - Backend creates or links the account and sets a session cookie
GitHub OAuth requires the user's primary email to be verified and public (or accessible via the GitHub emails API).
HttpOnly— JavaScript cannot read the session cookieSameSite=Lax— protects against cross-site request forgerySecure— set only whenCOOKIE_SECURE=true(enable in production with HTTPS)
- A logged-in user submits a moderator-role request via the UI (or
POST /api/v1/moderator-request). - Admins review open requests in the Admin Panel.
- Admin approves or rejects. The user is notified and their role changes accordingly.
- A moderator selects "Report" on a post.
- A report is created (
POST /api/v1/reports) with a reason. - Admins see all open reports in the Admin Panel.
- Admin replies to or resolves the report.
- View and search all users
- Change any user's role (except last-admin demotion or self-demotion)
- Activate / deactivate accounts
- Manage categories (create, rename, delete)
- Review and action moderator requests
- Review, reply to, and resolve reports
All endpoints are versioned under /api/v1. The backend returns JSON with a
consistent envelope:
{ "data": { ... } }
{ "error": { "code": "NOT_FOUND", "message": "..." } }| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/health |
— | Health check |
POST |
/api/v1/users/register |
— | Register |
POST |
/api/v1/users/login |
— | Login |
POST |
/api/v1/users/logout |
✓ | Logout |
GET |
/api/v1/users/me |
✓ | Current user |
PATCH |
/api/v1/users/me/profile |
✓ | Update bio |
POST |
/api/v1/users/me/avatar |
✓ | Upload avatar |
GET |
/api/v1/users/{id}/profile |
optional | Public profile |
GET |
/api/v1/posts |
optional | List published posts |
POST |
/api/v1/posts |
✓ | Create post |
GET |
/api/v1/posts/{id} |
optional | Get post |
PATCH |
/api/v1/posts/{id} |
✓ | Edit post |
DELETE |
/api/v1/posts/{id} |
✓ | Delete post |
GET/POST |
/api/v1/posts/draft |
✓ | List / create draft |
GET |
/api/v1/categories |
— | List categories |
GET |
/api/v1/notifications |
✓ | List notifications |
PATCH |
/api/v1/notifications/{id} |
✓ | Mark notification read |
DELETE |
/api/v1/notifications/{id} |
✓ | Delete notification |
POST |
/api/v1/moderator-request |
✓ | Request moderator role |
POST |
/api/v1/reports |
✓ (mod+) | Report a post |
GET |
/api/v1/admin/users |
✓ (admin) | List all users |
PATCH |
/api/v1/admin/users/{id}/role |
✓ (admin) | Change role |
GET |
/api/v1/admin/reports |
✓ (admin) | List reports |
For the full route list see internal/router/router.go.
# Build
make docker-build
# Run with a persistent database volume
docker run -d \
-p 8080:8080 \
-v forum_data:/data \
-e APP_ENV=production \
-e DB_PATH=/data/forum.db \
-e COOKIE_SECURE=true \
-e FRONTEND_URL=https://forum.example.com \
forum-backend
# Using the Makefile shorthand (env vars in Makefile docker-run target):
make docker-run- Database: mount
/dataas a Docker volume soforum.dbsurvives container restarts. - Uploaded images: the frontend binary serves uploads from
web/static/uploads/. If you run the frontend in a container or behind a proxy, ensure this path is writable and persistent.
Route all traffic to the frontend (:3000); the frontend proxies /api/
requests to the backend (:8080) automatically. You do not need to expose
the backend port publicly.
-
COOKIE_SECURE=true(HTTPS required) -
APP_ENV=production -
DB_PATHpoints to a persistent volume -
FRONTEND_URLandFRONTEND_ORIGINset to the public domain - OAuth redirect URLs registered with the provider to match the public domain
-
SMTP_*variables set if email is needed -
/datavolume mounted before first run
Port already in use:
# Linux — free the port
fuser -k 8080/tcp
# Or change the port:
PORT=9090 make run-backendCGO not available (SQLite build error):
# CGO is required for go-sqlite3. Ensure GCC is installed:
sudo apt install gcc
# Then build normally — CGO_ENABLED=1 is the default
Database file not found / permission error:
# Ensure the data directory exists before the first local run:
mkdir -p dataOAuth login fails:
Verify that GOOGLE_REDIRECT_URL / GITHUB_REDIRECT_URL exactly match
the callback URL registered in the OAuth app settings, including scheme and port.
| Symptom | Likely cause | Fix |
|---|---|---|
listen tcp :8080: bind: address already in use |
Another process on port 8080 | fuser -k 8080/tcp or set PORT=... |
cgo: C compiler "gcc" not found |
GCC missing | sudo apt install gcc |
no such file or directory: ./data/forum.db |
data/ dir missing |
mkdir -p data |
| OAuth redirect mismatch | Redirect URL not registered | Register callback URL in OAuth app |
| Password reset link logged to stderr | SMTP_HOST not set |
Expected in dev; set SMTP vars in prod |
| DEBUG logs in production | APP_ENV not set |
Set APP_ENV=production |
| Document | Description |
|---|---|
architecture.md |
Layered architecture, layer rules, component map |
AGENTS.md |
Conventions for AI coding agents working on this project |
docs/PRD.md |
Product requirements — what the product does and why |
docs/SDS.md |
Software design spec — data model, API surface, layering rules |
docs/roadmap.md |
Issue-driven roadmap, grouped by theme |
.env.example |
Annotated environment variable reference |
- Chris Baikas (chbaikas)
- Alex Smyroglou (asmyrogl)
This project is licensed under the MIT License.