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.
- Project Overview
- Repository Layout
- Backend Architecture
- Infrastructure & Services
- Development Workflow
- Configuration
- Testing
- API Documentation
- Coding Conventions & Agent Guidelines
- Roadmap Context
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.
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
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
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."
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)
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 | 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) |
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 binaryCLI 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 pairWhen to use: Schema changes, initial setup, CI/CD deployment.
Populates the database with initial/fake data for development and testing.
When to use: Fresh development environment setup, integration test data.
Runs Asynq-based background jobs for async processing.
When to use: Email sending, notification processing, scheduled tasks.
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/reindexCore entities that model the business domain:
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,ProfileVisibilityUnlistedRoleUser,RoleModerator,RoleAdmin,RoleSuperAdminStatusActive,StatusSuspended,StatusBanned,StatusDeactivated
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
}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
}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 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| 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 |
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
}Uses bcrypt for password hashing.
func HashPassword(password string) (string, error)
func VerifyPassword(hash, password string) boolStandardized 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": "..."
}Wraps go-playground/validator with custom rules.
type Validator struct {
validate *validator.Validate
}
func (v *Validator) Validate(data any) map[string]stringCustom validators:
username: Alphanumeric, underscores, hyphens only- Standard:
required,email,min,max,url,uuid,oneof
Utilities for offset-based pagination.
func GetRequestID(ctx context.Context) stringStructured logging utilities.
Standfor.me uses Meilisearch for full-text search across movements, users, and organizations.
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 |
Movements support filtering by depth-of-commitment metrics:
avg_verification_tier,min_verification_tier,max_verification_tiermin_badge_level_numeric,max_badge_level_numericsupporter_count,trending_scorehas_verified_org,claimed_by_org_idcategory_ids,category_slugs
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)"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- Real-time indexing: After create/update/delete in Postgres, call
IndexX()to sync to Meilisearch - Batch reindexing: Run
cmd/worker/reindexto rebuild entire index from Postgres - Data transformation:
buildMovementDocument(),buildUserDocument(),buildOrgDocument()flatten PostgreSQL data into search documents
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| 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
# 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-cleanapp_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: 1mEnvironment-specific overrides merged on top of base config.
Defines index settings (searchable attributes, filterable attributes, ranking rules, typo tolerance).
- Go 1.25.5+
- Docker & Docker Compose
- Make
# 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-devThe API will be available at http://localhost:8080.
| 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 |
- Code your changes
- Hot-reload automatically restarts the server (
make run-dev) - Test with
make testormake test-one - Format with
make format - Lint with
make lintormake lint-fix - Check SQL migrations with
make sql-check(for new .sql files) - Commit (pre-commit hooks run automatically)
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-reportWhen 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
# Full reindex
cd backend && go run ./cmd/worker/reindex
# Or use make (if target exists)
make reindexConfiguration is loaded in this order (later overrides earlier):
configs/config.yamlβ Base defaultsconfigs/config.development.yamlβ Development overrides (whenAPP_ENV=development)configs/config.production.yamlβ Production overrides (whenAPP_ENV=production)- Environment variables (highest priority)
| 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 |
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.Tests live alongside source files with _test.go suffix:
internal/server/auth_handler_test.gointernal/service/auth_service_test.gointernal/middleware/auth_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
}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
})
}
}Use t.Helper() for test helpers:
func testRedisClient(t *testing.T) *redis.Client {
t.Helper()
// Setup logic
}if testing.Short() {
t.Skip("skipping integration test")
}# 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-htmlAPI documentation is auto-generated via Swag and stored in:
backend/docs/docs.gobackend/docs/swagger.jsonbackend/docs/swagger.yaml
When the API is running:
- Swagger UI:
http://localhost:8080/swagger/index.html - JSON spec:
http://localhost:8080/swagger/doc.json
make swagThis runs:
cd backend && swag init -g cmd/api/main.go -o docs --parseDependency --parseInternalNote: Swag comments must be present on handlers for documentation to be complete.
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"
)- Use
gofumpt(strictergo fmt) - Run
make formatbefore committing - golangci-lint auto-fixes formatting issues
| 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 |
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)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))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)When adding a new domain entity (e.g., Campaign), follow this order:
-
Domain types (
internal/domain/campaign.go)- Define struct, constants, param types
- Add domain errors if needed
-
Repository interfaces (
internal/repository/postgres/campaign_repository.go)- Implement CRUD operations
- Add indexing method if searchable
-
Service layer (
internal/service/campaign_service.go)- Business logic
- Validation
- Transaction coordination
-
HTTP handlers (
internal/server/campaign_handler.go)- Request parsing
- Response formatting
- Error handling
-
Routes (
internal/server/routes.go)- Add route definitions
- Apply middleware
-
Meilisearch integration (if searchable)
- Add document type (
internal/domain/search/document.go) - Update
search.yaml - Implement indexing in service
- Add document type (
-
Tests
- Unit tests for service
- Integration tests for repository
- Handler tests
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)
})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
}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}
Pre-commit hook (.git/hooks/pre-commit) runs on staged Go files:
gofumptformatting checkgo vetstatic analysis- Debug leftover scan (
fmt.Print,TODO:,FIXME:, etc.) - 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
Understanding the three-phase MVP plan helps agents prioritize work and avoid scope creep.
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
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
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
- Frontend (Vue.js in separate repository)
- Mobile applications
- Payment processing for donations
- OAuth integrations (Twitter, Facebook, etc.)
- Real-time notifications (WebSocket)
- Advanced analytics dashboard
| 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 |
# 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 β buildGET /β Root (returns JSON message)GET /healthβ Health checkGET /swagger/*β Swagger UI
All API routes are prefixed with /api/v1:
POST /api/v1/auth/registerGET /api/v1/users/{username}GET /api/v1/movementsGET /api/v1/search/movements
This document is maintained for AI coding agents. Update when architecture, conventions, or workflows change.