Skip to content

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.

  1. Confirm JWT_SECRET is set and identical on both sides:
    echo $JWT_SECRET
    
  2. Check jwt.expiry in config.{ENV}.yaml. Default is 24h.
  3. Generate a new secret if you suspect drift:
    openssl rand -base64 32
    
  4. Have the client re-authenticate (call POST /api/v1/auth/refresh or 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.