Help Desk Ticketing System
A fault at a retail-outlet terminal becomes a ticket — pinned to its outlet, owned by a technician, and tracked from intake to an on-site close.
- My role
- Full-stack · Go API, data model, auth & lifecycle
- Domain
- Field-service support for a retail-outlet network
- Scope
- Intake · assignment · status workflow · reply threads · dashboard
- Stack
- Go (Gin) · PostgreSQL (GORM) · React · JWT auth
A helpdesk for a network of retail outlets, where a problem at a specific terminal becomes a ticket pinned to its area, regional, and outlet — assigned to a technician, moved through explicit states, and closed after a field visit. I built the Go API, the PostgreSQL model, the auth middleware, and the ticket lifecycle behind a React console built on a Bootstrap admin template.

A ticket that knows where it lives.
Every ticket carries its own address — terminal, outlet, regional, area — written onto the row itself, so the queue slices at any level of the hierarchy without joining out to find where a fault physically is. Each account carries a matching scope. But that scope is handed to the browser at login and sent back as filter params the API takes at their word: a view, not a boundary.

The trend chart is one grouped query.
Three COUNT(*) FILTER aggregates bucketed by day, straight from Postgres — the chart renders finished numbers instead of counting raw tickets in the app. The status tiles beside it are a second query, so the dashboard is two, not one.

Status you earn — and a dropdown for the rest.
Starting and closing are buttons, not fields: each stamps the status, the actor and the time in one call, and the request carries no status to override. Reopen a ticket and it drops its close timestamp and the name on it — a closed-by that's no longer true is worse than an empty one. What's missing is the state machine. Nothing stops Finish → Process.
Every route through the same gate.
Auth isn't a decorator you remember to add — every non-public route group goes through the same two middlewares, so no endpoint ships without them. The gap is what they ask. Both check who you are; neither checks what you're allowed to do, so the roles and permissions modelled in Postgres never get read on the server. And the second one — a shared-secret signature next to the JWT — bought nothing: the build inlines that secret into the bundle, so it shipped to every browser. A second copy of the same trust, not a second layer of it.
A helpdesk stays honest when the ticket carries its own context — which terminal, which outlet, who owns it, what state it's in — so nothing depends on the person who happened to pick up the phone.