Full-stack Go + React boilerplate with PostgreSQL, Redis/KeyDB, Docker Hardened Images (DHI) — ready to extend.
| Layer | Technology |
|---|---|
| Backend | Go 1.26, chi v5, pgx v5, Bun ORM, PostgreSQL 18, KeyDB/Redis |
| Frontend | React 19, TypeScript 6.0, Vite 8, TanStack (Router + Query + Form), Tailwind v4, shadcn/ui, Vitest, ESLint 10 |
| Deployment | Docker Hardened Images (DHI), Docker Compose, Nginx, Cloudflare (edge) |
- Go 1.26+
- Node 24+ (LTS)
- Docker + Docker Compose v2.22+ (for PostgreSQL + Redis)
docker compose up -d postgres keydbcd backend
export JWT_SECRET=your-secret-key-change-me
export DATABASE_URL=postgres://app_user:devpassword@localhost:5432/boilerplate?sslmode=disable
go mod tidy
go run ./cmd/serverServer starts on :8080. Health check: curl http://localhost:8080/api/v1/health
View auto-generated API docs: curl http://localhost:8080/docs/json
cd frontend
npm install
npm run devOpens at http://localhost:5173 with hot reload.
cd backend
bash scripts/seed.shCreates admin user: admin@boilerplate.com / password123
cmd/server/main.go — entry point, wiring, graceful shutdown
internal/
config/config.go — env-based configuration
handler/
auth_handler.go — POST /login, /refresh, /logout, GET /me
health_handler.go — GET /health (DB + Redis ping)
middleware/
auth.go — JWT verification from cookie or Authorization header
logger.go — structured JSON request logging via slog (with request_id)
ratelimit.go — Redis sliding window rate limiter (atomic Lua script)
recover.go — custom JSON panic handler (INTERNAL_ERROR)
security.go — OWASP security headers (HSTS 2y, X-Frame-DENY, etc.)
validate.go — generic JSON request validation (returns *T, toSnakeCase)
model/
user.go — User, RefreshToken structs
response.go — APIResponse, ErrorResponse, TokenResponse, LoginRequest
repository/
user_repo.go — bun.IDB-based user CRUD
refresh_token_repo.go — token rotation + cleanup in PostgreSQL transaction
service/
auth_service.go — Login (bcrypt + JWT), Refresh (rotation with reuse detection), Logout, GetUser
pkg/database/
postgres.go — pgx pool with Bun ORM wrapper
migrations/ — SQL migration files (users + refresh_tokens)
scripts/
seed.sh — seed admin user
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/v1/auth/login | No | Login — sets cookies + returns tokens |
| POST | /api/v1/auth/refresh | Token* | Rotate refresh token (cookie or body) |
| POST | /api/v1/auth/logout | Token* | Revoke refresh token (cookie or body) |
| GET | /api/v1/auth/me | JWT | Get current user |
| GET | /api/v1/health | No | DB + Redis health check |
| GET | /docs/json | No | Auto-generated route documentation (JSON) |
* Token can be sent as HttpOnly cookie (web) or refresh_token in JSON body (API clients).
Login returns tokens in both HttpOnly cookies (browsers) and JSON body (API clients). Refresh uses rotating tokens with reuse detection — if a stolen token is reused, all sessions are revoked.
| Variable | Default | Required | Description |
|---|---|---|---|
| SERVER_PORT | 8080 | No | HTTP listen port |
| DATABASE_URL | — | Yes | PostgreSQL connection string |
| JWT_SECRET | — | Yes | JWT signing key (min 32 chars) |
| CORS_ALLOWED_ORIGINS | http://localhost:3000 | No | Comma-separated allowed origins |
| LOG_LEVEL | info | No | debug, info, warn, error |
| COOKIE_SECURE | true | No | Set Secure flag on cookies (false for local HTTP dev) |
| TOKEN_TYPE | Bearer | No | Token type field in login/refresh response body |
| KEYDB_ADDR | localhost:6379 | No | Redis/KeyDB address |
- Token rotation with reuse detection — if a refresh token is used twice (stolen + legitimate), all sessions are invalidated
- Periodic cleanup — goroutine purges expired/revoked tokens every 24 hours
- Rate limiting — Redis sliding window with atomic Lua script (300 req/min global, 30 req/min on auth routes)
- Fail-open on Redis error — rate limit bypassed if Redis is down
- IP detection — respects
CF-Connecting-IP(Cloudflare),X-Real-IP(nginx), falls back toRemoteAddr - Custom JSON panic recovery — returns consistent
INTERNAL_ERRORJSON instead of plain text - Structured JSON logging — every log entry includes
request_id,status,duration_ms - Stripped binary — Docker build uses
-ldflags="-s -w"for ~30% smaller images - Connection pooling — pgx with MaxConns=20, MinConns=5
- Timing-safe auth — dummy bcrypt hash prevents email enumeration via response timing
- Auto-generated API docs —
GET /docs/jsonreturns full route tree via chi docgen
cd backend
go test ./... # 86 tests
go test -v ./internal/service/ # verbose, single packagesrc/
App.tsx — QueryClient, AuthProvider, Router, Toast provider
main.tsx — React 19 StrictMode mount
index.css — Tailwind v4 @theme tokens (shadcn/ui colors)
routeTree.gen.ts — auto-generated by TanStack Router plugin
types/
auth.ts — User, TokenResponse, LoginRequest, ApiError
lib/
api.ts — typed fetch client with GET dedup, auto-refresh, mutation timeout
constants.ts — API_BASE_URL from env
utils.ts — cn() helper (clsx + tailwind-merge)
contexts/
auth-context.tsx — AuthContext, AuthProvider (passive cache subscriber via useQuery)
toast-context.tsx — ToastContext, ToastProvider (auto-dismiss notifications)
hooks/
use-auth.tsx — useAuth hook
use-toast.tsx — useToast hook
use-idle-timeout.ts — 30-min auto-logout on inactivity
components/
layout/
query-error-boundary.tsx — Error boundary with retry
pages/
login-page.tsx — Email + password form with TanStack Form
dashboard-page.tsx — Dashboard with user info and logout
ui/
button.tsx — shadcn/ui CVA button (6 variants + loading state)
card.tsx — Card layout components
input.tsx — Styled input
label.tsx — Form label
toaster.tsx — Toast notification display (auto-positioned)
routes/
__root.tsx — Root layout, HeadContent, Outlet (no auth guard)
index.tsx — Dashboard route with beforeLoad auth guard
login.tsx — Login route with cache-only beforeLoad check
- Route code splitting —
autoCodeSplitting: true, pages lazy-loaded per route - Link preloading —
defaultPreload: 'intent'prefetches route data on hover - Auth via beforeLoad — TanStack Router
beforeLoad+throw redirect()pattern - Zero API calls on login page — AuthProvider uses
useQuery({ enabled: false }), auth state populated by route guards - SEO via TanStack Router + static fallback — OG tags, Twitter Card, canonical, theme-color, noscript in both
index.htmland per-routehead(). Plussitemap.xmlandrobots.txt. - shadcn/ui components — button, card, input, label with CVA variants
- Auto-refresh on 401 — API client silently rotates tokens and retries once
- GET deduplication — concurrent requests to the same URL share one network call
- Mutation timeout — POST/PUT/DELETE auto-abort after 30 seconds
- Idle timeout — auto-logout after 30 minutes of inactivity
- TypeScript 6.0 — strict mode, exact types throughout
| Variable | Default | Description |
|---|---|---|
| VITE_API_URL | http://localhost:8080/api/v1 | Backend API base URL |
cd frontend
npm test # 40 tests (Vitest)
npm test -- --watch # watch mode
npm test -- --coverage # with coverage report# Start all services
docker compose up -d
# Or start only infrastructure (run backend/frontend locally)
docker compose up -d postgres keydbdocker compose build
docker compose up -dAll stages use Docker Hardened Images (DHI) from dhi.io — CIS-hardened, minimal footprint, non-root runtime.
| Service | Build image | Runtime image |
|---|---|---|
| Backend | dhi.io/golang:1.26-debian13-dev |
dhi.io/static:20250419-debian13 |
| Frontend | dhi.io/node:24-debian13-dev |
dhi.io/nginx:1.30-debian13 |
The compose file includes:
- PostgreSQL with persistent volume (mount at
/var/lib/postgresqlfor PG 18+ support) and health check - KeyDB (multi-threaded Redis drop-in) with LRU eviction (256MB max)
- Backend — Go API hardened image (cache mounts, COPY --link, stripped binary, non-root)
- Frontend — Nginx hardened image serving SPA (VITE_API_URL as build-time ARG)
CSP in nginx.conf is relaxed for local development (connect-src 'self' http://localhost:*).
In production, Cloudflare handles HSTS and strict CSP at the edge — no nginx changes needed.
A Git pre-commit hook via Husky + lint-staged runs automatically on every git commit:
| Step | Check | What it catches |
|---|---|---|
| 1 | go vet ./... |
Go code quality issues |
| 2 | go build ./cmd/server |
Compilation errors |
| 3 | eslint --fix on staged .ts/.tsx |
Lint errors |
| 4 | tsc --noEmit |
TypeScript type errors |
Hook fails the commit if any step fails. Installed automatically via npm install at the repo root.
root/
backend/ Go API server
cmd/server/main.go Entry point
internal/ App logic (handlers, middleware, services, repos)
migrations/ SQL migrations
pkg/database/ DB connection helpers
scripts/ Utility scripts
frontend/ React SPA
src/ App source
public/ Static assets (sitemap, robots, favicon, OG image)
Dockerfile DHI nginx-served production build
docker-compose.yml Full-stack development + production
- Create the page component in
src/components/pages/your-page.tsx - Create
src/routes/your-page.tsx— import and use the page component, setbeforeLoadif protected - Add
head()for SEO meta tags - TanStack Router auto-generates the route tree; route is lazy-loaded automatically
- Add handler in
backend/internal/handler/ - Register route in
cmd/server/main.go - Add repository method if needed
- Add frontend API call via
get()/post()/put()/del()fromsrc/lib/api.ts
- Create migration in
backend/migrations/ - Add model struct in
backend/internal/model/ - Add repository in
backend/internal/repository/
MIT