Documents the Go API architecture and patterns: - Project structure and tech stack (Echo, GORM, Asynq, Redis) - Task logic package usage (predicates, scopes, categorization) - Layer architecture (Handler → Service → Repository) - Build commands, testing, and environment configuration - API endpoints reference - Links to related docs (TASK_LOGIC_ARCHITECTURE.md) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
11 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Important Guidelines
⚠️ DO NOT auto-commit code changes. Always ask the user before committing. Only create commits when the user explicitly requests it with commands like "commit this work" or "create a commit".
Required Reading Before Task-Related Work
STOP! Before writing ANY task-related code, you MUST read:
📖 docs/TASK_LOGIC_ARCHITECTURE.md
This document contains:
- The consolidated task logic architecture (predicates, scopes, categorization)
- Developer checklist for adding task features
- Common pitfalls (PostgreSQL DATE vs TIMESTAMP, preload requirements)
- Examples of correct vs incorrect patterns
Why this matters: Task logic (completion detection, overdue calculation, kanban categorization) is centralized in internal/task/. Duplicating this logic elsewhere causes bugs where different parts of the app show different results.
Quick reference:
// Use these - DON'T write inline task logic
import "github.com/treytartt/casera-api/internal/task"
task.IsCompleted(t) // Check if task is completed
task.IsOverdue(t, now) // Check if task is overdue
task.ScopeOverdue(now) // GORM scope for overdue tasks
task.CategorizeTask(t, 30) // Get kanban column for task
Project Overview
Casera API is a Go REST API for the MyCrib/Casera property management platform. It provides backend services for iOS and Android mobile apps built with Kotlin Multiplatform.
Tech Stack:
- HTTP Framework: Echo v4
- ORM: GORM with PostgreSQL
- Push Notifications: APNs (apns2) + FCM HTTP API
- Admin Panel: GoAdmin
- Background Jobs: Asynq with Redis
- Caching: Redis
- Logging: zerolog
- Configuration: Viper
Project Structure
myCribAPI-go/
├── cmd/
│ ├── api/main.go # API server entry point
│ └── worker/main.go # Background worker entry point
├── internal/
│ ├── config/ # Configuration (Viper)
│ ├── database/ # Database connection setup
│ ├── models/ # GORM models
│ ├── repositories/ # Data access layer
│ ├── services/ # Business logic
│ ├── handlers/ # HTTP handlers (Echo)
│ ├── middleware/ # Echo middleware (auth, logging, etc.)
│ ├── router/ # Route setup
│ ├── dto/ # Request/Response DTOs
│ ├── task/ # Task logic (predicates, scopes, categorization)
│ ├── push/ # APNs/FCM push notifications
│ ├── worker/ # Asynq background jobs
│ ├── admin/ # GoAdmin tables
│ ├── apperrors/ # Custom error types
│ ├── i18n/ # Internationalization
│ ├── monitoring/ # Health checks, metrics
│ ├── validator/ # Custom validation rules
│ └── integration/ # Integration tests
├── migrations/ # SQL migrations
├── seeds/ # Seed data (lookups, test data)
├── docs/ # Documentation
├── docker/ # Docker files
├── go.mod
└── Makefile
Build & Run Commands
# Install dependencies
make deps
# Run the API server (development)
make run
# or
go run ./cmd/api
# Run the background worker
make run-worker
# Build binaries
make build # API only
make build-all # API, Worker, Admin
# Run tests
make test # All tests
make test-coverage # With coverage report
go test ./internal/handlers # Specific package
# Lint and format
make lint
make fmt
# Docker
make docker-up # Start all services
make docker-down # Stop services
make docker-logs # View logs
make docker-restart # Restart services
# Database migrations
make migrate-up
make migrate-down
Architecture Patterns
Layer Architecture
Request → Router → Middleware → Handler → Service → Repository → Database
↓
Response DTO
- Handlers (
internal/handlers/): HTTP request/response handling, input validation - Services (
internal/services/): Business logic, orchestration - Repositories (
internal/repositories/): Database operations (GORM) - Models (
internal/models/): GORM entities (map to PostgreSQL tables)
Task Logic Package
The internal/task/ package centralizes all task-related logic:
internal/task/
├── predicates/predicates.go # Pure functions: IsCompleted, IsOverdue, etc.
├── scopes/scopes.go # GORM scopes: ScopeOverdue, ScopeActive, etc.
├── categorization/chain.go # Kanban column determination
└── task.go # Re-exports for single import
Always use this package for task logic:
import "github.com/treytartt/casera-api/internal/task"
// Predicates (in-memory checks)
if task.IsCompleted(t) { ... }
if task.IsOverdue(t, time.Now()) { ... }
// Scopes (database queries)
db.Scopes(task.ScopeActive).Scopes(task.ScopeOverdue(now)).Find(&tasks)
// Categorization
column := task.CategorizeTask(t, 30) // 30 = "due soon" threshold days
Authentication
Token-based authentication using Django's authtoken format for compatibility:
// Middleware extracts user from token
user := middleware.GetUserFromContext(c)
// Protected routes require auth middleware
api := e.Group("/api")
api.Use(middleware.AuthRequired(db))
Error Handling
Use structured errors from internal/apperrors/:
import "github.com/treytartt/casera-api/internal/apperrors"
// Return typed errors
return apperrors.NewNotFoundError("task", taskID)
return apperrors.NewValidationError("title is required")
return apperrors.NewForbiddenError("not authorized to access this residence")
// Handler converts to HTTP response
// 404, 400, 403 respectively
Database
GORM Models
Models map to Django's existing PostgreSQL tables:
// internal/models/task.go
type Task struct {
ID uint `gorm:"primaryKey"`
ResidenceID uint `gorm:"column:residence_id"`
Title string `gorm:"column:title"`
DueDate *time.Time `gorm:"column:due_date;type:date"`
NextDueDate *time.Time `gorm:"column:next_due_date;type:date"`
IsCancelled bool `gorm:"column:is_cancelled"`
IsArchived bool `gorm:"column:is_archived"`
InProgress bool `gorm:"column:in_progress"`
// ...
}
func (Task) TableName() string {
return "task_task" // Django table name
}
Common Pitfalls
- DATE vs TIMESTAMP: Use
type:datefor date-only fields, nottype:timestamp - Preloading: Always preload associations when needed:
db.Preload("Completions").Preload("Priority").Find(&task) - Timezone: Server uses UTC; client sends timezone header for overdue calculations
Migrations
# Create new migration
make migrate-create name=add_new_column
# Run migrations
make migrate-up
# Rollback
make migrate-down
API Endpoints
Public Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/health/ |
Health check |
| POST | /api/auth/login/ |
Login |
| POST | /api/auth/register/ |
Register |
| GET | /api/static_data/ |
Cached lookups (ETag support) |
| GET | /api/upgrade-triggers/ |
Subscription upgrade triggers |
Protected Endpoints (Token Auth)
| Method | Path | Description |
|---|---|---|
| GET | /api/residences/my/ |
User's residences with summaries |
| GET | /api/residences/:id/ |
Residence detail |
| GET | /api/tasks/ |
All user's tasks (kanban columns) |
| GET | /api/tasks/by-residence/:id/ |
Tasks for residence (kanban) |
| POST | /api/tasks/ |
Create task |
| POST | /api/tasks/:id/complete/ |
Complete task |
| GET | /api/contractors/ |
User's contractors |
| GET | /api/documents/ |
User's documents |
| GET | /api/subscription/status/ |
Subscription status |
Adding New Features
Adding a New API Endpoint
- Create/update model in
internal/models/ - Create repository in
internal/repositories/:type MyRepository struct { db *gorm.DB } func (r *MyRepository) FindByID(id uint) (*models.MyModel, error) { var m models.MyModel if err := r.db.First(&m, id).Error; err != nil { return nil, err } return &m, nil } - Create service in
internal/services/:type MyService struct { repo *repositories.MyRepository } func (s *MyService) DoSomething(id uint) error { // Business logic here } - Create handler in
internal/handlers/:type MyHandler struct { service *services.MyService } func (h *MyHandler) Get(c echo.Context) error { id := c.Param("id") result, err := h.service.DoSomething(id) if err != nil { return err } return c.JSON(http.StatusOK, result) } - Register routes in
internal/router/router.go
Adding Task-Related Logic
ALWAYS use the task package. Never write inline task logic.
- Add predicate to
internal/task/predicates/predicates.go - Add corresponding scope to
internal/task/scopes/scopes.go - Re-export in
internal/task/task.go - Add tests for both predicate and scope
- Update
docs/TASK_LOGIC_ARCHITECTURE.md
Testing
# Run all tests
go test ./...
# Run specific package tests
go test ./internal/handlers -v
# Run with race detection
go test -race ./...
# Integration tests (requires database)
TEST_DATABASE_URL="..." go test ./internal/integration -v
Test Patterns
func TestTaskHandler_Create(t *testing.T) {
// Setup test database
db := testutil.SetupTestDB(t)
// Create handler with dependencies
handler := NewTaskHandler(db)
// Create test request
req := httptest.NewRequest(http.MethodPost, "/api/tasks/", body)
rec := httptest.NewRecorder()
c := echo.New().NewContext(req, rec)
// Execute
err := handler.Create(c)
// Assert
assert.NoError(t, err)
assert.Equal(t, http.StatusCreated, rec.Code)
}
Environment Variables
| Variable | Description | Required |
|---|---|---|
PORT |
Server port (default: 8000) | No |
DEBUG |
Enable debug mode | No |
SECRET_KEY |
Token signing secret | Yes |
POSTGRES_HOST |
Database host | Yes |
POSTGRES_PORT |
Database port | Yes |
POSTGRES_USER |
Database user | Yes |
POSTGRES_PASSWORD |
Database password | Yes |
POSTGRES_DB |
Database name | Yes |
REDIS_URL |
Redis connection URL | Yes |
APNS_KEY_ID |
Apple Push key ID | For push |
APNS_TEAM_ID |
Apple Team ID | For push |
APNS_TOPIC |
App bundle ID | For push |
FCM_SERVER_KEY |
Firebase server key | For push |
Related Documentation
docs/TASK_LOGIC_ARCHITECTURE.md- Task logic patterns (MUST READ)docs/PUSH_NOTIFICATIONS.md- Push notification setupdocs/SUBSCRIPTION_WEBHOOKS.md- App Store/Play Store webhooksdocs/DOKKU_SETUP.md- Production deployment
Related Repositories
- Mobile App:
../MyCribKMM- Kotlin Multiplatform iOS/Android app - Root Docs:
../CLAUDE.md- Full-stack documentation