Deep Dive into the Backend and JWT Authentication(blog-2)

#. Introduction

In Blog 1 — my full-stack project that uses Go, React, PostgreSQL, Docker, and JWT to provide real-time project monitoring. We discussed the overall system architecture, deployment strategy, and why JWT is important.

In this second blog, we’ll zoom into the backend. By the end of this guide, you’ll understand:

  • How the backend entry point (main.go) works.

  • Why Go’s concurrency model makes background monitoring possible.

  • How Gorilla Mux and middleware organize the API.

  • Exactly how JWT authentication is implemented and why it’s secure.

  • How the database layer (PostgreSQL) integrates with Go.

github repo:- https://github.com/sidharth-chauhan/Trackly

#. Workflow

    • main.go boots the service: loads environment, connects DB, starts the background monitor goroutine, registers middleware and routes, and starts the HTTP server.

      • The internal/db package uses GORM to connect and migrate Postgres models.

      • Models are defined in internal/models.

      • The middleware package provides CORS handling.

      • The project package implements all project-related handlers and the background monitor.

      • The user package implements registration, login, JWT issuance and verification, and a helper to get the authenticated user ID from context.

      • The utils package handles sending HTML email alerts.#. Backend Entry Point — main.go

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"

    "Trackly/internal/db"
    "Trackly/internal/middleware"
    "Trackly/internal/project"
    "Trackly/internal/user"

    "github.com/gorilla/mux"
    "github.com/joho/godotenv"
)

func main() {
    // Load .env only if running locally
    if os.Getenv("RENDER") == "" {
        if err := godotenv.Load(); err != nil {
            log.Println("⚠️ No .env file found, using system environment variables")
        }
    }

    // Check if critical env vars exist
    if os.Getenv("DB_URL") == "" {
        log.Fatal("❌ DB_URL environment variable not set")
    }

    db.ConnectDB()

    //BACKGROUND MONITOR
    go project.MonitorProjects()

    r := mux.NewRouter()

    //  middleware
    r.Use(middleware.CORSMiddleware)

    // Healthcheck
    r.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        fmt.Fprint(w, "OpenAnalytics backend is running 🚀")
    }).Methods("GET")

    // Public routes
    r.HandleFunc("/user/register", user.Register).Methods("POST", "OPTIONS")
    r.HandleFunc("/user/login", user.HandleLogin).Methods("POST", "OPTIONS")

    // Project routes (JWT protected)
    projectRouter := r.PathPrefix("/project").Subrouter()
    projectRouter.Use(user.JwtMiddleware)
    projectRouter.HandleFunc("", project.CreateProject).Methods("POST", "OPTIONS")
    projectRouter.HandleFunc("", project.GetProjects).Methods("GET", "OPTIONS")
    projectRouter.HandleFunc("/dashboard", project.GetDashboard).Methods("GET", "OPTIONS")
    projectRouter.HandleFunc("/status", project.CheckProjectStatus).Methods("GET", "OPTIONS")
    projectRouter.HandleFunc("/{id}", project.UpdateProject).Methods("PUT", "OPTIONS")
    projectRouter.HandleFunc("/{id}", project.DeleteProject).Methods("DELETE", "OPTIONS")
    projectRouter.HandleFunc("/{id}", project.GetProjectByID).Methods("GET", "OPTIONS")

    // Use PORT from Render or default to 8080
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    fmt.Printf("🚀 Trackly backend is running on port %s\n", port)
    log.Fatal(http.ListenAndServe(":"+port, r))
}

a). The boot sequence (main.go)

This is the exact sequence I use to bring Trackly up:

  1. Load environment variables locally (godotenv) but do not load in production. Production secrets are set in the host (Render).

  2. Fail fast if critical config like DB_URL is missing.

  3. Connect to the database (db.ConnectDB()).

  4. Start background monitoring in a goroutine: go project.MonitorProjects().

  5. Create a mux.Router, register global middleware (CORS, request-id, logging).

  6. Register public routes (/user/register, /user/login) and protected subroutes under /project that use user.JwtMiddleware.

  7. Start the HTTP server using the PORT environment value (Render injects PORT).

b). Environment & configuration

  • Keep secrets out of source code. Use JWT_SECRET, DB_URL, and other secrets as environment variables.

  • For local development, I use .env with godotenv (never commit .env).

  • In production, use platform secret management (Render ,Github).

  • Use a single DB connection string in production: postgres://user:pass@host:5432/dbname?sslmode=require (or disable for local dev).

  • Validate essential environment variables at startup and fail early if missing.

c). Database connection pattern (internal/db)

centralize DB setup in internal/db. Key points:

    • DB is a package-level variable that other packages (e.g., internal/user, internal/project) can import and use.

      • AutoMigrate is convenient during development and early production. For mature systems, use a dedicated migration tool (e.g., golang-migrate) for versioned, auditable migrations.

      • DB uses default GORM pooling. If you want to tune connection pooling, get the underlying *sql.DB and set SetMaxOpenConns, SetMaxIdleConns, and SetConnMaxLifetime.

package db

import (
    "fmt"
    "log"
    "os"

    "gorm.io/driver/postgres"
    "gorm.io/gorm"

    "Trackly/internal/models"
)

var DB *gorm.DB

func ConnectDB() {
    // Get database URL from environment variable
    dsn := os.Getenv("DB_URL")
    if dsn == "" {
        log.Fatal("❌ DB_URL environment variable not set")
    }

    // Connect to PostgreSQL
    var err error
    DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal("❌ Failed to connect to database:", err)
    }

    // Run database migrations
    err = DB.AutoMigrate(&models.User{}, &models.Project{})
    if err != nil {
        log.Fatal("❌ AutoMigrate failed:", err)
    }

    fmt.Println("✅ Connected to PostgreSQL")
}

d). Models (internal/models)

package models

import "time"

type User struct {
    ID        int    `gorm:"primaryKey"`
    Email     string `gorm:"unique;not null"`
    Password  string `gorm:"not null"`
    CreatedAt time.Time
}
  • Email: Must be unique and non-null.

  • Password: Stored as a hashed string for security.

  • No UpdatedAt because user profile rarely changes compared to projects.

package models

import "time"

type Project struct {
    ID          int       `gorm:"primaryKey" json:"id"`
    Name        string    `json:"name"`
    Description string    `json:"description"`
    Link        string    `json:"link"`
    UserID      uint      `json:"user_id"`
    CreatedAt   time.Time `json:"created_at"`
    UpdatedAt   time.Time `json:"updated_at"`
}
  • gorm:"primaryKey": Marks ID as the primary key in the table.

  • json:"name": Ensures JSON serialization aligns with API requests/responses.

  • UserID: Maintains user-to-project ownership (each project belongs to a specific user).

  • CreatedAt and UpdatedAt: Automatically managed timestamps.

e). Middleware (middleware)

CORS (Cross-Origin Resource Sharing) middleware ensures the React frontend (GitHub Pages) can communicate with the Go backend (Render).

package middleware

import (
    "fmt"
    "net/http"
)

func CORSMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // ✅ Debug log
        fmt.Println("✅ CORS middleware called:", r.Method, r.URL.Path, "Origin:", r.Header.Get("Origin"))

        // Allow GitHub Pages frontend
        w.Header().Set("Access-Control-Allow-Origin", "https://sidharth-chauhan.github.io")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        w.Header().Set("Access-Control-Allow-Credentials", "true")

        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusNoContent)
            return
        }

        next.ServeHTTP(w, r)
    })
}
  • Frontend hosted on GitHub Pages must talk to the backend hosted on Render.

  • Without CORS, browsers will block cross-origin API calls.

  • Here, only requests from https://sidharth-chauhan.github.io are allowed — this ensures security by not opening APIs to everyone.

#. Project APIs

This is where most of the backend functionality lives — the CRUD operations for projects.

Create Project

func CreateProject(w http.ResponseWriter, r *http.Request) {
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Failed to read request body", http.StatusBadRequest)
        return
    }

    var input struct {
        Name        string `json:"name"`
        Description string `json:"description"`
        Link        string `json:"link"`
    }
    err = json.Unmarshal(body, &input)
    if err != nil || input.Name == "" {
        http.Error(w, "Invalid input data", http.StatusBadRequest)
        return
    }

    userIDStr, ok := user.GetUserIDFromContext(r)
    if !ok || userIDStr == "" {
        http.Error(w, "User ID not found in context", http.StatusUnauthorized)
        return
    }

    userIDUint64, err := strconv.ParseUint(userIDStr, 10, 32)
    if err != nil {
        http.Error(w, "Invalid user ID", http.StatusInternalServerError)
        return
    }
    userID := uint(userIDUint64)

    project := models.Project{
        Name:        input.Name,
        Description: input.Description,
        Link:        input.Link,
        UserID:      userID,
    }

    result := db.DB.Create(&project)
    if result.Error != nil {
        http.Error(w, "Failed to create project", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(project)
}
  • Read & Validate Input: Ensures project has a Name.

  • Authorization: Extracts user_id from JWT (only logged-in users can create projects).

  • Save to DB: Persists the project tied to the authenticated user.

  • Response: Returns the created project in JSON.

Get All Projects

func GetProjects(w http.ResponseWriter, r *http.Request) {
    userIDStr, ok := user.GetUserIDFromContext(r)
    if !ok || userIDStr == "" {
        http.Error(w, "User ID not found in context", http.StatusUnauthorized)
        return
    }

    parsedID, err := strconv.ParseUint(userIDStr, 10, 32)
    if err != nil {
        http.Error(w, "Invalid user ID", http.StatusInternalServerError)
        return
    }
    userID := uint(parsedID)

    var projects []models.Project
    result := db.DB.Where("user_id = ?", userID).Find(&projects)
    if result.Error != nil {
        http.Error(w, "Failed to fetch projects", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(projects)
}
  • Returns all projects owned by the logged-in user.

  • This ensures users only see their own data — a critical security principle called multi-tenancy isolation.

Update & Delete Project

Both UpdateProject and DeleteProject follow a similar flow:

  1. Validate project ID from URL.

  2. Extract user ID from JWT context.

  3. Ensure the project belongs to the user.

  4. Perform the update/delete action.

This pattern prevents unauthorized access (e.g., one user modifying another user’s project).

Dashboard & Project Status

Trackly goes beyond CRUD by providing:

  • Dashboard: Summarizes total projects, latest project, and last updated timestamp.

  • Status Checker: Periodically checks each project’s Link to determine if it’s UP or DOWN.

This is where the app behaves more like a real monitoring tool rather than just a CRUD system.

Background Monitoring with Goroutines

One of Go’s strengths is concurrency. Trackly uses a background goroutine (MonitorProjects) to:

  • Loop through all users and their projects.

  • Send an HTTP request to each project link.

  • If the project is down, send an email alert using the utils.SendEmail function.

  • Run this check every 10 minutes.

This ensures that project owners are notified in real time if their deployed projects go offline.

#. Conclusion

  • Database connection setup with GORM.

  • User and Project models with constraints.

  • CORS middleware for frontend-backend communication.

  • Full CRUD APIs for projects with authentication checks.

  • Dashboard and Monitoring features powered by goroutines and email alerts.

This layer forms the core of the Trackly backend. In the next blog, I will focus on User authentication with JWT, why JWT is chosen over sessions, and how it integrates with the project flow.

0
Subscribe to my newsletter

Read articles from Sidharth chauhan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sidharth chauhan
Sidharth chauhan

🌟 Hello! I'm Sidharth Chauhan, a passionate DevOps Engineer dedicated to automating and optimizing processes to enhance software development and deployment. With expertise in Docker, Kubernetes, CI/CD, AWS, and more, I thrive in environments that leverage cutting-edge technologies and tools.I love sharing my knowledge and experiences through blogging, and I'm always eager to connect with like-minded professionals. Whether you're looking to collaborate on a project, need help with DevOps, or just want to chat about the latest tech trends, feel free to reach out!🔗 Connect with Me: