.github/skills/backend-development/SKILL.md
Build high-performance, optimized Go/Fiber backend services for Clarin CRM. Use when modifying API handlers, repository queries, services, domain entities, database migrations, Kommo sync, or WhatsApp integration. Enforces performance-first patterns, efficient SQL, proper indexing, caching, and zero-waste architecture.
npx skillsauth add ricardoalejandro/clarin backend-developmentInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Every endpoint must be fast. Think sub-50ms for reads, sub-200ms for writes. Never accept slow queries, N+1 problems, or wasteful memory allocations. Profile mentally before coding.
docker compose build backendbackend/
cmd/server/main.go → Entry point, service initialization
internal/
api/server.go → HTTP handlers (Fiber), REST routes + WebSocket (~4300 lines)
domain/entities.go → Domain structs (Lead, Contact, Chat, Tag, etc.)
repository/repository.go → Data access layer (pgx/PostgreSQL)
service/service.go → Business logic
kommo/
client.go → Rate-limited Kommo API v4 HTTP client
sync.go → One-way Kommo → Clarin sync worker (5s polling)
whatsapp/device_pool.go → WhatsApp multi-device pool (whatsmeow)
ws/hub.go → WebSocket hub for real-time broadcasts
pkg/
config/config.go → Environment variables
database/database.go → DB connection + migrations (InitDB)
// ALWAYS parameterized — NEVER concatenate strings
row := db.QueryRow(ctx, `SELECT id, name FROM leads WHERE phone = $1 AND account_id = $2`, phone, accountID)
// SELECT only the columns you need — NEVER SELECT *
// BAD:
rows, _ := db.Query(ctx, `SELECT * FROM leads WHERE account_id = $1`, accountID)
// GOOD:
rows, _ := db.Query(ctx, `SELECT id, name, phone, status FROM leads WHERE account_id = $1`, accountID)
// Use LIMIT for paginated queries
rows, _ := db.Query(ctx, `SELECT id, name FROM leads WHERE account_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, accountID, limit, offset)
// Batch operations with single queries instead of loops
// BAD — N queries in a loop:
for _, tag := range tags {
db.Exec(ctx, `INSERT INTO lead_tags ...`, leadID, tag.ID)
}
// GOOD — single batch with unnest or VALUES:
db.Exec(ctx, `
INSERT INTO lead_tags (lead_id, tag_id)
SELECT $1, unnest($2::bigint[])
ON CONFLICT DO NOTHING
`, leadID, tagIDs)
// BAD — N+1: one query per lead to get tags
leads, _ := repo.GetLeads(ctx, accountID)
for _, lead := range leads {
lead.Tags, _ = repo.GetLeadTags(ctx, lead.ID) // N extra queries!
}
// GOOD — JOIN or subquery in a single query
rows, _ := db.Query(ctx, `
SELECT l.id, l.name, l.phone,
COALESCE(array_agg(t.name) FILTER (WHERE t.name IS NOT NULL), '{}') as tags
FROM leads l
LEFT JOIN lead_tags lt ON lt.lead_id = l.id
LEFT JOIN tags t ON t.id = lt.tag_id
WHERE l.account_id = $1
GROUP BY l.id
ORDER BY l.created_at DESC
`, accountID)
// ALTERNATIVE — batch load with IN clause
tagsByLead, _ := repo.GetTagsForLeadIDs(ctx, leadIDs) // single query with WHERE lead_id = ANY($1)
// When adding migrations, always consider indexes for:
// - Columns used in WHERE clauses
// - Columns used in JOIN conditions
// - Columns used in ORDER BY with large tables
// - Foreign keys
// In InitDB():
_, _ = db.Exec(ctx, `CREATE INDEX IF NOT EXISTS idx_leads_account_id ON leads(account_id)`)
_, _ = db.Exec(ctx, `CREATE INDEX IF NOT EXISTS idx_leads_phone ON leads(phone)`)
_, _ = db.Exec(ctx, `CREATE INDEX IF NOT EXISTS idx_messages_chat_id_created ON messages(chat_id, created_at DESC)`)
_, _ = db.Exec(ctx, `CREATE INDEX IF NOT EXISTS idx_chats_account_id ON chats(account_id)`)
// Composite indexes for common query patterns:
_, _ = db.Exec(ctx, `CREATE INDEX IF NOT EXISTS idx_leads_account_status ON leads(account_id, status)`)
// Cache expensive or frequently-accessed data
// Pattern: Check cache → miss → query DB → set cache
func (s *Service) GetLeadCached(ctx context.Context, accountID, leadID int64) (*domain.Lead, error) {
key := fmt.Sprintf("lead:%d:%d", accountID, leadID)
// Try cache first
cached, err := s.redis.Get(ctx, key).Result()
if err == nil {
var lead domain.Lead
json.Unmarshal([]byte(cached), &lead)
return &lead, nil
}
// Cache miss — query DB
lead, err := s.repo.GetLead(ctx, accountID, leadID)
if err != nil {
return nil, err
}
// Set cache with TTL
data, _ := json.Marshal(lead)
s.redis.Set(ctx, key, data, 5*time.Minute)
return lead, nil
}
// ALWAYS invalidate cache on write:
func (s *Service) UpdateLead(ctx context.Context, lead *domain.Lead) error {
err := s.repo.UpdateLead(ctx, lead)
if err != nil {
return err
}
s.redis.Del(ctx, fmt.Sprintf("lead:%d:%d", lead.AccountID, lead.ID))
return nil
}
// When you need multiple independent queries, run them concurrently
var (
leads []domain.Lead
contacts []domain.Contact
errG errgroup.Group
)
errG.Go(func() error {
var err error
leads, err = repo.GetLeads(ctx, accountID)
return err
})
errG.Go(func() error {
var err error
contacts, err = repo.GetContacts(ctx, accountID)
return err
})
if err := errG.Wait(); err != nil {
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
// Use pagination for all list endpoints
func (s *Server) handleGetLeads(c *fiber.Ctx) error {
accountID := c.Locals("account_id").(int64)
page := c.QueryInt("page", 1)
limit := c.QueryInt("limit", 50)
if limit > 200 {
limit = 200 // Cap maximum
}
offset := (page - 1) * limit
leads, total, err := s.repo.GetLeadsPaginated(ctx, accountID, limit, offset)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
return c.JSON(fiber.Map{
"data": leads,
"total": total,
"page": page,
"limit": limit,
})
}
// Omit empty/null fields with omitempty
type Lead struct {
ID int64 `json:"id"`
Name string `json:"name"`
Phone string `json:"phone,omitempty"`
Notes string `json:"notes,omitempty"`
}
// pgx pool is configured in database.go — don't create new connections
// Use the pool's context-aware methods:
db.QueryRow(ctx, ...) // single row
db.Query(ctx, ...) // multiple rows
db.Exec(ctx, ...) // commands (INSERT, UPDATE, DELETE)
// ALWAYS use context with timeout for external calls:
ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second)
defer cancel()
// Pre-allocate slices when you know the size
leads := make([]domain.Lead, 0, expectedCount)
// Use strings.Builder for string concatenation
var b strings.Builder
for _, tag := range tags {
b.WriteString(tag.Name)
b.WriteString(",")
}
// Close database rows ALWAYS
rows, err := db.Query(ctx, query, args...)
if err != nil {
return err
}
defer rows.Close()
// Scan directly into struct fields — no intermediate maps
for rows.Next() {
var lead domain.Lead
err := rows.Scan(&lead.ID, &lead.Name, &lead.Phone)
// ...
}
result, err := repo.GetLead(ctx, id)
if err != nil {
log.Printf("[API] Error getting lead %d: %v", id, err)
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
log.Printf("[API] Creating lead for account %d", accountID)
log.Printf("[SYNC] Syncing %d leads from Kommo", len(leads))
log.Printf("[WHATSAPP] Device %s connected", deviceID)
log.Printf("[WS] Broadcasting to account %d", accountID)
func (s *Server) handleGetLeads(c *fiber.Ctx) error {
accountID := c.Locals("account_id").(int64)
leads, err := s.repo.GetLeads(ctx, accountID)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
return c.JSON(leads)
}
import (
// stdlib
"context"
"fmt"
"log"
// third-party
"github.com/gofiber/fiber/v2"
"github.com/jackc/pgx/v5/pgxpool"
// internal
"clarin/internal/domain"
"clarin/internal/repository"
)
// ALWAYS use kommo.NormalizePhone() for phone numbers
normalized := kommo.NormalizePhone(rawPhone)
err checked and logged.context.Context passed to all DB/HTTP operations.domain/entities.go if neededrepository/repository.go — efficient query, proper indexesservice/service.go if needed — consider cachingapi/server.go — pagination, error handling, validationsetupRoutes()docker compose build backend && docker compose up -dAfter any operation that changes data visible to the frontend:
if s.hub != nil {
s.hub.BroadcastToAccount(accountID, ws.EventLeadUpdate, map[string]interface{}{
"action": "updated",
})
}
development
Enforce code quality, self-testing, and verification standards for Clarin CRM. Use this skill to ensure every change is compiled, deployed, and validated before presenting to the user. Acts as a senior engineer code review checklist.
tools
Work with Kommo CRM integration in Clarin. Use when modifying the sync worker, API client, lead/contact/tag synchronization, or phone normalization. Covers the one-way Kommo to Clarin sync flow.
development
Create beautiful, polished, and highly usable Next.js/React/TypeScript interfaces for Clarin CRM. Use when creating or modifying dashboard pages, components, API calls, WebSocket listeners, or UI styling. Enforces visual excellence, micro-interactions, accessibility, and the emerald/slate design system.
development
Make database schema changes for Clarin CRM. Use when adding tables, columns, indexes, or modifying the PostgreSQL schema. Migrations live in database.go InitDB() function.