production-ready full-stack template. go backend + next.js frontend. batteries included.
backend
chi— lightweight http router with middleware supportgorm— sql orm for gopostgres— persistent storageredis— cache and ephemeral state- jwt authentication
- single openapi spec at
backend/internal/swagger/openapi.yaml
frontend
- next.js 15 with typescript
- bun as package manager and runtime
- msw (mock service worker) for request-level api mocking in tests — no brittle function stubs
infrastructure
- docker compose for local test services
- ansible roles for provisioning and deployment
- github actions ci on push and pr
quality
- test-first workflow
- contract tests that catch drift between openapi spec and implementation
- coverage reporting
- security-focused middleware defaults (hardened response headers)
- go 1.22+ — https://go.dev/dl/
- bun — https://bun.sh
- docker desktop — https://www.docker.com/products/docker-desktop/
- make — preinstalled on macos/linux; windows:
choco install makeor use wsl - deployment only: a linux server with ssh access
1. clone
git clone <your-repo-url>
cd go-project2. bootstrap
make bootstrapfirst run: prompts for your go module name and renames it throughout the codebase.
subsequent runs: skips setup, goes straight to dependency install and test verification.
completion writes .bootstrap-done locally (git-ignored) so the prompt never repeats.
3. configure env
cp .env.example .env
cp backend/.env.example backend/.env
cp frontend/.env.example frontend/.envrequired variables:
| variable | description |
|---|---|
DATABASE_URL |
postgres connection string |
REDIS_URL |
redis connection string |
JWT_SECRET |
token signing secret — generate with openssl rand -hex 32 |
STAGE_STATUS |
backend runtime mode: dev locally, prod in production |
4. start dev
make devbackend on :5000, frontend on :3000.
5. open
- app: http://localhost:3000
- api docs (swagger ui): http://localhost:5000/swagger/ui
make test # unit tests
make validate-contracts # openapi spec vs implementation
bash scripts/test-integration.sh # integration tests (requires docker)
make coverage # coverage report
make check # full pre-deploy gate — run this before every deploygo-project/
├── backend/ # go api server
│ ├── internal/ # private packages (router, auth, db, cache, config, contract tests)
│ ├── middleware/ # shared http middleware (auth, rate limit, headers, validation)
│ ├── migrations/ # sql migration files
│ └── main.go # backend entrypoint
├── frontend/ # next.js application
│ └── src/
│ ├── app/ # app router pages, layout, sitemap, robots
│ ├── lib/ # api client and shared frontend utilities
│ └── mocks/ # msw api mocks used in tests
├── roles/ # ansible deployment roles
├── docs/ # architecture docs, adrs, and security notes
├── scripts/ # automation scripts (bootstrap, checks, integration helpers)
├── docker-compose.test.yml # postgres/redis test infrastructure
├── Makefile # common project commands
└── AGENTS.md # contributor and ai agent workflow guide
current automation is optimized for single-host deployment:
- one server hosts frontend static files, backend api, nginx, postgres, and redis
- nginx serves frontend and proxies selected backend routes
- ansible playbook targets one inventory group (
cgapp_project) and applies all roles to that same host
if you plan to host frontend and backend on different servers/domains, read docs/DEPLOYMENT_TOPOLOGIES.md before using the default playbook.
- linux server (ubuntu 22.04 recommended) with ssh access
- server ip address or hostname
- user with sudo privileges on the server
1. update hosts
edit hosts.ini and replace the placeholder with your server ip:
[servers]
your.server.ip.here2. review playbook
playbook.yml runs roles that provision: docker, nginx, postgres, redis, the backend service, and the frontend deployment artifacts. review if needed.
for split-host deployments, do not run the default playbook unchanged. use the split-host checklist in docs/DEPLOYMENT_TOPOLOGIES.md.
3. set production env
STAGE_STATUS=prod4. run the pre-deploy gate
make check5. deploy
ansible-playbook -i hosts.ini playbook.yml6. after deploy
- nginx serves the frontend on ports 80/443
- backend api runs on
:5000behind nginx - postgres and redis run as docker containers on the server
-
hosts.iniupdated with real server ip -
.envhas production values (strongJWT_SECRET, realDATABASE_URL) -
make checkpasses locally - ssh key configured for your server user
- ports 80 and 443 open in server firewall
if frontend and backend are on different hosts/domains, add these before deploy:
-
frontend/.envsetsNEXT_PUBLIC_API_URLto backend public origin (for examplehttps://api.example.com) - backend enables and configures cors allowlist for frontend origin
- auth strategy is chosen explicitly (cookie-based cross-site auth requires
SameSite=None; Secure; bearer-header auth avoids cross-site cookie constraints) - nginx/frontend host config is updated so api routes are either proxied intentionally or not proxied at all
- ansible roles/inventory are split so frontend host does not accidentally run backend/postgres/redis roles (and vice versa)
- both frontend and backend are served over https
-
docs/DEPLOYMENT_TOPOLOGIES.mdassumptions/limitations have been reviewed by the team
- open
backend/internal/swagger/openapi.yamland add your new route - run
make validate-contracts— it will fail; this is expected - write a failing test in
backend/internal/router/ - write the handler
- run
make test— confirm it passes - run
make validate-contracts— confirm it passes - add the msw mock in
frontend/src/mocks/handlers.ts - commit
the backend accepts two auth mechanisms simultaneously:
- bearer token via
Authorization: Bearer <token>header - httponly cookie named
auth_token
priority order
- bearer token present → use it
- no bearer token → fall back to
auth_tokencookie - neither present →
401
browser clients
login sets auth_token automatically. requests send it automatically with credentials: "include" in the api client. no extra cookie handling needed.
cli clients
curl -H "Authorization: Bearer <your-token>" https://yourapi.com/protectedmcp and programmatic clients use the same bearer header pattern.
getting a token
POST /auth/login with { email, password } — returns token data as json and sets the auth_token cookie.
logout
POST /auth/logout clears the auth_token cookie. bearer-only clients should discard the stored token client-side.
xss tradeoff
localStoragetokens are readable by javascript- httponly cookies are not
- this template supports both: cookie flow for browsers, bearer flow for cli/mcp/api clients
privacy-friendly, self-hostable, gdpr-compliant.
file: frontend/src/app/layout.tsx → inside the <head> of root layout
<script
defer
src="https://your-umami-instance.com/script.js"
data-website-id="your-website-id"
/>replace src with your umami instance url and data-website-id with your dashboard id.
self-host docs: https://umami.is/docs/install
backend (already wired)
file: backend/internal/telemetry/sentry.go — no code changes needed. set in .env:
SENTRY_DSN=your-dsn-from-sentry-dashboard
SENTRY_ENVIRONMENT=production
SENTRY_RELEASE=v1.0.0dsn location: sentry dashboard → project → settings → client keys
frontend (not wired yet)
cd frontend && bun add @sentry/nextjs
bunx @sentry/wizard@latest -i nextjsset in .env: NEXT_PUBLIC_SENTRY_DSN=your-dsn
docs: https://docs.sentry.io/platforms/javascript/guides/nextjs/
no email code exists yet. add it with:
- create
backend/internal/mailer/mailer.go - install sdk:
go get github.com/resendlabs/resend-go - set in
.env:RESEND_API_KEY=your-key
docs: https://resend.com/docs/send-with-go
no payment code exists yet. add it with:
- backend webhook handler:
backend/internal/router/webhook.go - frontend checkout page:
frontend/src/app/checkout/page.tsx - set in
.env:
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...docs: https://stripe.com/docs/webhooks
works with supabase storage, cloudflare r2, aws s3. no storage code exists yet. add it with:
- create
backend/internal/storage/storage.go - install sdk:
go get github.com/aws/aws-sdk-go-v2 - set in
.env:
STORAGE_ENDPOINT=https://your-endpoint
STORAGE_BUCKET=your-bucket
STORAGE_KEY=your-access-key
STORAGE_SECRET=your-secret-keyno oauth code exists yet. add it with:
- extend:
backend/internal/router/auth.go - recommended library:
golang.org/x/oauth2 - add callback routes in
backend/internal/swagger/openapi.yamlfirst
inspect postgres locally with tableplus or pgadmin. use DATABASE_URL from .env as the connection string.
| variable | required | description |
|---|---|---|
STAGE_STATUS |
required | dev or prod. controls cookie Secure flag and graceful shutdown mode. |
DATABASE_URL |
required | full postgres connection string including host, port, user, password, and database name. |
REDIS_URL |
required | redis connection string. |
JWT_SECRET |
required | secret for signing auth tokens. generate with openssl rand -hex 32. |
SERVER_HOST |
optional | backend bind host. default: 0.0.0.0. |
SERVER_PORT |
optional | backend api port. default: 5000. |
LOGGER_LEVEL |
optional | log verbosity: debug, info, warn, error. |
LOGGER_PRETTY |
optional | pretty console log output (true/false). |
SENTRY_DSN |
optional | sentry project dsn. leave empty to disable. |
SENTRY_ENVIRONMENT |
optional | sentry environment tag, usually aligned with STAGE_STATUS. |
SENTRY_RELEASE |
optional | release version tag sent to sentry. |
SENTRY_TRACES_SAMPLE_RATE |
optional | trace sample rate for sentry performance events. |
TEST_DATABASE_URL |
dev only | postgres url used by tests. local default points to port 5433. |
TEST_REDIS_URL |
dev only | redis url used by tests. local default points to port 6380. |
SERVER_READ_TIMEOUT |
optional | http read timeout in seconds. |
SERVER_WRITE_TIMEOUT |
optional | http write timeout in seconds. |
SERVER_IDLE_TIMEOUT |
optional | http idle timeout in seconds. |
DB_ENABLE |
optional | enables postgres integration when true. |
DB_HOST |
optional | postgres host when DB_ENABLE=true. |
DB_PORT |
optional | postgres port when DB_ENABLE=true. |
DB_USER |
optional | postgres user when DB_ENABLE=true. |
DB_PASSWORD |
optional | postgres password when DB_ENABLE=true. |
DB_NAME |
optional | postgres database name when DB_ENABLE=true. |
DB_SSL_MODE |
optional | postgres ssl mode (disable, require, etc). |
DB_TIMEZONE |
optional | postgres timezone setting. |
REDIS_ENABLE |
optional | enables redis integration when true. |
REDIS_HOST |
optional | redis host when REDIS_ENABLE=true. |
REDIS_PORT |
optional | redis port when REDIS_ENABLE=true. |
REDIS_PASSWORD |
optional | redis password when required. |
REDIS_DB |
optional | redis db index. |
NEXT_PUBLIC_UMAMI_WEBSITE_ID |
optional | umami site identifier used by the frontend script. |
NEXT_PUBLIC_UMAMI_SCRIPT_URL |
optional | umami script url loaded by the frontend. |
copy .env.example to .env and fill in all required variables before running make dev or make bootstrap. never commit .env — blocked by the pre-commit hook.
AGENTS.md— contributor conventions and ai agent workflow guidedocs/ARCHITECTURE.md— system design overviewdocs/adr/— decision records explaining why key choices were made- github issues — bugs and questions