Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ PUBLIC_URL=http://localhost:3000
# Optional: listen port (Railway sets it).
# PORT=3000

# Local docker compose Postgres. Hosted Postgres uses ?sslmode=require.
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/app?sslmode=disable
# Unset: make points it at this checkout's docker compose Postgres (`make env`).
# Hosted Postgres uses ?sslmode=require.
# DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/app?sslmode=disable

# At least 32 characters. Generate one with: openssl rand -base64 32
AUTH_SECRET=dev-only-secret-change-me-0123456789abcdef
Expand All @@ -28,7 +29,9 @@ AUTH_SECRET=dev-only-secret-change-me-0123456789abcdef
RESEND_API_KEY=
FROM_EMAIL=noreply@example.com

# Host ports for docker compose (override when taken by other projects).
# make derives a compose project and ports per checkout so parallel worktrees
# never collide; `make env` prints them. Override only if you need fixed values.
# COMPOSE_PROJECT_NAME=myapp
# POSTGRES_PORT=5432
# PGWEB_PORT=5050
# TEST_POSTGRES_PORT=54329
# TEST_POSTGRES_PORT=54329 (unset: a free port on every start)
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,17 @@ The most common defect is a change that works on the path you tested and is miss

**Killing by pattern.** Never `pkill -f`, `pgrep | kill`, or kill a PID found by matching a name, path or port. Many servers run on this machine and yours can match. Kill only the PID you captured at spawn.

**Trusting the wrong database.** The Docker Postgres from `make db-up` is the sandbox: seed it, reset it, break it. A hosted database (Railway, Neon, production) is never a playground: don't point dev at one, migrate one, or copy real data in or out unless explicitly asked. `docker compose down -v` deletes the local volume.
**Trusting the wrong database.** The Docker Postgres from `make db-up` is the sandbox: seed it, reset it, break it. A hosted database (Railway, Neon, production) is never a playground: don't point dev at one, migrate one, or copy real data in or out unless explicitly asked. `make db-down` keeps the volume; `docker compose down -v` deletes it.

**Baking in origins.** Never hardcode `http://localhost:3000` in code, emails or auth callbacks. Origins come from `PUBLIC_URL`, `INTERNAL_URL` and `TRUSTED_ORIGINS` through `config.Config`, or every non-local client breaks.

## Dev loop

- `make db-up && make migrate && make dev` starts Postgres + pgweb (bound to 127.0.0.1), runs migrations, and serves on `0.0.0.0:3000`. `.env` is created from `.env.example` on first run; set `POSTGRES_PORT` etc. there if the defaults are taken.
- `make db-up && make migrate && make dev` starts Postgres + pgweb (bound to 127.0.0.1), runs migrations, and serves on `0.0.0.0:3000`. `.env` is created from `.env.example` on first run.
- Each checkout (clone or git worktree) gets its own compose project and dev database ports, derived from its path; the test Postgres takes a free port on every start. Parallel worktrees never share or collide. `make env` prints the values; override `COMPOSE_PROJECT_NAME`, `POSTGRES_PORT`, `PGWEB_PORT`, `TEST_POSTGRES_PORT` or `DATABASE_URL` in `.env` only when you need fixed ones. Always go through `make`: bare `docker compose` uses the shared default project. `PORT` (the app) is not derived; set it in `.env` when two worktrees run `make dev` at once.
- Auth is real in every environment. In development email verification is off and the dev mailer logs verification and reset links to the server log, so sign up through `/api/auth/sign-up/email` (or the UI) and copy links from the log. `packages/auth/README.md` has the endpoints, the security model and how to port more Better Auth features.
- A running dev server is fine to leave up. Stop only what you started, by PID.
- Inspect data with pgweb (`127.0.0.1:5050`). Seed realistic data before verifying a user-facing flow; an empty database is a bad test. Secrets go in `.env` only when the flow needs them, and never get committed.
- Inspect data with pgweb (URL from `make env`). Seed realistic data before verifying a user-facing flow; an empty database is a bad test. Secrets go in `.env` only when the flow needs them, and never get committed.

## Verifying

Expand Down
36 changes: 30 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,20 @@ export
GO ?= go
TOOL := $(GO) tool -modfile=tools/go.mod
COMPOSE := docker compose
TEST_POSTGRES_PORT ?= 54329
TEST_DATABASE_URL ?= postgres://postgres:postgres@127.0.0.1:$(TEST_POSTGRES_PORT)/postgres?sslmode=disable
BIN := bin/server

# Every checkout (clone or git worktree) gets its own compose project and dev
# ports, derived from its path, so parallel worktrees never share containers.
# Override any of these in the environment or .env.
CHECKOUT_ID := $(shell printf %s '$(CURDIR)' | cksum | cut -d' ' -f1)
SLOT := $(shell expr $(CHECKOUT_ID) % 1000)
COMPOSE_PROJECT_NAME ?= $(shell basename '$(CURDIR)' | tr 'A-Z' 'a-z' | tr -c 'a-z0-9\n' '-' | cut -c1-30)-$(shell printf %x $(CHECKOUT_ID) | cut -c1-6)
POSTGRES_PORT ?= $(shell expr 25000 + $(SLOT))
PGWEB_PORT ?= $(shell expr 26000 + $(SLOT))
DATABASE_URL ?= postgres://postgres:postgres@127.0.0.1:$(POSTGRES_PORT)/app?sslmode=disable
# Empty: the test server gets a free port from Docker on every start.
TEST_POSTGRES_PORT ?=

.DEFAULT_GOAL := help

.env:
Expand Down Expand Up @@ -48,8 +58,15 @@ lint-layering: ## Prove forbidden imports fail lint
./scripts/check-layering.sh

.PHONY: test
test: test-db-up ## go test -race against a throwaway Postgres
TEST_DATABASE_URL='$(TEST_DATABASE_URL)' $(GO) test -race ./...
test: ## go test -race against this checkout's throwaway Postgres (or TEST_DATABASE_URL)
ifdef TEST_DATABASE_URL
$(GO) test -race ./...
else
@$(MAKE) --no-print-directory test-db-up
@port=$$($(COMPOSE) port postgres-test 5432 | sed 's/.*://'); \
echo "test db: 127.0.0.1:$$port (project $(COMPOSE_PROJECT_NAME))"; \
TEST_DATABASE_URL="postgres://postgres:postgres@127.0.0.1:$$port/postgres?sslmode=disable" $(GO) test -race ./...
endif

.PHONY: ci
ci: ## Run the same checks as CI
Expand All @@ -63,8 +80,15 @@ ci: ## Run the same checks as CI
migrate: ## Run auth then app migrations against DATABASE_URL
$(GO) run ./apps/web/cmd/server migrate

.PHONY: env
env: ## Print this checkout's compose project, ports and DATABASE_URL
@echo "compose project: $(COMPOSE_PROJECT_NAME)"
@echo "postgres: 127.0.0.1:$(POSTGRES_PORT) (DATABASE_URL=$(DATABASE_URL))"
@echo "pgweb: http://127.0.0.1:$(PGWEB_PORT)"
@echo "test postgres: 127.0.0.1:$(if $(TEST_POSTGRES_PORT),$(TEST_POSTGRES_PORT),<free port per start>)"

.PHONY: db-up
db-up: ## Start Postgres + pgweb (127.0.0.1 only)
db-up: env ## Start Postgres + pgweb (127.0.0.1 only)
$(COMPOSE) up -d --wait postgres pgweb

.PHONY: db-down
Expand All @@ -73,7 +97,7 @@ db-down: ## Stop Postgres + pgweb (keeps the data volume)

.PHONY: test-db-up
test-db-up:
$(COMPOSE) --profile test up -d --wait postgres-test
$(COMPOSE) --profile test up -d --wait --quiet-pull postgres-test

.PHONY: docker
docker: ## Build the production image
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ A Go + Datastar monorepo template: one static binary serving server-rendered `te
Needs Go (version in `mise.toml`; `mise install`) and Docker.

```sh
make db-up # Postgres 18 + pgweb on 127.0.0.1 (creates .env from .env.example)
make db-up # Postgres 18 + pgweb on 127.0.0.1, ports per checkout (creates .env)
make migrate # auth migrations, then app migrations
make dev # http://<this-machine>:3000, reachable over LAN/Tailscale
```
Expand All @@ -29,19 +29,20 @@ curl -b jar -H 'Origin: http://localhost:3000' -X POST localhost:3000/api/auth/s

Auth endpoints live under `/api/auth/` (sign-up, sign-in, sign-out, get-session, verify-email, send-verification-email, request-password-reset, reset-password); see [packages/auth/README.md](packages/auth/README.md).

Ports taken? Set `POSTGRES_PORT`, `PGWEB_PORT`, `TEST_POSTGRES_PORT` and `PORT` in `.env` (and the port in `DATABASE_URL`).
Each checkout gets its own compose project and database ports, derived from its path, so any number of clones or git worktrees can run `make db-up` and `make test` at once. `make env` prints them (pgweb URL included). To pin them, set `COMPOSE_PROJECT_NAME`, `POSTGRES_PORT`, `PGWEB_PORT`, `TEST_POSTGRES_PORT` or `DATABASE_URL` in `.env`. The app's `PORT` stays 3000 unless you set it.

## Commands

| | |
|---|---|
| `make dev` | run the server with `.env` |
| `make test` | `go test -race ./...` against a throwaway tmpfs Postgres |
| `make test` | `go test -race ./...` against this checkout's throwaway tmpfs Postgres (or `TEST_DATABASE_URL`) |
| `make lint` | gofmt + golangci-lint (includes layering rules) |
| `make ci` | everything CI runs |
| `make generate` | sqlc (and templ) code generation |
| `make build` / `make docker` | static binary / production image |
| `make db-up` / `make db-down` | start / stop the local databases |
| `make env` | print this checkout's compose project, ports and `DATABASE_URL` |

## Stack

Expand Down
8 changes: 5 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Local sandbox only. Ports bind to 127.0.0.1; set POSTGRES_PORT / PGWEB_PORT /
# TEST_POSTGRES_PORT in .env when the defaults are taken.
# Local sandbox only; ports bind to 127.0.0.1. Run it through make: the
# Makefile gives each checkout its own project name and ports (`make env`), so
# parallel worktrees never collide. The defaults below apply to bare
# `docker compose` only.
name: ${COMPOSE_PROJECT_NAME:-ai-patterns-go}

services:
Expand Down Expand Up @@ -42,7 +44,7 @@ services:
POSTGRES_PASSWORD: postgres
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off", "-c", "max_connections=200"]
ports:
- "127.0.0.1:${TEST_POSTGRES_PORT:-54329}:5432"
- "127.0.0.1:${TEST_POSTGRES_PORT:-}:5432" # empty: Docker picks a free port
tmpfs:
- /var/lib/postgresql
healthcheck:
Expand Down
Loading