Troubleshooting¶
Common backend boot and dev-loop problems and the fastest fix for each. Symptom → Cause → Fix.
Build / runtime¶
dyld: missing LC_UUID load command on macOS¶
Cause. A known issue with go run on macOS (especially Apple Silicon) where the just-built binary lacks a LC_UUID segment that the dynamic linker now requires for h3-go and other cgo-linked libraries.
Fix. Build to disk, then execute, instead of go run:
go build -o bin/server ./cmd/server
ENV=local ./bin/server
task dev already does this via Air, which builds into tmp/ and re-execs. If you're invoking Go manually, prefer go build + ./bin/server. If the error persists, clear the module cache:
go clean -cache && go clean -modcache && go mod download
package github.com/tomoda-labs/tomoda/... not in GOROOT¶
Cause. Go can't resolve the module. Usually because you ran a command outside the backend/ directory.
Fix. All Go commands must run with backend/ as the working directory. Use task (which sets dir: for every backend task) or cd backend && go ....
wire_gen.go is out of date¶
Cause. You added or removed a constructor in backend/internal/wiring/providers.go and didn't regenerate the Wire output.
Fix.
task gen:wire
# or: cd backend && wire ./cmd/server
Install Wire first if needed: go install github.com/google/wire/cmd/wire@latest. See Wiring (DI) for full details.
Database¶
failed to connect to database: dial tcp [::1]:5432: connection refused¶
Cause. Postgres isn't running.
Fix.
docker-compose -f docker-compose.dev.yml up -d postgres
Verify:
docker-compose -f docker-compose.dev.yml ps postgres
psql -h localhost -p 5432 -U tomoda -d tomoda # default local creds
If Postgres is up but auth fails, check DB_USER / DB_PASSWORD in your env or YAML.
failed to migrate database: ...¶
Cause. Schema drift — a column type was changed in a way GORM's AutoMigrate cannot reconcile (e.g. enum narrowing, FK with bad existing rows).
Fix. In local dev, the fastest path is a fresh DB:
task db:reset # docker:clean + docker:up + sleep + task db:migrate
task db:test:setup # re-seed
In production, never use db:reset — write a proper SQL migration in backend/migrations/.
Configuration¶
failed to load YAML config: no config file found¶
Cause. The loader can't find config.{ENV}.yaml or config.yaml, or you're running from the wrong directory.
Fix.
ls backend/config*.yaml # confirm the file exists
ENV=local task dev:backend
If the file exists but isn't being picked up, your CWD is wrong: config.Load() reads files relative to the process's working directory.
Stripe Webhook Secret NOT loaded or too short¶
Cause. .env.{ENV} isn't being loaded, or STRIPE_WEBHOOK_SECRET is unset.
Fix. Pull secrets from GCP:
eval $(./scripts/pull-secrets.sh --export)
Or copy the example template and fill in your own values:
cp backend/env.example.local backend/.env.local
$EDITOR backend/.env.local
GCP auth not loaded — running with local defaults only¶
Cause. gcloud auth print-access-token failed. Either you aren't logged in, the active config points at the wrong project, or you're offline.
Fix.
gcloud auth login
gcloud config set project development-485000
gcloud auth application-default login
If GCP isn't available at all (offline dev), the local defaults baked into task dev are sufficient for Postgres, Redis, and MinIO. OAuth, Stripe, and email features will not work without real secrets.
Auth¶
invalid token on every request¶
Cause. Either JWT_SECRET differs between the issuer and verifier, or the token has expired.
Fix.
- Confirm
JWT_SECRETis set and identical on both sides:echo $JWT_SECRET - Check
jwt.expiryinconfig.{ENV}.yaml. Default is24h. - Generate a new secret if you suspect drift:
openssl rand -base64 32 - Have the client re-authenticate (call
POST /api/v1/auth/refreshor log in again).
JWT signing failure on boot¶
Cause. JWT_SECRET is empty.
Fix. Set it in .env.{ENV} or your shell. Boot will succeed but every token issued will fail to verify on the next request.
Network / I/O¶
failed to send email: SMTP not configured (or email API key not configured)¶
Cause. Email.APIKey and/or Email.BaseURL are unset. The backend uses an HTTP-based email API (see internal/services/email_service.go) — there is no direct SMTP path.
Fix. Set EMAIL_APIKEY and email.base_url in YAML. For local dev, you can disable email-dependent flows or point EMAIL_APIKEY at a mock endpoint.
failed to connect to redis: dial tcp [::1]:6379: connection refused¶
Cause. Redis isn't running.
Fix.
docker-compose -f docker-compose.dev.yml up -d redis
redis-cli -h localhost -p 6379 ping # should reply PONG
If Redis is up but the backend still can't connect, check REDIS_HOST, REDIS_PORT, and REDIS_PASSWORD.
listen tcp :8080: bind: address already in use¶
Cause. Another process owns port 8080 (often a previous backend instance that didn't shut down cleanly).
Fix.
lsof -i :8080
kill -9 <PID>
Or run on a different port temporarily:
PORT=8081 task dev:backend
Quick diagnostic¶
When in doubt, run a quick health check across the stack:
# API
curl -i http://localhost:8080/health
# Database
psql -h localhost -U tomoda -d tomoda -c "SELECT 1"
# Redis
redis-cli -h localhost ping
# Object storage
curl -s http://localhost:9000/minio/health/live && echo OK
If all four pass and the backend still misbehaves, check backend/tmp/main.log (Air's build/run log) and docker-compose -f docker-compose.dev.yml logs -f postgres redis minio.