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:
Load environment variables locally (
godotenv
) but do not load in production. Production secrets are set in the host (Render).Fail fast if critical config like
DB_URL
is missing.Connect to the database (
db.ConnectDB()
).Start background monitoring in a goroutine:
go project.MonitorProjects()
.Create a
mux.Router
, register global middleware (CORS, request-id, logging).Register public routes (
/user/register
,/user/login
) and protected subroutes under/project
that useuser.JwtMiddleware
.Start the HTTP server using the
PORT
environment value (Render injectsPORT
).
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
withgodotenv
(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
(ordisable
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 setSetMaxOpenConns
,SetMaxIdleConns
, andSetConnMaxLifetime
.
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"
: MarksID
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
andUpdatedAt
: 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:
Validate project ID from URL.
Extract user ID from JWT context.
Ensure the project belongs to the user.
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.
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: