A lightweight, robust, and idiomatic RESTful Ticket Management System built in Go (Golang) with PostgreSQL storage and JWT authentication.
This project was built following the EVA Bharat Backend Intern Assignment specification, emphasizing clean architecture, readable and non-overengineered code, strict ownership-based authorization, and deployment readiness.
- Frontend Application: https://ticket-system-frontend-ya0o.onrender.com
- Backend API: https://ticket-system-dwan.onrender.com
- Health Check: https://ticket-system-dwan.onrender.com/health
- Overview
- Key Features
- Project Structure
- Getting Started Locally
- Running with Docker
- Environment Variables
- API Documentation & Contract
- Ticket Status Lifecycle
- Screenshots & Working Flow
- Authorization & Security
- Assumptions
- Automated Testing
- Free Cloud Deployment Guide
The system allows users to:
- Register a secure user account (passwords securely hashed with bcrypt).
- Authenticate to obtain a JSON Web Token (JWT).
- Create tickets that start in
openstatus. - View and list only the tickets created by the logged-in user.
- Transition a ticket's status through an enforced progression:
open -> in_progress -> closed. - Enforce that closed tickets can never be reopened or modified.
- No Over-engineering: Built with clean Go standard library routing (
net/http) and minimal vetted libraries. - Secure Password Storage: Passwords are never stored in plain text; hashed using standard bcrypt.
- JWT Authentication: Protected endpoints require standard
Authorization: Bearer <token>. - Strict Data Isolation: Users can never view, list, or mutate tickets owned by other users. Accessing another user's ticket returns
404 Not Foundto prevent data leakage or ID enumeration. - Enforced State Machine: Ticket status strictly follows
open -> in_progress -> closed. Reopening closed tickets is rejected with400 Bad Request. - Pure Go PostgreSQL: Uses
github.com/lib/pq(standard pure Go PostgreSQL driver with zero CGO), making builds seamless across Windows, macOS, and Linux Docker without C compiler dependencies.
TicketSystem/
βββ backend/ # Complete Go REST backend
β βββ auth/ # Password hashing (bcrypt) & JWT token handling
β β βββ jwt.go
β β βββ password.go
β βββ database/ # PostgreSQL connection, schema migrations, and queries
β β βββ db.go
β βββ handlers/ # REST API HTTP handlers
β β βββ auth.go # Register and login handlers
β β βββ health.go # Health check handler
β β βββ tickets.go # Ticket CRUD and status handlers
β βββ middleware/ # Auth middleware and HTTP JSON response helpers
β β βββ auth.go
β βββ models/ # Domain models, status rules, and DTOs
β β βββ ticket.go
β β βββ user.go
β βββ .dockerignore
β βββ .env.example # Sample environment configuration
β βββ Dockerfile # Multi-stage Docker build for deployment
β βββ go.mod # Module definition
β βββ go.sum # Checksums for dependencies
β βββ main.go # Server bootstrap and route configuration
β βββ main_test.go # End-to-end integration and unit tests
βββ frontend/ # React frontend application
βββ .gitignore
βββ README.md # Project documentation
- Go 1.22+ installed on your machine.
-
Navigate to the backend directory:
cd backend -
Download dependencies:
go mod download
-
Run the server:
go run main.go
-
The server will start on port
8080:Ticket System backend listening on http://localhost:8080 -
Verify the service is running:
curl http://localhost:8080/health
You can build and run the application container using Docker:
cd backend
docker build -t ticket-system .
docker run -p 8080:8080 ticket-systemdocker build -t ticket-system -f backend/Dockerfile backend
docker run -p 8080:8080 ticket-systemVerify:
curl http://localhost:8080/healthAn example configuration file is provided in .env.example:
| Variable | Default Value | Description |
|---|---|---|
PORT |
8080 |
The HTTP port the server listens on |
JWT_SECRET |
your-secret-key-here |
Secret key used to sign and verify JWT tokens |
DATABASE_URL |
your-postgres-service-url-here |
PostgreSQL connection string |
Public endpoint to check if the server is alive.
- Method:
GET - Endpoint:
/health - Authentication: None (Public)
- Response:
200 OK{ "status": "ok" }
Registers a new user account with secure password hashing.
- Method:
POST - Endpoint:
/auth/register - Authentication: None (Public)
- Request Body:
{ "name": "Jane Doe", "email": "jane@example.com", "password": "strongPassword123" } - Response:
201 Created{ "message": "user registered successfully", "user": { "id": 1, "name": "Jane Doe", "email": "jane@example.com", "created_at": "2026-09-11T07:40:00Z" } } - Error Codes:
400 Bad Request: Missing fields or password shorter than 6 characters.409 Conflict: User with the same email already exists.
Verifies credentials and returns a signed JWT token.
- Method:
POST - Endpoint:
/auth/login - Authentication: None (Public)
- Request Body:
{ "email": "jane@example.com", "password": "strongPassword123" } - Response:
200 OK{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } - Error Codes:
401 Unauthorized: Invalid email or incorrect password.
Creates a new ticket under the authenticated user's account.
- Method:
POST - Endpoint:
/tickets - Authentication:
Bearer <token> - Request Body:
{ "title": "Payment Gateway Timeout", "description": "User reported failure during checkout payment step." } - Response:
201 Created{ "id": 1, "title": "Payment Gateway Timeout", "description": "User reported failure during checkout payment step.", "status": "open", "user_id": 1, "created_at": "2026-09-11T07:42:00Z", "updated_at": "2026-09-11T07:42:00Z" } - Notes: Initial status is always automatically set to
"open".
Retrieves all tickets created by the authenticated user.
- Method:
GET - Endpoint:
/tickets - Authentication:
Bearer <token> - Response:
200 OK[ { "id": 1, "title": "Payment Gateway Timeout", "description": "User reported failure during checkout payment step.", "status": "open", "user_id": 1, "created_at": "2026-09-11T07:42:00Z", "updated_at": "2026-09-11T07:42:00Z" } ] - Notes: Never returns tickets created by any other user.
Retrieves details of a single ticket by its ID.
- Method:
GET - Endpoint:
/tickets/{id} - Authentication:
Bearer <token> - Response:
200 OK{ "id": 1, "title": "Payment Gateway Timeout", "description": "User reported failure during checkout payment step.", "status": "open", "user_id": 1, "created_at": "2026-09-11T07:42:00Z", "updated_at": "2026-09-11T07:42:00Z" } - Security Rule: If the ticket does not exist or belongs to another user, returns
404 Not Found.
Updates the lifecycle status of a ticket owned by the user.
- Method:
PATCH - Endpoint:
/tickets/{id}/status - Authentication:
Bearer <token> - Request Body:
{ "status": "in_progress" } - Response:
200 OK{ "id": 1, "title": "Payment Gateway Timeout", "description": "User reported failure during checkout payment step.", "status": "in_progress", "user_id": 1, "created_at": "2026-09-11T07:42:00Z", "updated_at": "2026-09-11T07:45:10Z" } - Error Codes:
400 Bad Request: Invalid transition (e.g., trying to jump fromopentoclosed, or attempting to modify aclosedticket).404 Not Found: Ticket does not exist or belongs to another user.
The ticket system enforces a strict sequential workflow:
[ open ] βββββββ> [ in_progress ] βββββββ> [ closed ]
- Creation: Every new ticket starts with status
open. - Valid Transitions:
open->in_progressin_progress->closed
- Forbidden Transitions:
open->closed(skipping intermediate step is rejected)in_progress->open(moving backward is rejected)closed->open(reopening is strictly rejected)closed->in_progress(reopening is strictly rejected)
Any attempt to execute a forbidden transition results in a 400 Bad Request status code with an informative error message.
The following screenshots demonstrate the verified end-to-end working flow tested via Thunder Client:
Server Health Check (200 OK)
β
User Registration (201 Created)
β
User Login & JWT Generation (200 OK)
β
Bearer JWT Authentication (Header Setup)
β
Create Ticket with initial status 'open' (201 Created)
β
GET Ticket by ID (200 OK)
β
PATCH: open β in_progress (200 OK)
β
PATCH: in_progress β closed (200 OK)
β
Attempt to reopen/update closed ticket (400 Bad Request)
Verifies that the server is running and responding with status 200 OK ({"status": "ok"}).
Registers a new user (ganesh, pankajad20@gmail.com) with password securely hashed via bcrypt, returning status 201 Created.
Authenticates credentials and returns a signed JSON Web Token (JWT) with status 200 OK.
Configures the Authorization: Bearer <token> HTTP header required to authenticate all protected ticket endpoints.
Creates a new ticket under the authenticated user's account with initial status "open", returning status 201 Created.
Retrieves details of the created ticket (id: 7) with status 200 OK, enforcing user data ownership.
Transitions the ticket from open to in_progress using PATCH /tickets/7/status, returning status 200 OK.
Transitions the ticket from in_progress to closed using PATCH /tickets/7/status, returning status 200 OK.
Enforces lifecycle immutability: attempting to update or reopen a closed ticket is strictly rejected with 400 Bad Request ({"error": "closed ticket cannot be updated or reopened"}).
The Ticket Management System is fully deployed in production:
- Frontend App: https://ticket-system-frontend-ya0o.onrender.com
- Backend API: https://ticket-system-dwan.onrender.com
User dashboard on the deployed frontend showing authenticated session, ticket creation form, and listed tickets with OPEN status.
Viewing ticket details on production with OPEN status badge and action button to Start Progress.
Ticket status successfully transitioned to IN_PROGRESS on production, with action button to Close Ticket.
Ticket status moved to CLOSED on production. Further status modifications are disabled, displaying "Closed ticket cannot be reopened".
- Passwords: Securely hashed with bcrypt using a salt generated per user.
- Bearer Token Authentication: Standard JWT tokens signed with HMAC-SHA256 (
HS256). - Context Injection: Authenticated user ID is passed safely through the request context.
- Data Isolation: All ticket database queries and mutations include
WHERE id = ? AND user_id = ?. - No Data Leakage: Accessing another user's ticket responds with
404 Not Found, preventing unauthorized users from detecting whether a ticket ID exists.
- User Identity: User emails are treated as unique, case-insensitive identifiers.
- Ticket Assignment & Roles: In accordance with the assignment brief, no admin role or ticket assignment flow is implemented. Every user acts as the sole manager of their own tickets.
- Database: PostgreSQL is used for robust, persistent relational storage across service restarts and spin-downs.
- Token Expiration: JWT tokens are issued with a 24-hour expiration window.
The project includes an end-to-end integration test suite verifying every functional requirement and edge case.
Run all tests:
cd backend
go test -v ./...Note: Database-dependent tests connect using the TEST_DATABASE_URL (or DATABASE_URL) environment variable. If unset, database-dependent tests are cleanly skipped.
TestHealthCheck: Validates public access and exact{"status": "ok"}JSON format.TestAuth_RegisterAndLogin: Validates registration, duplicate email rejection (409), password length checks (400), correct login with JWT (200), and invalid credentials rejection (401).TestTickets_AuthRequirement: Validates missing and malformed token rejection (401).TestTickets_CreateAndInitialStatus: Validates ticket creation with default statusopen.TestTickets_OwnershipAndIsolation: Validates that users only see their own tickets and cannot view or update tickets belonging to others.TestTickets_StatusTransitions: Validates strictly allowed flow (open -> in_progress -> closed) and rejections for skips and reopenings.TestDatabase_DirectOperations: Directly validates database operations (user registration, duplicate handling, login lookup, ticket CRUD, isolation, and status transitions).TestCORS: Validates preflight and CORS response headers for allowed origins.
The service includes a multi-stage Dockerfile ready to deploy on any free hosting service (Render, Railway, Fly.io, or Koyeb).
- Push your code to GitHub.
- Sign up / log in to Render.
- Create a free PostgreSQL instance on Render and copy its Internal Database URL (or External URL).
- Click New + -> Web Service.
- Connect your GitHub repository.
- In the settings:
- Environment:
Docker - Plan:
Free
- Environment:
- Under Environment Variables, add:
DATABASE_URL:your-postgres-service-url-here(your Render PostgreSQL connection string)JWT_SECRET:your-secret-key-here
- Click Deploy Web Service.
- Render will provide a public URL like
https://ticket-system-xyz.onrender.com. - Your public health check URL will be:
https://ticket-system-xyz.onrender.com/health
- Push your code to GitHub.
- In Railway, click New Project -> Deploy from GitHub repo.
- Railway automatically detects the
Dockerfileand deploys it. - Generate a public domain in the service networking settings.
A clean, developer-focused React application is located in frontend/.
- Dark, Professional Theme: Charcoal/dark green tones, crisp borders, and accessible typography.
- Register & Login: Clean credential forms with human-readable error handling and JWT storage.
- Dashboard:
- Create Ticket: Submit tickets with automatic refresh upon creation.
- My Tickets: Displays only the authenticated user's tickets.
- Status Transitions: Sequential buttons (
open -> in_progress -> closed) that prevent invalid state transitions. - Ticket Details: Detailed modal view powered by
GET /tickets/{id}. - Automatic 401 Handling: Returns the user cleanly to the login screen if the token expires.
-
Navigate to the frontend directory:
cd frontend -
Install dependencies:
npm install
-
Configure API URL (Optional): Copy
.env.exampleto.env:cp .env.example .env
By default, Vite proxies
/health,/auth, and/ticketstohttp://localhost:8080in local development without CORS issues. For production deployment, setVITE_API_URLto your backend URL (e.g.https://your-backend.onrender.com). -
Start the development server:
npm run dev
Open
http://localhost:5173in your browser.












