Go Clean Architecture (Handler → Service → Repository) + Next.js 14 Standalone
A production-grade, full-stack task management system built with battle-tested patterns,
enterprise security practices, and developer-first DX.
- Overview
- Key Architectural Decisions
- Bonus Features Implemented
- Tech Stack
- Project Structure
- Environment Configuration
- How to Run (One-Command Setup)
- API Endpoints
- Testing
- Contributing
- License
This is an enterprise-grade task management application designed with scalability, security, and maintainability as first-class concerns. The backend follows Go Clean Architecture with a strict layer separation pattern, while the frontend leverages Next.js 14 Standalone for optimal rendering strategies and deployment efficiency.
The application enables users to:
- Register and authenticate securely via JWT stored in HTTP-Only cookies
- Create, read, update, and delete tasks with rich metadata (priority, status, due dates, categories)
- Filter, sort, and paginate through tasks with sub-second response times
- View real-time activity logs (audit trail) for every task mutation
- Experience instant UI updates via optimistic updates with TanStack Query v5
- Receive real-time changes pushed from the server via Server-Sent Events (SSE)
┌─────────────────────────────────────────────────────┐
│ HTTP Layer │
│ (Handlers / Middleware) │
│ - Request parsing, validation, response writing │
│ - JWT extraction via middleware │
│ - No business logic │
├─────────────────────────────────────────────────────┤
│ Service Layer │
│ (Business Logic Orchestration) │
│ - Transaction management │
│ - Authorization checks │
│ - Audit logging (activity trail) │
│ - SSE event broadcasting │
├─────────────────────────────────────────────────────┤
│ Repository Layer │
│ (Data Access / Persistence) │
│ - PostgreSQL queries with prepared statements │
│ - Pagination, filtering, sorting │
│ - Index-optimized access patterns │
└─────────────────────────────────────────────────────┘
Each layer communicates only with the layer directly below it. Handlers never touch the database; repositories never handle HTTP concerns. This separation yields:
- Testability: Each layer can be unit-tested in isolation with mocks
- Maintainability: Changing the database driver or HTTP framework requires zero changes outside the affected layer
- Security: Authorization logic lives in the service layer, ensuring it cannot be bypassed regardless of HTTP implementation
Authentication is implemented using signed JWT tokens stored exclusively in HTTP-Only, Secure, SameSite=Strict cookies. This approach eliminates the risk of XSS-based token theft — JavaScript in the browser never has programmatic access to the authentication token.
| Property | Value |
|---|---|
| Storage | HTTP-Only Cookie (not localStorage) |
| XSS Protection | ✅ Immune — JS cannot read the cookie |
| CSRF Protection | ✅ SameSite=Strict + Origin validation |
| Token Signature | HMAC-SHA256 via golang-jwt/jwt/v5 |
| Password Hashing | bcrypt via golang.org/x/crypto |
The database schema is intentionally indexed to support the most common query patterns:
- Composite index on
(user_id, created_at DESC)— powers the main task listing with pagination - Partial index on
(status)for status-based filtering queries - GIN index on
(to_tsvector('english', title || ' ' || description))— enables full-text search without external search infrastructure - Unique index on
(id, user_id)— used by the repository for ownership-guaranteed lookups
The frontend leverages TanStack Query v5 (React Query) to implement optimistic updates. When a user performs a mutation (create, update, delete a task), the UI immediately reflects the expected result before the server responds. If the server confirms the mutation, the cache is updated seamlessly. If the server rejects it, the UI automatically rolls back to the previous state with zero user-perceived latency.
User Action → Optimistic Cache Update → UI Renders Instantly
↓
Server Mutation API Call
↓
✅ Success → Cache Sync ❌ Failure → Rollback
Instead of polling or WebSocket overhead, the backend pushes real-time updates to connected clients using Server-Sent Events. When a task is created, updated, or deleted by one user, all other authenticated clients receive the change instantly.
- Lightweight, unidirectional protocol over standard HTTP
- Automatic reconnection handled by the browser's native
EventSourceAPI - Scoped to authenticated user sessions — users only receive events for tasks they own
Every state mutation to a task is recorded in an immutable activity log. The activity_logs table stores:
- Actor — the user who performed the action
- Action — the type of mutation (created, updated status, deleted, etc.)
- Entity — the affected task ID
- Diff / Snapshot — the before/after state of the changed fields
- Timestamp — when the mutation occurred (server time, monotonic)
This provides a complete, forensically sound history of every change in the system.
The build pipeline uses multi-stage Docker builds with distroless runtime images to minimize attack surface and deployment size:
Backend (Go):
| Stage | Image | Size |
|---|---|---|
| Build | golang:1.22-alpine |
~900 MB |
| Runtime | alpine:3.19 (distroless, non-root user) |
~18 MB ✅ |
- Statically linked binary with
CGO_ENABLED=0 - Stripped debug symbols (
-ldflags="-w -s") - Runs as
appuser(UID 1001) — not root
Frontend (Next.js):
| Stage | Image | Size |
|---|---|---|
| Build | node:20-alpine |
~1.2 GB |
| Runtime | node:20-alpine (standalone output) |
~150 MB ✅ |
- Uses Next.js
output: 'standalone'— copies only the minimal required files - No development dependencies included in the final image
| Technology | Purpose |
|---|---|
| Go 1.22 | Primary language — compiled, statically typed, high performance |
| Chi Router v5 | Lightweight, idiomatic HTTP router with middleware chaining |
| pgx / lib/pq | PostgreSQL driver with prepared statement support |
| golang-jwt v5 | JWT creation and verification |
| go-playground/validator | Struct-level request validation |
| golang-migrate | Database schema migrations |
| google/uuid | UUID v4 generation for task and user IDs |
| Technology | Purpose |
|---|---|
| Next.js 14 (App Router) | React framework with SSR, SSG, and standalone output |
| TypeScript | Type-safe development |
| TanStack Query v5 | Server state management, caching, optimistic updates |
| Tailwind CSS | Utility-first styling |
| shadcn/ui | Accessible, composable component primitives |
| Technology | Purpose |
|---|---|
| Docker | Containerization for both services |
| Docker Compose | Multi-service orchestration |
| PostgreSQL 16 | Primary data store with full-text search support |
task-management-app/
├── backend/
│ ├── cmd/
│ │ └── api/
│ │ └── main.go # Application entrypoint
│ ├── internal/
│ │ ├── handlers/
│ │ │ ├── auth_handler.go # Authentication endpoints
│ │ │ ├── task_handler.go # CRUD + activity endpoints
│ │ │ └── helpers.go # Response helpers (JSON, errors)
│ │ ├── middleware/
│ │ │ └── auth.go # JWT extraction + context injection
│ │ ├── models/
│ │ │ ├── auth.go # Auth request/response DTOs
│ │ │ ├── task.go # Task domain model + DTOs
│ │ │ └── api.go # Generic API response envelope
│ │ ├── repository/
│ │ │ ├── user_repository.go # User persistence
│ │ │ ├── task_repository.go # Task CRUD + search queries
│ │ │ └── activity_repository.go # Activity log persistence
│ │ └── service/
│ │ ├── auth_service.go # Registration, login, JWT issuance
│ │ ├── task_service.go # Business logic + authorization
│ │ └── sse_service.go # Server-Sent Events broadcasting
│ ├── migrations/
│ │ └── 000001_init.up.sql # Schema + indexes
│ ├── go.mod
│ ├── Dockerfile # Multi-stage distroless build
│ └── .dockerignore
├── frontend/
│ ├── src/
│ │ ├── app/ # Next.js App Router pages
│ │ ├── components/ # UI components
│ │ ├── hooks/ # Custom hooks (mutations, SSE)
│ │ └── lib/ # API client, utilities
│ ├── public/
│ ├── next.config.js # `output: 'standalone'`
│ ├── Dockerfile # Standalone production build
│ ├── package.json
│ └── tsconfig.json
├── docker-compose.yml # Production orchestration
├── .env.example # Environment variable reference
└── README.md # This file
Copy .env.example to .env and configure the following variables:
| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | postgres://taskmanager:secretpassword@db:5432/taskmanager?sslmode=disable |
POSTGRES_USER |
Database username | taskmanager |
POSTGRES_PASSWORD |
Database password | secretpassword |
POSTGRES_DB |
Database name | taskmanager |
JWT_SECRET |
HMAC signing key for JWT tokens | Must be changed in production |
JWT_EXPIRATION |
Token TTL (Go duration format) | 24h |
API_PORT |
Backend server port | 8080 |
API_ALLOWED_ORIGINS |
CORS allowed origins | http://localhost:3000 |
NEXT_PUBLIC_API_URL |
Public-facing API URL (browser) | http://localhost:8080/api |
NEXT_SERVER_API_URL |
Internal API URL (server-side) | http://api:8080/api |
⚠️ Production Requirements: ChangeJWT_SECRETto a cryptographically random string (minimum 256 bits). Use strong, unique credentials for the database. Enable TLS by terminating SSL at a reverse proxy (e.g., nginx, Caddy, or a cloud load balancer).
- Docker (v24+) and Docker Compose (v2.20+)
- Git
# 1. Clone the repository
git clone https://github.com/your-org/task-management-app.git
cd task-management-app
# 2. Configure environment (optional — defaults work for local dev)
cp .env.example .env
# 3. Build and launch everything with a single command
docker compose up --buildThis single command will:
- Build the Go backend binary using the multi-stage Dockerfile
- Build the Next.js frontend with
output: 'standalone' - Start a PostgreSQL 16 instance with health checks
- Run database migrations automatically (via the backend's startup process)
- Expose the API on
http://localhost:8080and the frontend onhttp://localhost:3000
# Health check — API
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/v1/auth/me
# Health check — Frontend
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000
# View logs
docker compose logs -fdocker compose down
# To also delete the database volume (⚠️ destroys all data)
docker compose down -vThe frontend is fully adapted for native Vercel deployment via the Next.js standalone output mode. Key configuration points:
| Concern | Implementation | File |
|---|---|---|
| Output mode | output: 'standalone' in next.config.js — produces a minimal self-contained build |
frontend/next.config.js |
| API routing | NEXT_PUBLIC_API_URL and NEXT_SERVER_API_URL control client vs server-side API base URLs |
.env / Vercel Environment Variables |
| Static asset handling | Public assets served from /public, bundled into standalone output |
frontend/public/ |
| Image optimization | Configured via next.config.js; uses default Vercel image optimizer when deployed |
frontend/next.config.js |
Note: For local Docker Compose development,
NEXT_SERVER_API_URL=http://api:8080/apiresolves via the internal Docker network. On Vercel, both variables point to the same public API URL because there is no internal Docker network.
When the frontend is hosted on Vercel and the backend on Render (or any separate domain), the browser's Same-Origin Policy and cookie scoping rules require explicit opt-in for cross-domain cookie delivery:
| Attribute | Required Value | Rationale |
|---|---|---|
SameSite |
None |
Allows the cookie to be sent with cross-origin requests. SameSite=Lax (the browser default in 2025) would block the cookie on the actual cross-origin fetch() call. |
Secure |
true |
Required by the Fetch Standard when SameSite=None is set. The backend endpoint must be served over HTTPS with a valid TLS certificate. |
Domain |
Not set | Leaving the domain attribute unspecified restricts the cookie to the issuing origin (the backend). The browser will still attach it to cross-origin fetch() requests because SameSite=None is in effect. |
HttpOnly |
true |
Prevents JavaScript from reading the cookie, eliminating XSS-based token theft. |
Backend cookie-setting code example (Go):
http.SetCookie(w, &http.Cookie{
Name: "auth_token",
Value: tokenString,
Path: "/",
HttpOnly: true,
Secure: true, // Required when frontend and backend are on different origins
SameSite: http.SameSiteNoneMode, // Allows cross-origin delivery
MaxAge: 86400, // 24 hours
})
⚠️ Important:http.SameSiteNoneModerequires Go 1.16+. The backend binary uses Go 1.22, so this value is fully supported. EnsureSecure: trueis only enabled in production — local development over HTTP should useSameSite=http.SameSiteStrictModewithSecure: false.
- CORS Origin: Set
API_ALLOWED_ORIGINSin the backend deployment (e.g., Render, Railway, Fly.io, or your own VPS) to match the exact Vercel frontend domain (e.g.,https://task-manager.vercel.appor your custom domain). The Go CORS middleware (backend/internal/middleware/cors.go) automatically merges this with the built-in defaults (http://localhost:3000andhttps://task-management-application-omega-lyart.vercel.app). - Cookie Domain: For cross-domain cookies (frontend on Vercel, backend on another host), ensure the backend sets
SameSite=None; Secureon the JWT cookie and the backend domain is served over HTTPS with a valid TLS certificate.
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST |
/api/v1/auth/register |
Create a new user account | ❌ |
POST |
/api/v1/auth/login |
Authenticate and receive JWT cookie | ❌ |
POST |
/api/v1/auth/logout |
Invalidate session (clear cookie) | ✅ |
GET |
/api/v1/auth/me |
Get current authenticated user | ✅ |
GET |
/api/v1/tasks |
List tasks (paginated, filterable, sortable) | ✅ |
POST |
/api/v1/tasks |
Create a new task | ✅ |
GET |
/api/v1/tasks/{id} |
Get a single task by ID | ✅ |
PUT |
/api/v1/tasks/{id} |
Update a task | ✅ |
DELETE |
/api/v1/tasks/{id} |
Delete a task | ✅ |
GET |
/api/v1/tasks/{id}/activity |
Get activity log for a task | ✅ |
GET |
/api/v1/events |
SSE stream for real-time updates | ✅ |
🔧 CORS Preflight Optimization
The server explicitly handles
OPTIONSpreflight requests at the middleware layer and returns HTTP 200 OK immediately — without entering the route execution pipeline. This behaviour eliminates unnecessary latency for cross-origin requests (the preflight round-trip completes in a single network hop) and ensures full compatibility with credentialed requests (cookies) by returning the exactAccess-Control-Allow-OriginandAccess-Control-Allow-Credentials: trueheaders. The preflight cache is set to3600seconds (Access-Control-Max-Age: 3600), so browsers only send oneOPTIONSrequest per origin per hour.Implementation details (
backend/internal/middleware/cors.go):
- The middleware reads
API_ALLOWED_ORIGINSfrom the environment and merges it with two built-in default origins:http://localhost:3000(local development) andhttps://task-management-application-omega-lyart.vercel.app(production Vercel domain).- Wildcard origins (
*) are filtered out at startup — never returned to clients — because theAccess-Control-Allow-Credentials: trueheader required for cookie-based auth is incompatible with wildcard origins per the Fetch Standard.- Preflight requests are terminated at the middleware boundary before reaching any authentication guards or route handlers, ensuring that cross-origin cookie flows are never blocked by a missing or expired JWT during the preflight phase.
| Parameter | Type | Description | Default |
|---|---|---|---|
page |
int |
Page number (1-based) | 1 |
per_page |
int |
Items per page (max 100) | 20 |
status |
string |
Filter by status (pending, in_progress, completed) |
— |
priority |
string |
Filter by priority (low, medium, high, critical) |
— |
search |
string |
Full-text search across title and description | — |
sort_by |
string |
Sort field (created_at, updated_at, due_date, priority) |
created_at |
sort_dir |
string |
Sort direction (asc or desc) |
desc |
The backend uses Go's standard testing package with table-driven test patterns for comprehensive test coverage:
# Run all backend tests with verbose output
docker compose exec api go test ./... -v -count=1
# Run tests with race detection
docker compose exec api go test ./... -race -count=1
# Run tests for a specific package
docker compose exec api go test ./internal/... -v
# Generate coverage report
docker compose exec api go test ./... -coverprofile=coverage.out
docker compose exec api go tool cover -html=coverage.out -o coverage.html# Run frontend tests with Jest and React Testing Library
docker compose exec frontend npm test
# Run tests in watch mode
docker compose exec frontend npm test -- --watch
# Run tests with coverage
docker compose exec frontend npm test -- --coverageWe welcome contributions that align with the architectural principles of this project:
- Fork the repository
- Create a feature branch (
git checkout -b feat/your-feature) - Write tests for any new functionality
- Ensure all existing tests pass before submitting
- Open a pull request with a clear description of changes
- Go: Follow standard
gofmtformatting. Usegolangci-lintfor linting. - TypeScript / React: Use the project's ESLint and Prettier configurations.
- SQL: Use
golang-migratefor schema changes. Never modify existing migrations.
This project is licensed under the MIT License. See the LICENSE file for details.
Built with ❤️ by Mohammad Fahad
Go Clean Architecture · Next.js 14 Standalone · PostgreSQL 16 · Vercel Deploy Ready