A minimal, production-friendly user authentication template for Go. It reduces boilerplate for common auth workflows using Gin, GORM, Postgres, bcrypt, and JWT. It ships with clean layering (controller → service → repository), auth middleware, cookie-based JWT session handling, migrations, CORS, health checks, and tests (including Docker-backed integration tests).
Background and inspiration: I built this primarily for my own learning and because I was tired of rewriting the same boilerplate for every new project. The structure and setup were influenced by Melkey's go-blueprint: https://github.com/Melkeydev/go-blueprint.
- RESTful auth endpoints: register, login, me, logout, change password, delete account
- Cookie-based JWT authentication (HTTP-only cookie named
Authorization) - Password hashing with bcrypt
- Postgres with GORM ORM and a simple migration entrypoint
- Config via environment variables, auto-loaded from
.env - CORS configured for credentialed requests
- Health endpoint and graceful shutdown
- Unit tests and integration tests using Testcontainers
- Go (Gin HTTP framework)
- GORM with Postgres driver
- JWT (github.com/golang-jwt/jwt/v5)
- bcrypt (golang.org/x/crypto/bcrypt)
- Testcontainers for integration testing
cmd/
api/
main.go # server startup with graceful shutdown
internal/
database/ # DB service, health, connection management
middlewares/ # auth middleware (JWT + DB lookup)
models/ # GORM models (User)
server/ # HTTP server, routes, CORS, health
user/ # auth module: DTOs, repository, service, controller
utils/ # bcrypt and JWT helpers
migrate/
migrate.go # simple migration runner (AutoMigrate)
Makefile # common tasks (run, test, docker, watch)
docker-compose.yml # Postgres service for local dev
This project uses github.com/joho/godotenv/autoload, so variables from a .env file in the repository root will be loaded automatically for the API and migrations. Docker Compose also reads .env.
Copy .env.example to .env and adjust values:
cp .env.example .envRequired variables:
- Server
PORT(e.g., 8080)SECRET_KEY– HMAC secret for signing JWTsCOOKIE_DOMAIN– domain to set on theAuthorizationcookie (e.g.,localhost)
- Database
BLUEPRINT_DB_HOST(e.g.,localhost)BLUEPRINT_DB_PORT(e.g.,5432)BLUEPRINT_DB_USERNAMEBLUEPRINT_DB_PASSWORDBLUEPRINT_DB_DATABASEBLUEPRINT_DB_SCHEMA(e.g.,public)
- Start Postgres with Docker Compose:
make docker-run- Run migrations:
go run migrate/migrate.go- Start the API:
make runThe server listens on :$PORT.
internal/server/routes.go enables CORS with credentials for http://localhost:5173 by default. If your frontend runs elsewhere, update the AllowOrigins list.
To make authenticated browser requests, ensure your frontend sends credentials (cookies) with each request.
- On register and login, the server returns a JWT access token and sets an HTTP-only cookie
Authorizationwith the token value. SameSite is set to Lax. - Token expiration is 7 days. The cookie lifetime is configured in the controller (currently 30 days). Clients should handle 401 responses and re-authenticate when the token is expired.
- Protected routes require the cookie and will fetch the user from the database by the token subject (
sub).
Base URL: http://localhost:$PORT
- GET
/– Hello world - GET
/health– Database health stats
Auth routes (all under /auth):
-
POST
/auth/register- Body:
{ "name": string, "email": string, "password": string(min 6) } - Effects: Creates user, sets
Authorizationcookie, returnstokenand user info - Responses:
201 Createdon success
- Body:
-
POST
/auth/login- Body:
{ "email": string, "password": string } - Effects: Verifies credentials, sets
Authorizationcookie, returnstokenand user info - Responses:
201 Createdon success,401 Unauthorizedon invalid credentials
- Body:
-
GET
/auth/me(protected)- Reads user from
Authorizationcookie - Responses:
200 OKwith{ id, name, email }, or401 Unauthorized
- Reads user from
-
POST
/auth/logout(protected)- Clears the
Authorizationcookie - Responses:
200 OK
- Clears the
-
POST
/auth/change-password(protected)- Body:
{ "old_password": string, "new_password": string(min 6) } - Effects: Validates old password and updates to hashed new password
- Responses:
200 OKor400 Bad Request
- Body:
-
DELETE
/auth/delete-account(protected)- Effects: Deletes the current user and clears cookie
- Responses:
200 OK
Register:
curl -i \
-H 'Content-Type: application/json' \
-d '{"name":"Alice","email":"alice@example.com","password":"secret123"}' \
http://localhost:$PORT/auth/registerLogin:
curl -i \
-H 'Content-Type: application/json' \
-d '{"email":"alice@example.com","password":"secret123"}' \
http://localhost:$PORT/auth/loginMe (reusing the cookie captured from the previous response headers):
curl -i \
--cookie 'Authorization=YOUR_JWT' \
http://localhost:$PORT/auth/meThis template uses GORM with Postgres. The migrate/migrate.go file connects using the environment variables above and runs AutoMigrate for internal/models.User.
Run migrations:
go run migrate/migrate.gomake all– build and testmake build– build binary to./mainmake run– run the APImake docker-run– start Postgres via Docker Composemake docker-down– stop Postgresmake test– run all testsmake itest– run database integration tests onlymake clean– remove./mainmake watch– live reload usingair(prompts to install if missing)
- Unit tests:
go test ./... -v - Integration tests:
make itest(uses Testcontainers; Docker must be running)
Notable tests:
internal/server/routes_test.gocovers the hello world handlerinternal/database/database_test.goboots a real Postgres container and validates connection health and lifecycle
- JWT signing uses
HS256withSECRET_KEY. Claims includesub(user ID) andexp(expiration). - The auth middleware reads the
Authorizationcookie, validates the token, loads the user by ID, and setsc.Set("user", models.User)for downstream handlers. - CORS is configured with
AllowCredentials: trueandAllowOrigins: ["http://localhost:5173"]. Update this for your frontend. - Graceful shutdown is implemented in
cmd/api/main.goand handles SIGINT/SIGTERM with a 5s drain period.
Contributions are welcome. See CONTRIBUTING.md for how to get started, coding standards, and the PR process.
If you discover a vulnerability, please follow the guidance in SECURITY.md and do not open a public issue.
MIT. See LICENSE for details.