Skip to content

Latest commit

Β 

History

History
1321 lines (1015 loc) Β· 38.9 KB

File metadata and controls

1321 lines (1015 loc) Β· 38.9 KB

AGENTS.md - Standfor.me Development Guide

This document provides comprehensive guidance for AI coding agents working on the Standfor.me codebase. It covers project architecture, development workflows, coding conventions, and agent-specific guidelines.


Table of Contents

  1. Project Overview
  2. Repository Layout
  3. Backend Architecture
  4. Infrastructure & Services
  5. Development Workflow
  6. Configuration
  7. Testing
  8. API Documentation
  9. Coding Conventions & Agent Guidelines
  10. Roadmap Context

1. Project Overview

What is Standfor.me?

Standfor.me is a digital platform that transforms passive sympathy into visible, verified advocacy. It provides users with a centralized public profile where they can declare the social movements and causes they support, with a structured verification system that quantifies the depth of their commitment.

The Problem

Digital activism suffers from a "credibility gap":

  • Support for causes is fragmented across social media platforms
  • Performative activism ("slacktivism") is indistinguishable from genuine commitment
  • No standardized way to aggregate or verify advocacy across multiple dimensions

The Solution

Standfor.me creates an "advocacy portfolio" β€” a shareable public profile (username.standfor.me) that displays:

  • Movements a user supports
  • Verification badges proving depth of engagement
  • A reputation system based on demonstrated commitment

Verification Hierarchy

The platform uses a six-tier badge system to validate different levels of engagement:

Tier Name Validation Method
0 Self-Declared User publicly lists causes they align with
1 Bronze (Social Proof) Digital footprint: following orgs, using advocacy hashtags
2 Silver (Financial Proof) Charitable contributions, Patreon, donation receipts
3 Gold (Action Proof) Real-world engagement: rally attendance, volunteer hours, event check-ins
4 Platinum (Organization Vouched) Verified NGOs officially vouch for user as volunteer/ambassador/staff
5 Diamond Reserved for exceptional long-term commitment (future)

This hierarchy creates a trust layer for digital activism, allowing users to build a "resume for activism."


2. Repository Layout

standfor.me/
β”œβ”€β”€ AGENTS.md                 # This file - comprehensive agent guide
β”œβ”€β”€ README.md                 # Project overview and quick start
β”œβ”€β”€ Makefile                  # All build, test, lint, docker targets
β”œβ”€β”€ docker-compose.yaml       # Infrastructure stack (Postgres, Redis, Meilisearch, pgAdmin, RedisInsight)
β”œβ”€β”€ .env                      # Environment variables (database, redis, meilisearch secrets)
β”œβ”€β”€ .envrc                    # direnv configuration
β”œβ”€β”€ .goreleaser.yaml          # GoReleaser configuration for releases
β”œβ”€β”€ .gitignore
β”œβ”€β”€ skills-lock.json          # Agent skills configuration
β”‚
β”œβ”€β”€ backend/                  # Go backend API (chi router + PostgreSQL + Redis + Meilisearch)
β”‚   β”œβ”€β”€ cmd/                  # Application entrypoints
β”‚   β”‚   β”œβ”€β”€ api/              # Main HTTP API server
β”‚   β”‚   β”œβ”€β”€ migrate/          # Database migration CLI
β”‚   β”‚   β”œβ”€β”€ seed/             # Data seeding utility
β”‚   β”‚   └── worker/           # Asynq background worker
β”‚   β”‚       └── reindex/      # Meilisearch reindexing worker
β”‚   β”œβ”€β”€ configs/              # Configuration files
β”‚   β”‚   β”œβ”€β”€ config.yaml       # Base configuration
β”‚   β”‚   β”œβ”€β”€ config.development.yaml
β”‚   β”‚   β”œβ”€β”€ config.production.yaml
β”‚   β”‚   └── search.yaml       # Meilisearch index configurations
β”‚   β”œβ”€β”€ docs/                 # Swagger-generated API documentation
β”‚   β”‚   β”œβ”€β”€ docs.go
β”‚   β”‚   β”œβ”€β”€ swagger.json
β”‚   β”‚   └── swagger.yaml
β”‚   β”œβ”€β”€ migrations/           # SQL migration files (up/down pairs)
β”‚   β”œβ”€β”€ internal/             # Private application code
β”‚   β”‚   β”œβ”€β”€ config/           # Configuration loading (viper)
β”‚   β”‚   β”œβ”€β”€ domain/           # Domain types and errors
β”‚   β”‚   β”‚   β”œβ”€β”€ errors.go
β”‚   β”‚   β”‚   β”œβ”€β”€ user.go
β”‚   β”‚   β”‚   β”œβ”€β”€ organization.go
β”‚   β”‚   β”‚   β”œβ”€β”€ movement.go
β”‚   β”‚   β”‚   β”œβ”€β”€ category.go
β”‚   β”‚   β”‚   β”œβ”€β”€ movement_category.go
β”‚   β”‚   β”‚   β”œβ”€β”€ user_movement.go
β”‚   β”‚   β”‚   β”œβ”€β”€ token.go
β”‚   β”‚   β”‚   └── search/       # Search-specific domain types
β”‚   β”‚   β”œβ”€β”€ middleware/       # HTTP middleware stack
β”‚   β”‚   β”‚   β”œβ”€β”€ auth.go            # JWT authentication
β”‚   β”‚   β”‚   β”œβ”€β”€ rateLimit/         # Redis sliding-window rate limiter
β”‚   β”‚   β”‚   β”œβ”€β”€ cors.go
β”‚   β”‚   β”‚   β”œβ”€β”€ csp_nonce.go
β”‚   β”‚   β”‚   β”œβ”€β”€ security_headers.go
β”‚   β”‚   β”‚   β”œβ”€β”€ compress.go
β”‚   β”‚   β”‚   β”œβ”€β”€ timeout.go
β”‚   β”‚   β”‚   β”œβ”€β”€ payload_limit.go
β”‚   β”‚   β”‚   β”œβ”€β”€ recoverer.go
β”‚   β”‚   β”‚   β”œβ”€β”€ request_id.go
β”‚   β”‚   β”‚   └── canonical_logger.go
β”‚   β”‚   β”œβ”€β”€ pkg/              # Shared utilities
β”‚   β”‚   β”‚   β”œβ”€β”€ crypto/       # Password hashing (bcrypt)
β”‚   β”‚   β”‚   β”œβ”€β”€ jwt/          # JWT issuance and validation
β”‚   β”‚   β”‚   β”œβ”€β”€ logger/       # Structured logging helpers
β”‚   β”‚   β”‚   β”œβ”€β”€ pagination/   # Pagination utilities
β”‚   β”‚   β”‚   β”œβ”€β”€ requestid/    # Request ID context helpers
β”‚   β”‚   β”‚   β”œβ”€β”€ response/     # HTTP response helpers
β”‚   β”‚   β”‚   β”œβ”€β”€ validator/    # Custom validator (go-playground/validator)
β”‚   β”‚   β”‚   └── httputil/     # HTTP utilities
β”‚   β”‚   β”œβ”€β”€ repository/       # Data access layer
β”‚   β”‚   β”‚   β”œβ”€β”€ postgres/     # PostgreSQL repositories
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ connection.go
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ user_repository.go
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ movement_repository.go
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ movement_indexing.go
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ user_indexing.go
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ organization_indexing.go
β”‚   β”‚   β”‚   β”‚   └── refresh_token_repository.go
β”‚   β”‚   β”‚   β”œβ”€β”€ redis/        # Redis client and caching
β”‚   β”‚   β”‚   └── meilisearch/  # Meilisearch search repositories
β”‚   β”‚   β”‚       β”œβ”€β”€ client.go
β”‚   β”‚   β”‚       β”œβ”€β”€ filter_builder.go
β”‚   β”‚   β”‚       β”œβ”€β”€ user_repository.go
β”‚   β”‚   β”‚       β”œβ”€β”€ movement_repository.go
β”‚   β”‚   β”‚       └── organization_repository.go
β”‚   β”‚   β”œβ”€β”€ server/           # Chi router and HTTP handlers
β”‚   β”‚   β”‚   β”œβ”€β”€ server.go     # Server setup and middleware
β”‚   β”‚   β”‚   β”œβ”€β”€ routes.go     # Route definitions
β”‚   β”‚   β”‚   β”œβ”€β”€ handler.go    # Base handler utilities
β”‚   β”‚   β”‚   β”œβ”€β”€ auth_handler.go
β”‚   β”‚   β”‚   β”œβ”€β”€ user_handler.go
β”‚   β”‚   β”‚   β”œβ”€β”€ movement_handler.go
β”‚   β”‚   β”‚   β”œβ”€β”€ search_handler.go
β”‚   β”‚   β”‚   └── ratelimit.go
β”‚   β”‚   └── service/          # Business logic layer
β”‚   β”‚       β”œβ”€β”€ auth_service.go
β”‚   β”‚       β”œβ”€β”€ user_service.go
β”‚   β”‚       β”œβ”€β”€ movement_service.go
β”‚   β”‚       └── search/
β”‚   β”‚           β”œβ”€β”€ service.go       # Search orchestration
β”‚   β”‚           β”œβ”€β”€ interfaces.go    # Repository interfaces
β”‚   β”‚           └── index_data.go    # Indexing data structures
β”‚   β”œβ”€β”€ tmp/                  # Build output (gitignored)
β”‚   β”œβ”€β”€ go.mod / go.sum
β”‚   β”œβ”€β”€ .golangci.yml         # Linter configuration
β”‚   └── .air.toml             # Hot-reload configuration
β”‚
β”œβ”€β”€ storage/                  # Persistent volumes (gitignored)
β”‚   β”œβ”€β”€ postgres/             # PostgreSQL data
β”‚   β”œβ”€β”€ redis/                # Redis data
β”‚   └── meilisearch/          # Meilisearch index data
β”‚
└── cmd/                      # Root-level command scripts (if any)

3. Backend Architecture

Layered Architecture

The backend follows a clean architecture pattern with clear separation of concerns:

HTTP Request Flow:
  Client β†’ Middleware Stack β†’ Handler β†’ Service β†’ Repository β†’ Database/Search

Response Flow:
  Database/Search β†’ Repository β†’ Service β†’ Handler β†’ Middleware β†’ Client

Layer Responsibilities

Layer Package Responsibility
Entrypoint cmd/api, cmd/worker, cmd/migrate Application bootstrap, dependency injection
HTTP Handlers internal/server/ Request parsing, validation, response formatting
Business Logic internal/service/ Domain logic, orchestration, transaction coordination
Data Access internal/repository/ SQL queries, Meilisearch operations, Redis caching
Domain Types internal/domain/ Core entities, value objects, domain errors
Middleware internal/middleware/ Cross-cutting concerns (auth, rate limiting, logging)
Utilities internal/pkg/ Shared helpers (JWT, crypto, pagination, response)

Command Entrypoints (backend/cmd/)

cmd/api/main.go β€” HTTP API Server

The main application entrypoint. Responsibilities:

  • Load configuration from configs/ directory
  • Establish database connections (Postgres, Redis, Meilisearch)
  • Initialize repositories, services, and middleware
  • Configure chi router with middleware stack
  • Start HTTP server with graceful shutdown

When to use: Running the API locally or in production.

make run-dev          # Hot-reload during development
make build-api && ./backend/tmp/standfor-me  # Production binary

cmd/migrate/main.go β€” Database Migrations

CLI tool for running database migrations using golang-migrate/migrate.

Commands:

make migrate-up       # Apply all pending migrations
make migrate-down     # Rollback last migration
make migrate-create NAME=create_foo  # Create new migration pair

When to use: Schema changes, initial setup, CI/CD deployment.

cmd/seed/main.go β€” Data Seeding

Populates the database with initial/fake data for development and testing.

When to use: Fresh development environment setup, integration test data.

cmd/worker/main.go β€” Background Worker

Runs Asynq-based background jobs for async processing.

When to use: Email sending, notification processing, scheduled tasks.

cmd/worker/reindex/main.go β€” Meilisearch Reindexer

Dedicated worker for rebuilding Meilisearch indexes from PostgreSQL.

When to use:

  • Initial index population
  • Full reindex after schema changes
  • Recovery from index corruption
cd backend && go run ./cmd/worker/reindex

Domain Types (internal/domain/)

Core entities that model the business domain:

user.go β€” User Entity

type User struct {
    ID                uuid.UUID
    Username          string     // Unique, for profile URLs
    Email             string
    EmailVerifiedAt   *time.Time
    PasswordHash      *string
    DisplayName       string
    Bio               *string
    AvatarURL         *string
    Location          *string
    ProfileVisibility string     // "public", "private", "unlisted"
    EmbedEnabled      bool
    Role              string     // "user", "moderator", "admin", "superadmin"
    Status            string     // "active", "suspended", "banned", "deactivated"
    CreatedAt         time.Time
    UpdatedAt         time.Time
}

Key Constants:

  • ProfileVisibilityPublic, ProfileVisibilityPrivate, ProfileVisibilityUnlisted
  • RoleUser, RoleModerator, RoleAdmin, RoleSuperAdmin
  • StatusActive, StatusSuspended, StatusBanned, StatusDeactivated

organization.go β€” Organization Entity

Represents NGOs, non-profits, and advocacy groups that can vouch for users.

type Organization struct {
    ID               uuid.UUID
    Name             string
    Slug             string
    ShortDescription string
    LongDescription  string
    LogoURL          string
    CoverImageURL    string
    WebsiteURL       string
    ContactEmail     string
    EINTaxIDHash     string       // Hashed for privacy
    CountryCode      string
    Status           OrganizationStatus
    VerificationStatus VerificationStatus
    IsVerified       bool
    VerifiedAt       *time.Time
    SocialLinks      SocialLinks  // X, Bluesky, Instagram, etc.
    CreatedByUserID  uuid.UUID
    CreatedAt        time.Time
    UpdatedAt        time.Time
}

movement.go β€” Movement Entity

Represents social causes and movements users can support.

type Movement struct {
    ID              uuid.UUID
    Slug            string
    Name            string
    ShortDescription string
    LongDescription *string
    ImageURL        *string
    IconURL         *string
    WebsiteURL      *string
    SupporterCount  int
    TrendingScore   float64
    Status          string  // "draft", "active", "archived", "rejected", "pending_review"
    ClaimedByOrgID  *uuid.UUID
    CreatedByUserID *uuid.UUID
    CreatedAt       time.Time
    UpdatedAt       time.Time
}

errors.go β€” Domain Errors

Standardized error types for consistent error handling:

// Sentinel errors
var (
    ErrNotFound           = errors.New("resource not found")
    ErrConflict           = errors.New("resource already exists")
    ErrUnauthorized       = errors.New("unauthorized")
    ErrForbidden          = errors.New("forbidden")
    ErrInternal           = errors.New("internal server error")
    ErrValidation         = errors.New("validation failed")
    ErrRateLimit          = errors.New("rate limit exceeded")
    ErrInvalidCredentials = errors.New("invalid credentials")
)

// AppError wrapper for structured errors
type AppError struct {
    Op      string
    Message string
    Details map[string]string
    Cause   error
}

Constructor helpers:

  • NewNotFoundError(op, message)
  • NewValidationError(op, details)
  • NewUnauthorizedError(op, message)
  • NewConflictError(op, message)
  • NewInternalError(op, cause)

Middleware Stack

Middleware is applied in internal/server/server.go in this order:

s.router.Use(middleware.RequestID)                    // 1. Generate request ID
s.router.Use(middleware.Recoverer(s.logger))          // 2. Panic recovery
s.router.Use(s.globalRateLimiter())                   // 3. Global rate limiting
s.router.Use(middleware.PayloadLimit(...))            // 4. Max body size
s.router.Use(middleware.Compress)                     // 5. gzip compression
s.router.Use(middleware.CanonicalLogger(s.logger))    // 6. Structured logging
s.router.Use(middleware.CORS(...))                    // 7. CORS headers
s.router.Use(middleware.SecurityHeaders(...))         // 8. Security headers
s.router.Use(middleware.CSPNonce())                   // 9. CSP nonce
s.router.Use(middleware.Timeout(cfg.RequestTimeout))  // 10. Request timeout

Key Middleware

Middleware File Purpose
RequestID request_id.go Adds X-Request-ID header, stores in context
Recoverer recoverer.go Catches panics, logs stack trace, returns 500
globalRateLimiter ratelimit/ Redis sliding-window rate limiter
PayloadLimit payload_limit.go Rejects requests > 1MB by default
CanonicalLogger canonical_logger.go Logs all requests with timing and status
CORS cors.go Cross-origin request handling
SecurityHeaders security_headers.go HSTS, X-Frame-Options, X-Content-Type-Options
CSPNonce csp_nonce.go Content-Security-Policy with nonce
Timeout timeout.go Request timeout with context cancellation
Authenticate auth.go JWT validation, stores claims in context
RequireAuth auth.go Alias for Authenticate
RequireRole auth.go Role-based access control
RequireMinRole auth.go Minimum role level check

Utilities (internal/pkg/)

jwt/ β€” JWT Service

Handles access token issuance and validation.

type Service struct {
    secret          []byte
    accessTokenTTL  time.Duration  // 15m
    refreshTokenTTL time.Duration  // 168h (7 days)
    issuer          string         // "standfor.me"
}

func (s *Service) IssueAccessToken(user *domain.User) (string, error)
func (s *Service) ValidateAccessToken(raw string) (*domain.AccessTokenClaims, error)

Claims structure:

type AccessTokenClaims struct {
    UserID uuid.UUID
    Role   string
    Email  string
}

crypto/ β€” Password Hashing

Uses bcrypt for password hashing.

func HashPassword(password string) (string, error)
func VerifyPassword(hash, password string) bool

response/ β€” HTTP Response Helpers

Standardized JSON response formatting.

// Success responses
response.JSON(w, r, http.StatusOK, data)           // With data payload
response.JSONMessage(w, r, http.StatusOK, "OK")    // Message only

// Error responses (handlers should ONLY use this for errors)
response.JSONError(w, r, err)

Response envelope:

{
  "success": true,
  "data": { ... },
  "message": "..."
}

Error envelope:

{
  "success": false,
  "error": {
    "message": "...",
    "code": "not_found",
    "details": { "field": "error" }
  },
  "request_id": "..."
}

validator/ β€” Request Validation

Wraps go-playground/validator with custom rules.

type Validator struct {
    validate *validator.Validate
}

func (v *Validator) Validate(data any) map[string]string

Custom validators:

  • username: Alphanumeric, underscores, hyphens only
  • Standard: required, email, min, max, url, uuid, oneof

pagination/ β€” Pagination Helpers

Utilities for offset-based pagination.

requestid/ β€” Request ID Context

func GetRequestID(ctx context.Context) string

logger/ β€” Logging Helpers

Structured logging utilities.


Search Architecture

Standfor.me uses Meilisearch for full-text search across movements, users, and organizations.

Indexes (configs/search.yaml)

Three indexes are configured:

Index UID Purpose
movements movements Search movements by name, description, category
users users Search advocates by display name, username, bio
organizations organizations Search organizations by name, description

Filterable Attributes

Movements support filtering by depth-of-commitment metrics:

  • avg_verification_tier, min_verification_tier, max_verification_tier
  • min_badge_level_numeric, max_badge_level_numeric
  • supporter_count, trending_score
  • has_verified_org, claimed_by_org_id
  • category_ids, category_slugs

filter_builder.go β€” Query Builder

Fluent builder for constructing Meilisearch filter expressions:

filter := newFilterBuilder().
    addEquals("status", "active").
    addGTEFloat("avg_verification_tier", 2.0).
    addOptionalGTEInt64("supporter_count", req.MinSupporters).
    build()
// Result: "(status = \"active\") AND (avg_verification_tier >= 2.0) AND (supporter_count >= 100)"

Search Service (internal/service/search/)

Orchestrates search operations:

type Service struct {
    movements     MovementSearchRepo
    users         UserSearchRepo
    organizations OrganizationSearchRepo
    
    // Data repositories (Postgres) for indexing
    movementData MovementDataRepo
    userData     UserDataRepo
    orgData      OrgDataRepo
}

// Search methods
func (s *Service) SearchMovements(ctx, req) (*SearchResult[MovementDocument], error)
func (s *Service) SearchUsers(ctx, req) (*SearchResult[UserDocument], error)
func (s *Service) SearchOrganizations(ctx, req) (*SearchResult[OrganizationDocument], error)

// Indexing methods
func (s *Service) IndexMovement(ctx, movementID string) error
func (s *Service) IndexUser(ctx, userID string) error
func (s *Service) IndexOrganization(ctx, orgID string) error

Indexing Pipeline

  1. Real-time indexing: After create/update/delete in Postgres, call IndexX() to sync to Meilisearch
  2. Batch reindexing: Run cmd/worker/reindex to rebuild entire index from Postgres
  3. Data transformation: buildMovementDocument(), buildUserDocument(), buildOrgDocument() flatten PostgreSQL data into search documents

Database Migrations (backend/migrations/)

Migrations use golang-migrate/migrate with sequential numbering.

Current migrations:

File Purpose
000001_bootstrap.up.sql Initial schema setup
000002_create_users.up.sql Users table
000003_create_organizations.up.sql Organizations table
000004_create_categories.up.sql Categories table
000005_create_refresh_tokens.up.sql Refresh tokens table
000006_create_reserved_usernames.up.sql Reserved usernames table
000007_create_movements.up.sql Movements table
000008_create_movement_categories.up.sql Movement categories junction
000009_create_user_movements.up.sql User movements junction

Migration commands:

make migrate-up                    # Apply all pending
make migrate-down                  # Rollback last
make migrate-create NAME=add_foo   # Create 000010_add_foo.up.sql / .down.sql

4. Infrastructure & Services

Docker Compose Stack (docker-compose.yaml)

Service Image Port Purpose
postgres postgres:17-alpine 5432 Primary database
pgadmin dpage/pgadmin4 5050 Database administration UI
redis redis:8-alpine 6379 Caching, rate limiting, sessions
redisinsight redis/redisinsight:latest 5540 Redis visualization
meilisearch getmeili/meilisearch:latest 7700 Full-text search engine

Network: All services connected via standfor-net bridge network.

Volumes:

  • ./storage/postgres β†’ PostgreSQL data
  • ./storage/redis β†’ Redis data
  • ./storage/meilisearch β†’ Meilisearch index

Starting Infrastructure

# Start all services
make docker-up

# Start only core infrastructure (postgres + redis)
make docker-up-infra

# View logs
make docker-logs
make docker-logs-svc SVC=postgres

# Stop all
make docker-down

# Clean volumes
make docker-clean

Configuration Files (backend/configs/)

config.yaml β€” Base Configuration

app_env: development

server:
  host: localhost
  port: 8080
  read_timeout: 10s
  write_timeout: 15s
  shutdown_timeout: 30s
  request_timeout: 30s

database:
  host: localhost
  port: 5432
  user: postgres
  dbname: standfor_dev
  sslmode: disable
  max_open_conns: 10
  max_idle_conns: 5

redis:
  addr: localhost:6379
  pool_size: 10

jwt:
  access_token_ttl: 15m
  refresh_token_ttl: 168h
  issuer: standfor.me

rate_limit:
  global:
    limit: 100
    window: 1m

config.development.yaml / config.production.yaml β€” Environment Overrides

Environment-specific overrides merged on top of base config.

search.yaml β€” Meilisearch Configuration

Defines index settings (searchable attributes, filterable attributes, ranking rules, typo tolerance).


5. Development Workflow

Prerequisites

  • Go 1.25.5+
  • Docker & Docker Compose
  • Make

Quick Start

# 1. Install development tools
make install-tools

# 2. Start infrastructure
make docker-up-infra

# 3. Run migrations
make migrate-up

# 4. Seed test data (optional)
cd backend && go run ./cmd/seed

# 5. Start API with hot-reload
make run-dev

The API will be available at http://localhost:8080.

Common Make Targets

Target Description
make run-dev Hot-reload dev server (air)
make build Build API binary
make build-all Build API + migrate + worker binaries
make lint Run golangci-lint
make lint-fix Auto-fix lint issues
make format Format with gofumpt
make test Run all tests
make test-race Run tests with race detector
make test-one PKG=... NAME=... Run single test
make coverage-html Generate and open HTML coverage report
make swag Regenerate Swagger docs
make migrate-up Apply migrations
make migrate-create NAME=x Create new migration
make tidy Run go mod tidy
make all Full pipeline: tidy β†’ format β†’ lint β†’ test β†’ build
make sql-check Run SQL migration audit on all .sql files
make sql-check-ci Run SQL check with CI-friendly output
make sql-check-one FILE=x Check a single migration file
make sql-audit-report Generate comprehensive SQL audit report

Development Loop

  1. Code your changes
  2. Hot-reload automatically restarts the server (make run-dev)
  3. Test with make test or make test-one
  4. Format with make format
  5. Lint with make lint or make lint-fix
  6. Check SQL migrations with make sql-check (for new .sql files)
  7. Commit (pre-commit hooks run automatically)

SQL Migration Checking

The project includes a comprehensive SQL migration checker that performs strict static analysis on PostgreSQL .sql files. It detects:

  • πŸ”΄ CRITICAL: SQL injection vulnerabilities, missing migration pairs
  • 🟑 WARNING: Missing FK policies, missing indexes, non-idempotent statements, naming issues
  • πŸ”΅ INFO: Missing audit fields, text constraints, optimization suggestions
# Check all migrations
make sql-check

# Check a single file
make sql-check-one FILE=000009_create_user_movements.up.sql

# CI-friendly output format
make sql-check-ci

# Generate detailed audit report
make sql-audit-report

When to use:

  • Before committing new migration files
  • In CI/CD pipeline to block problematic migrations
  • When auditing existing migrations for technical debt
  • During code reviews of database schema changes

Example output:

🟑 WARNING: 000009_create_user_movements.up.sql:15 - Table name uses PascalCase, consider snake_case
🟑 WARNING: 000003_create_organizations.up.sql - Foreign key column 'verified_by_user_id' needs index
πŸ”΅ INFO: 000005_create_refresh_tokens.up.sql - Consider adding updated_at TIMESTAMPTZ with trigger

Reindexing Meilisearch

# Full reindex
cd backend && go run ./cmd/worker/reindex

# Or use make (if target exists)
make reindex

6. Configuration

Configuration Layering

Configuration is loaded in this order (later overrides earlier):

  1. configs/config.yaml β€” Base defaults
  2. configs/config.development.yaml β€” Development overrides (when APP_ENV=development)
  3. configs/config.production.yaml β€” Production overrides (when APP_ENV=production)
  4. Environment variables (highest priority)

Key Environment Variables

Variable Description Example
APP_ENV Environment name development, production
DATABASE_USER PostgreSQL username postgres
DATABASE_PASSWORD PostgreSQL password (see .env)
DATABASE_DBNAME Database name standfor_dev
DATABASE_PORT PostgreSQL port 5432
REDIS_PASSWORD Redis password (see .env)
REDIS_PORT Redis port 6379
MEILI_MASTER_KEY Meilisearch master key (see .env)
MEILI_HOST Meilisearch URL http://localhost:7700

Loading Configuration in Code

import "github.com/Yugsolanki/standfor-me/internal/config"

cfg, err := config.Load()
if err != nil {
    return err
}

// Access via cfg.Server, cfg.Database, cfg.Redis, cfg.JWT, etc.

7. Testing

Test Location

Tests live alongside source files with _test.go suffix:

  • internal/server/auth_handler_test.go
  • internal/service/auth_service_test.go
  • internal/middleware/auth_test.go

Test Patterns

Shared Database Setup (internal/repository/postgres/common_test.go)

Uses testcontainers-go to spin up ephemeral PostgreSQL containers for integration tests:

func getTestDB(t *testing.T) *sqlx.DB {
    // Starts postgres:17-alpine container
    // Runs migrations
    // Returns sqlx.DB connection
}

Table-Driven Tests

func TestExample(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        wantErr bool
    }{
        {"valid input", "test", false},
        {"invalid input", "", true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // test logic
        })
    }
}

Helper Functions

Use t.Helper() for test helpers:

func testRedisClient(t *testing.T) *redis.Client {
    t.Helper()
    // Setup logic
}

Skip When Dependencies Unavailable

if testing.Short() {
    t.Skip("skipping integration test")
}

Running Tests

# All tests
make test

# Verbose output
make test-verbose

# Race detector
make test-race

# Single test
make test-one PKG=./internal/middleware/ratelimit NAME=TestSlidingWindow

# Coverage
make coverage-html

8. API Documentation

Swagger Location

API documentation is auto-generated via Swag and stored in:

  • backend/docs/docs.go
  • backend/docs/swagger.json
  • backend/docs/swagger.yaml

Viewing Documentation

When the API is running:

  • Swagger UI: http://localhost:8080/swagger/index.html
  • JSON spec: http://localhost:8080/swagger/doc.json

Regenerating Documentation

make swag

This runs:

cd backend && swag init -g cmd/api/main.go -o docs --parseDependency --parseInternal

Note: Swag comments must be present on handlers for documentation to be complete.


9. Coding Conventions & Agent Guidelines

Import Organization

Group imports in this order (blank line between groups):

import (
    // 1. Standard library
    "context"
    "fmt"
    "log/slog"
    "net/http"
    "time"

    // 2. External packages
    "github.com/go-chi/chi/v5"
    "github.com/redis/go-redis/v9"
    "github.com/google/uuid"

    // 3. Internal packages
    "github.com/Yugsolanki/standfor-me/internal/config"
    "github.com/Yugsolanki/standfor-me/internal/domain"
    "github.com/Yugsolanki/standfor-me/internal/middleware"
)

Formatting

  • Use gofumpt (stricter go fmt)
  • Run make format before committing
  • golangci-lint auto-fixes formatting issues

Naming Conventions

Element Convention Example
Files snake_case rate_limit.go, auth_handler_test.go
Types/Functions PascalCase User, NewUserService
Variables/Methods camelCase userRepo, getUserByID
Constants PascalCase or SCREAMING_SNAKE_CASE StatusActive, ErrNotFound
Packages Short, lowercase, no underscores repo, service, middleware

Error Handling

Always handle errors explicitly β€” no _ discard:

// BAD
user, _ := h.service.GetUser(ctx, id)

// GOOD
user, err := h.service.GetUser(ctx, id)
if err != nil {
    return nil, fmt.Errorf("failed to get user: %w", err)
}

Use domain error constructors:

if err == postgres.ErrNotFound {
    return nil, domain.NewNotFoundError("user_repository.get_by_id", "user not found")
}

Wrap errors with context:

return nil, fmt.Errorf("failed to create limiter: %w", err)

Structured Logging

Use log/slog with structured attributes:

// GOOD
slog.Info("server started", "port", cfg.Server.Port)
slog.Error("connection failed", "error", err, "host", host)

// BAD
slog.Info(fmt.Sprintf("server started on port %d", cfg.Server.Port))

HTTP Handlers

Always use response helpers:

func (h *Handler) getUser(w http.ResponseWriter, r *http.Request) {
    user, err := h.service.GetUser(r.Context(), id)
    if err != nil {
        response.JSONError(w, r, err)  // ONLY function for error responses
        return
    }
    response.JSON(w, r, http.StatusOK, user)
}

Handler signature:

func (h *Handler) handlerName(w http.ResponseWriter, r *http.Request)

Adding New Domain Entities

When adding a new domain entity (e.g., Campaign), follow this order:

  1. Domain types (internal/domain/campaign.go)

    • Define struct, constants, param types
    • Add domain errors if needed
  2. Repository interfaces (internal/repository/postgres/campaign_repository.go)

    • Implement CRUD operations
    • Add indexing method if searchable
  3. Service layer (internal/service/campaign_service.go)

    • Business logic
    • Validation
    • Transaction coordination
  4. HTTP handlers (internal/server/campaign_handler.go)

    • Request parsing
    • Response formatting
    • Error handling
  5. Routes (internal/server/routes.go)

    • Add route definitions
    • Apply middleware
  6. Meilisearch integration (if searchable)

    • Add document type (internal/domain/search/document.go)
    • Update search.yaml
    • Implement indexing in service
  7. Tests

    • Unit tests for service
    • Integration tests for repository
    • Handler tests

Middleware Order (When Adding Routes)

When adding new middleware or routes, maintain this order:

r.Group(func(r chi.Router) {
    r.Use(middleware.RequireAuth(jwtSvc))            // Auth first
    r.Use(middleware.RequireMinRole(domain.RoleAdmin))  // Then role check
    // Add custom middleware here
    r.Get("/", handler)
})

Validation

Use struct tags for validation:

type CreateUserParams struct {
    Username string `validate:"required,min=3,max=30,username"`
    Email    string `validate:"required,email,max=255"`
    Password string `validate:"required,min=8,max=72"`
}

Validate in handlers:

var req CreateMovementRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
    response.JSONError(w, r, domain.NewBadRequestError("handler", "invalid JSON"))
    return
}

if errs := h.validator.Validate(&req); errs != nil {
    response.JSONError(w, r, domain.NewValidationError("handler", errs))
    return
}

Redis Usage

Use DB 15 for tests:

func testRedisClient(t *testing.T) *redis.Client {
    t.Helper()
    redisURL := os.Getenv("REDIS_URL")
    if redisURL == "" {
        redisURL = "localhost:6379"
    }
    // Use DB 15 for tests
    return redis.NewClient(&redis.Options{
        Addr:     redisURL,
        Password: os.Getenv("REDIS_PASSWORD"),
        DB:       15,
    })
}

Rate limiter prefix:

standfor:rl:global
standfor:rl:api:{endpoint}

Git Hooks

Pre-commit hook (.git/hooks/pre-commit) runs on staged Go files:

  1. gofumpt formatting check
  2. go vet static analysis
  3. Debug leftover scan (fmt.Print, TODO:, FIXME:, etc.)
  4. Module consistency check

Commit-msg hook (.git/hooks/commit-msg) validates conventional commits:

type(scope): description

Allowed types: feat, fix, docs, style, refactor, test, chore, ci, build, perf

Skip hooks: git commit --no-verify


10. Roadmap Context

Understanding the three-phase MVP plan helps agents prioritize work and avoid scope creep.

Phase 1 β€” Launch (Complete)

Core functionality for user profiles and movement browsing:

  • βœ… User registration and authentication (JWT-based)
  • βœ… User profiles with shareable links (username.standfor.me)
  • βœ… Movement database with browse/search
  • βœ… Add movements to personal list
  • βœ… Public profile page (embeddable)
  • βœ… Categories (Environment, Human Rights, etc.)
  • βœ… Meilisearch integration for full-text search
  • βœ… Basic admin panel (user management, movement moderation)

Status: Implemented and tested

Phase 2 β€” Verification (In Progress)

Verification badge system implementation:

  • πŸ”„ Verification badge data model
  • πŸ”„ Badge tier logic (Bronze β†’ Silver β†’ Gold β†’ Platinum)
  • πŸ”„ Proof upload endpoints (donation receipts, event check-ins)
  • πŸ”„ Organization vouching system
  • πŸ”„ Social proof verification (hashtag tracking, org following)
  • ⏳ AI verification engine (Python + Ollama) β€” future

Status: Data model in progress, verification logic pending

Phase 3 β€” Discovery (Future)

Social and discovery features:

  • ⏳ Find people with similar values
  • ⏳ Movement pages with supporter counts
  • ⏳ Trending movements algorithm
  • ⏳ Organization claimed pages
  • ⏳ Recommendation engine
  • ⏳ Activity feed

Status: Not started

Out of Scope (For Now)

  • Frontend (Vue.js in separate repository)
  • Mobile applications
  • Payment processing for donations
  • OAuth integrations (Twitter, Facebook, etc.)
  • Real-time notifications (WebSocket)
  • Advanced analytics dashboard

Appendix: Quick Reference

File Paths Cheat Sheet

Purpose Path
Main entrypoint backend/cmd/api/main.go
Routes backend/internal/server/routes.go
Server setup backend/internal/server/server.go
Domain types backend/internal/domain/*.go
Domain errors backend/internal/domain/errors.go
Auth middleware backend/internal/middleware/auth.go
Rate limiter backend/internal/middleware/ratelimit/
JWT service backend/internal/pkg/jwt/jwt.go
Response helpers backend/internal/pkg/response/response.go
User repository backend/internal/repository/postgres/user_repository.go
User service backend/internal/service/user_service.go
User handler backend/internal/server/user_handler.go
Migrations backend/migrations/
Config backend/configs/
Swagger backend/docs/
Tests *_test.go alongside source

Common Commands

# Development
make run-dev              # Hot-reload server
make docker-up-infra      # Start postgres + redis
make migrate-up           # Run migrations

# Code quality
make format               # Format code
make lint                 # Run linter
make lint-fix             # Auto-fix issues

# SQL Migration Checks
make sql-check            # Audit all SQL migrations
make sql-check-ci         # CI-friendly format
make sql-check-one FILE=x # Check single file

# Testing
make test                 # Run all tests
make test-race            # With race detector
make test-one PKG=... NAME=...  # Single test
make coverage-html        # Coverage report

# Build
make build                # Build API
make build-all            # Build all binaries
make swag                 # Regenerate Swagger docs

# Full pipeline
make all                  # tidy β†’ format β†’ lint β†’ test β†’ build

Health Check Endpoints

  • GET / β€” Root (returns JSON message)
  • GET /health β€” Health check
  • GET /swagger/* β€” Swagger UI

API Base Path

All API routes are prefixed with /api/v1:

  • POST /api/v1/auth/register
  • GET /api/v1/users/{username}
  • GET /api/v1/movements
  • GET /api/v1/search/movements

This document is maintained for AI coding agents. Update when architecture, conventions, or workflows change.