Project Structure
apps/api/
├── cmd/
│ ├── api/
│ │ └── main.go # API server entry point
│ ├── BAT_worker/
│ │ ├── main.go # Admission calculator worker
│ │ └── Dockerfile
│ └── email_worker/
│ ├── main.go # Email delivery worker
│ └── Dockerfile
├── internal/
│ ├── api/
│ │ ├── api.go # Router setup, route registration
│ | ├── cookie/ # Session cookie helpers
│ │ ├── middleware/
│ │ │ ├── auth.go # Session auth, platform roles
│ │ ├── response/ # JSON response helpers
│ | └── web/ # HTTP path/query param helpers
│ ├── config/
│ │ └── config.go # Env-driven configuration structs
│ ├── ctxutils/ # Context extraction helpers
│ ├── database/
│ │ ├── connection.go # pgxpool setup
│ │ ├── errors.go # DB error helpers (unique, not found)
│ │ ├── transaction.go # Transaction manager
│ │ ├── migrations/ # SQL migration files (goose)
│ │ ├── queries/ # Raw SQL query files (sqlc input)
│ │ ├── repository/ # Data access objects
│ │ └── sqlc/ # Generated Go code (do not edit)
│ ├── emailutils/
│ │ ├── ses.go # AWS SES client
│ │ ├── templates/ # HTML email templates
│ │ └── validation.go # Email address validation
│ ├── logger/ # Zerolog initialization
│ ├── oauth/
│ │ └── discord.go # Discord OAuth2 exchange + user info
│ ├── parse/ # Generic optional type, safe parsers
│ ├── ptr/ # Pointer helpers
│ ├── domains/ # Business logic (one package per domain)
│ ├── storage/
│ │ ├── r2.go # Cloudflare R2 client
│ │ └── presignable_storage.go
│ ├── tasks/ # Asynq task definitions
│ └── workers/ # Worker process implementations
├── docs/ # Generated Swagger/OpenAPI spec
├── Dockerfile # Production multi-stage build
├── Dockerfile.dev # Development build with Air hot reload
├── go.mod
├── go.sum
└── sqlc.yml # SQLc codegen configuration
Key Conventions
Handlers
Handlers live in internal/api/handlers/ with one file per domain (e.g., events.go, application.go). Each handler struct receives its service via constructor injection. Handlers are responsible for:
- Parsing path/query parameters
- Decoding and validating request bodies
- Calling the appropriate service method
- Writing JSON responses
Domains
Domains live in internal/domains/ with one package per domain. They contain all business logic and coordinate between repositories. Domains receive repositories via constructor injection and use the transaction manager for operations that require atomicity.
Repositories
Repositories live in internal/database/repository/ and provide a type-safe data access layer over the sqlc-generated queries. Each repository wraps a *sqlc.Queries and exposes domain-specific methods. Repositories support transactions via NewTx(tx).
Generated Code
internal/database/sqlc/ is fully generated by sqlc — never edit it directly. To regenerate after changing a query or migration:
Configuration
All configuration is loaded from environment variables via internal/config/config.go. The loader tries .env.local, .env.dev, and .env in order. See Installation for the full variable reference.