Skip to main content

Project Structure

oforum/
├── main.go                       # Entry point, routes, template setup
├── seed.go                       # Seed data generator
├── internal/
│   ├── auth/
│   │   ├── auth.go              # Password hashing, signup/login
│   │   └── middleware.go        # Session loading, auth guards
│   ├── models/
│   │   └── models.go            # All structs (User, Post, Comment, etc.)
│   ├── db/
│   │   ├── db.go                # Connection pool management
│   │   ├── users.go             # User queries (CRUD, profile, reputation)
│   │   ├── posts.go             # Post queries (ranked, new, search)
│   │   ├── comments.go          # Comment trees, add/delete
│   │   ├── upvotes.go           # Upvote toggle logic
│   │   ├── roles.go             # Role CRUD, user-role assignments
│   │   ├── tags.go              # Tag CRUD, post-tag associations
│   │   ├── admin.go             # Admin listings, bans, settings
│   │   ├── sessions.go          # Session create/get/delete
│   │   └── delete.go            # Post and comment deletion
│   ├── handlers/
│   │   ├── helpers.go           # Template helpers (TimeAgo, FormatContent)
│   │   ├── home.go              # Home page (top/new), pagination, search
│   │   ├── posts.go             # Post detail, submit, upvote
│   │   ├── comments.go          # Add comment, upvote, delete
│   │   ├── users.go             # Profile view/edit, leaderboard
│   │   ├── auth.go              # Login, signup, logout
│   │   ├── admin.go             # Admin dashboard, users, roles, tags
│   │   └── static.go            # Static pages (guidelines, FAQ, etc.)
│   └── middleware/
│       └── minify.go            # HTML minification
├── templates/                    # Go html/template files
│   ├── header.html              # Shared header with nav
│   ├── footer.html              # Shared footer with PJAX
│   ├── home.html                # Homepage post listing
│   ├── post.html                # Post detail with comment tree
│   ├── user.html                # User profile page
│   ├── submit.html              # Post submission form
│   └── admin*.html              # Admin panel templates
├── migrations/                   # Numbered SQL migrations
│   ├── 001_create_users.up.sql
│   ├── 001_create_users.down.sql
│   ├── 003_create_posts.up.sql
│   └── ...
└── cmd/
    └── seed/main.go             # Standalone seed command

Component Overview

main.go

The entry point coordinates everything:
//go:embed migrations/*.sql
var migrationsFS embed.FS

//go:embed templates/*.html
var templatesFS embed.FS

internal/models

All data structures in one file for easy reference:
models.go:5-17
type User struct {
    ID           int        `json:"id"`
    Username     string     `json:"username"`
    PasswordHash string     `json:"-"`
    DisplayName  string     `json:"display_name"`
    Bio          string     `json:"bio"`
    IsAdmin      bool       `json:"is_admin"`
    BannedUntil  *time.Time `json:"banned_until"`
    CreatedAt    time.Time  `json:"created_at"`
    Roles        []Role     `json:"-"` // loaded separately
}

internal/auth

Handles password hashing and session management:
auth/auth.go:12-20
func HashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword(
        []byte(password), 
        bcrypt.DefaultCost,
    )
    return string(bytes), err
}

internal/db

Each file handles one domain with plain SQL:
func CreatePost(ctx context.Context, userID int, title, body string, url *string) (*models.Post, error) {
    slug, err := uniqueSlug(ctx, Slugify(title))
    if err != nil {
        return nil, err
    }
    
    post := &models.Post{}
    err = Pool.QueryRow(ctx,
        `INSERT INTO posts (user_id, title, body, url, slug) 
         VALUES ($1, $2, $3, $4, $5)
         RETURNING id, user_id, title, slug, body, url, created_at`,
        userID, title, body, url, slug,
    ).Scan(&post.ID, &post.UserID, &post.Title, &post.Slug, &post.Body, &post.URL, &post.CreatedAt)
    
    return post, err
}

internal/handlers

Thin request handlers that delegate to db/ packages:
handlers/posts.go (simplified)
func ViewPost(c *gin.Context) {
    slug := c.Param("slug")
    
    // Get data from database layer
    post, err := db.GetPostBySlug(c.Request.Context(), slug, auth.GetCurrentUserID(c))
    if err != nil {
        c.HTML(404, "error.html", baseData(c))
        return
    }
    
    comments, _ := db.GetCommentsByPost(c.Request.Context(), post.ID, auth.GetCurrentUserID(c))
    
    // Render template
    data := baseData(c)
    data["Post"] = post
    data["Comments"] = comments
    c.HTML(200, "post.html", data)
}

Request Flow

1

Request arrives

Gin router matches URL to handler function
2

LoadUser middleware runs

Checks session cookie, loads user into context if valid
3

CheckBan middleware runs

Shows ban page if user is banned
4

Handler executes

Calls appropriate db/ functions to fetch data
5

Database queries run

Plain SQL queries via pgx connection pool
6

Template renders

Go’s html/template generates HTML with data
7

Response sent

HTML (or JSON for AJAX) returned to client

Dependency Tree

oForum has minimal external dependencies:
oforum
├── github.com/gin-gonic/gin          # HTTP router and middleware
├── github.com/jackc/pgx/v5            # PostgreSQL driver and connection pool
├── golang.org/x/crypto/bcrypt         # Password hashing
├── github.com/golang-migrate/migrate  # Database migrations
├── github.com/joho/godotenv            # .env file loading
└── github.com/gin-contrib/gzip        # Response compression
No ORM, no frontend framework, no complex abstractions. Just the essentials.

Embedded Resources

Migrations and templates are embedded into the binary at compile time:
main.go:29-33
//go:embed migrations/*.sql
var migrationsFS embed.FS

//go:embed templates/*.html
var templatesFS embed.FS
This means:
  • ✅ Single binary contains everything
  • ✅ No need to distribute separate files
  • ✅ Migrations run automatically on startup
  • ✅ Templates are always in sync with code

Adding New Features

Adding a New Page

1

Create Handler

Add function in appropriate file under internal/handlers/:
handlers/custom.go
func MyNewPage(c *gin.Context) {
    data := baseData(c)
    c.HTML(200, "my_new_page.html", data)
}
2

Create Template

Add templates/my_new_page.html:
{{ template "header" . }}
<h1>My New Page</h1>
{{ template "footer" . }}
3

Register Route

Add to main.go:
r.GET("/my-page", handlers.MyNewPage)

Adding a Database Field

1

Create Migration

Add migrations/013_add_field.up.sql:
ALTER TABLE users ADD COLUMN bio TEXT DEFAULT '';
2

Update Model

Edit internal/models/models.go:
type User struct {
    // ... existing fields
    Bio string `json:"bio"`
}
3

Update Queries

Add to internal/db/users.go queries
4

Update Templates

Display field in relevant templates

Next Steps

Database Layer

Learn query patterns and migrations

Templates

Work with Go html/template

Build docs developers (and LLMs) love