Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/egeuysall/ryva-archive/llms.txt

Use this file to discover all available pages before exploring further.

Backend Architecture

The Ryva backend is a high-performance Go API built with clean architecture principles, following the handler → service → repository pattern for maintainable and testable code.

Technology Stack

Language & Router

  • Go 1.25 - Latest Go features
  • Chi v5 - Lightweight, composable router
  • CORS - Chi CORS middleware

Database

  • PostgreSQL - Primary database
  • pgx/v5 - High-performance PostgreSQL driver
  • Connection pooling - pgxpool
  • Migrations - SQL migration files

External Services

  • Supabase - Authentication (JWT validation)
  • Stripe - Payment processing
  • Resend - Transactional emails
  • Sentry - Error tracking

Development

  • Air - Hot reload for Go
  • godotenv - Environment variables
  • testify - Testing framework
  • golangci-lint - Code linting

Project Structure

apps/api/
├── cmd/
│   └── server/
│       └── main.go              # Application entry point
├── internal/                    # Private application code
│   ├── modules/                # Feature modules
│   │   ├── auth/              # Authentication module
│   │   │   ├── handler.go     # HTTP handlers
│   │   │   ├── service.go     # Business logic
│   │   │   ├── repository.go  # Data access
│   │   │   ├── models.go      # Domain models
│   │   │   └── dto.go         # Data transfer objects
│   │   ├── organizations/     # Organization management
│   │   ├── billing/           # Stripe integration
│   │   ├── waitlist/          # Waitlist management
│   │   └── stripe/            # Stripe webhook handling
│   ├── router/                # HTTP routing
│   │   ├── router.go          # Router setup
│   │   └── routes.go          # Route definitions
│   ├── shared/                # Shared utilities
│   │   ├── middleware/        # HTTP middleware
│   │   ├── database/          # Database utilities
│   │   ├── email/             # Email service
│   │   ├── apperrors/         # Error types
│   │   ├── httputil/          # HTTP helpers
│   │   └── logger/            # Logging utilities
│   ├── config/                # Configuration
│   └── db/                    # Generated database code (sqlc)
│       ├── auth/              # Auth queries
│       ├── organizations/     # Organization queries
│       └── billing/           # Billing queries
├── db/                         # Database files
│   ├── migrations/            # SQL migrations
│   │   ├── 001_initial.up.sql
│   │   └── 001_initial.down.sql
│   └── queries/               # SQL query definitions (sqlc)
│       ├── auth.sql
│       ├── organizations.sql
│       └── billing.sql
├── Dockerfile                  # Multi-stage Docker build
├── Makefile                    # Build automation
├── go.mod                      # Go module definition
└── .air.toml                   # Air hot reload config

Clean Architecture Pattern

Ryva follows a three-layer architecture: Handler → Service → Repository. Each layer has a specific responsibility and depends only on layers below it.

Architecture Layers

┌─────────────────────────────────────────┐
│            Handler Layer                │
│  • HTTP request/response handling       │
│  • Input validation (DTO)               │
│  • Auth context extraction              │
│  • Response formatting (JSON)           │
└───────────────┬─────────────────────────┘


┌─────────────────────────────────────────┐
│            Service Layer                │
│  • Business logic                       │
│  • Input validation (domain rules)      │
│  • Error handling                       │
│  • Transaction coordination             │
└───────────────┬─────────────────────────┘


┌─────────────────────────────────────────┐
│          Repository Layer               │
│  • Database queries                     │
│  • Data mapping (DB ↔ Domain)          │
│  • Connection management                │
│  • Query composition                    │
└─────────────────────────────────────────┘

Layer Responsibilities

Responsibilities:
  • Parse HTTP requests
  • Validate request DTOs
  • Extract authentication context
  • Call service methods
  • Format responses as JSON
  • Handle HTTP errors
Example (apps/api/internal/modules/auth/handler.go:1-175):
func (h *Handler) GetMe(w http.ResponseWriter, r *http.Request) {
    userID := middleware.GetUserID(r.Context())
    
    user, err := h.service.GetMe(r.Context(), userID)
    if err != nil {
        httputil.Error(w, r, err)
        return
    }
    
    httputil.Success(w, r, user)
}
Responsibilities:
  • Implement business logic
  • Validate domain rules
  • Coordinate multiple repositories
  • Handle transactions
  • Return domain errors
Example (apps/api/internal/modules/auth/service.go:24-31):
func (s *Service) GetMe(ctx context.Context, userID uuid.UUID) (*User, error) {
    user, err := s.repo.GetUserByID(ctx, userID)
    if err != nil {
        return nil, err
    }
    return user, nil
}
Responsibilities:
  • Execute database queries
  • Map database rows to domain models
  • Handle database errors
  • Use prepared statements
Example (apps/api/internal/modules/auth/repository.go:1-149):
func (r *Repository) GetUserByID(ctx context.Context, id uuid.UUID) (*User, error) {
    row := r.queries.GetUserByID(ctx, id)
    if err := row.Scan(&user); err != nil {
        if errors.Is(err, pgx.ErrNoRows) {
            return nil, apperrors.NotFound("user not found")
        }
        return nil, err
    }
    return &user, nil
}

Module Architecture

Each feature module follows a consistent structure:
internal/modules/auth/
├── handler.go      # HTTP handlers
├── service.go      # Business logic
├── repository.go   # Data access
├── models.go       # Domain models
├── dto.go          # Request/response DTOs
├── *_test.go       # Unit tests
└── README.md       # Module documentation

Example: Auth Module

handler.go

HTTP request handlers:
  • GetMe - Get current user
  • UpdateProfile - Update user profile
  • CompleteOnboarding - Mark onboarding complete
  • GetPreferences - Get user preferences
  • UpdatePreferences - Update preferences

service.go

Business logic:
  • Input validation
  • Business rule enforcement
  • Error handling
  • Coordination between repos

repository.go

Database operations:
  • GetUserByID
  • UpdateUserProfile
  • CompleteOnboarding
  • GetUserPreferences
  • UpdateUserPreferences

models.go

Domain models:
  • User - User entity
  • UserWithOrganizations
  • Helper methods
  • Validation logic

Creating a New Module

  1. Create module directory:
    mkdir -p internal/modules/myfeature
    
  2. Create handler.go:
    package myfeature
    
    type Handler struct {
        service *Service
    }
    
    func NewHandler(service *Service) *Handler {
        return &Handler{service: service}
    }
    
    func (h *Handler) HandleRequest(w http.ResponseWriter, r *http.Request) {
        // Handler implementation
    }
    
  3. Create service.go:
    package myfeature
    
    type Service struct {
        repo RepositoryInterface
    }
    
    func NewService(repo RepositoryInterface) *Service {
        return &Service{repo: repo}
    }
    
    func (s *Service) DoSomething(ctx context.Context) error {
        // Business logic
        return nil
    }
    
  4. Create repository.go:
    package myfeature
    
    type Repository struct {
        db *pgxpool.Pool
    }
    
    func NewRepository(db *pgxpool.Pool) *Repository {
        return &Repository{db: db}
    }
    
    func (r *Repository) GetData(ctx context.Context) error {
        // Database query
        return nil
    }
    
  5. Register routes in router:
    r.Route("/v1/myfeature", func(r chi.Router) {
        r.Use(authMiddleware)
        r.Get("/", handlers.MyFeature.HandleRequest)
    })
    

Router Architecture

The router (apps/api/internal/router/router.go:1-123) uses Chi for composable routing:

Middleware Stack

func ApplyStandardMiddleware(r *chi.Mux, config Config) {
    r.Use(middleware.RequestID)        // Add request ID
    r.Use(middleware.RealIP)           // Get real IP
    r.Use(middleware.Logger)           // Log requests
    r.Use(middleware.Recoverer)        // Recover from panics
    r.Use(middleware.CORS)             // CORS headers
    r.Use(middleware.Timeout(30s))     // Request timeout
}

Route Organization

func registerAPIRoutes(r *chi.Mux, authConfig AuthMiddleware, handlers Handlers) {
    authMiddleware := getAuthMiddleware(authConfig)
    
    r.Route("/v1", func(r chi.Router) {
        // Public routes
        r.Post("/waitlist", handlers.Waitlist.Join)
        
        // Protected routes
        r.Route("/auth", func(r chi.Router) {
            r.Use(authMiddleware)
            r.Get("/me", handlers.Auth.GetMe)
            r.Patch("/profile", handlers.Auth.UpdateProfile)
        })
        
        r.Route("/organizations", func(r chi.Router) {
            r.Use(authMiddleware)
            r.Post("/", handlers.Organizations.CreateOrganization)
            r.Get("/", handlers.Organizations.ListUserOrganizations)
        })
    })
}

Authentication & Authorization

JWT Validation Middleware

The auth middleware validates JWT tokens from Supabase:
func RequireAuth(config AuthConfig) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Extract token from Authorization header
            token := extractToken(r)
            
            // Validate JWT with Supabase
            claims, err := validateJWT(token, config)
            if err != nil {
                httputil.Unauthorized(w, r, "invalid token")
                return
            }
            
            // Add user ID to context
            ctx := context.WithValue(r.Context(), "user_id", claims.UserID)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

Extracting User ID

func GetUserID(ctx context.Context) uuid.UUID {
    userID, ok := ctx.Value("user_id").(uuid.UUID)
    if !ok {
        panic("user_id not found in context")
    }
    return userID
}

Error Handling

Error Types

Ryva defines custom error types in internal/shared/apperrors/:
package apperrors

type AppError struct {
    Code    string
    Message string
    Status  int
}

func (e *AppError) Error() string {
    return e.Message
}

// Error constructors
func NotFound(message string) *AppError {
    return &AppError{
        Code:    "NOT_FOUND",
        Message: message,
        Status:  http.StatusNotFound,
    }
}

func InvalidInput(message string) *AppError {
    return &AppError{
        Code:    "INVALID_INPUT",
        Message: message,
        Status:  http.StatusBadRequest,
    }
}

func Unauthorized(message string) *AppError {
    return &AppError{
        Code:    "UNAUTHORIZED",
        Message: message,
        Status:  http.StatusUnauthorized,
    }
}

func BusinessRuleViolation(message string) *AppError {
    return &AppError{
        Code:    "BUSINESS_RULE_VIOLATION",
        Message: message,
        Status:  http.StatusBadRequest,
    }
}

Error Response Format

func Error(w http.ResponseWriter, r *http.Request, err error) {
    var appErr *AppError
    if errors.As(err, &appErr) {
        w.WriteHeader(appErr.Status)
        json.NewEncoder(w).Encode(map[string]any{
            "success": false,
            "error": map[string]any{
                "code":    appErr.Code,
                "message": appErr.Message,
            },
        })
        return
    }
    
    // Unknown error - return 500
    w.WriteHeader(http.StatusInternalServerError)
    json.NewEncoder(w).Encode(map[string]any{
        "success": false,
        "error": map[string]any{
            "code":    "INTERNAL_ERROR",
            "message": "An internal error occurred",
        },
    })
}

Error Handling Best Practices

// Bad
user, _ := s.repo.GetUserByID(ctx, userID)

// Good
user, err := s.repo.GetUserByID(ctx, userID)
if err != nil {
    return nil, err
}
user, err := s.repo.GetUserByID(ctx, userID)
if err != nil {
    return nil, fmt.Errorf("failed to get user: %w", err)
}
if user == nil {
    return apperrors.NotFound("user not found")
}

if email == "" {
    return apperrors.InvalidInput("email is required")
}

Database Architecture

Connection Pooling

Ryva uses pgxpool for efficient connection management:
func NewPool(ctx context.Context, config Config) (*pgxpool.Pool, error) {
    poolConfig, err := pgxpool.ParseConfig(getDatabaseURL())
    if err != nil {
        return nil, err
    }
    
    // Configure pool
    poolConfig.MaxConns = 25
    poolConfig.MinConns = 5
    poolConfig.MaxConnLifetime = time.Hour
    poolConfig.MaxConnIdleTime = 30 * time.Minute
    
    // Disable prepared statements in development
    if config.IsDevelopment {
        poolConfig.ConnConfig.DefaultQueryExecMode = pgx.QueryExecModeSimpleProtocol
    }
    
    pool, err := pgxpool.NewWithConfig(ctx, poolConfig)
    if err != nil {
        return nil, err
    }
    
    // Test connection
    if err := pool.Ping(ctx); err != nil {
        return nil, err
    }
    
    return pool, nil
}

Migrations

Migrations are SQL files in db/migrations/:
-- 001_initial.up.sql
CREATE TABLE users (
    id UUID PRIMARY KEY,
    email TEXT NOT NULL UNIQUE,
    full_name TEXT,
    avatar_url TEXT,
    onboarding_completed BOOLEAN DEFAULT FALSE,
    preferences JSONB DEFAULT '{}',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX idx_users_email ON users(email);
-- 001_initial.down.sql
DROP TABLE IF EXISTS users;

Query Generation with sqlc

Queries are defined in db/queries/ and generated into type-safe Go code:
-- db/queries/auth.sql
-- name: GetUserByID :one
SELECT * FROM users WHERE id = $1;

-- name: UpdateUserProfile :one
UPDATE users
SET full_name = COALESCE($2, full_name),
    avatar_url = COALESCE($3, avatar_url),
    updated_at = NOW()
WHERE id = $1
RETURNING *;
Generated code (internal/db/auth/):
func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) {
    // Generated implementation
}

func (q *Queries) UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error) {
    // Generated implementation
}

Application Initialization

The main.go file (apps/api/cmd/server/main.go:34-217) orchestrates service initialization:
func main() {
    // 1. Load environment variables
    godotenv.Load()
    
    // 2. Initialize Sentry
    sentry.Init(sentry.ClientOptions{
        Dsn: os.Getenv("SENTRY_DSN"),
    })
    
    // 3. Connect to database
    dbPool, err := database.NewPool(ctx, database.Config{
        IsDevelopment: getEnvironment() == "development",
    })
    
    // 4. Initialize email service
    emailClient := email.NewClient(email.Config{...})
    
    // 5. Initialize modules
    waitlistRepo := waitlist.NewRepository(dbPool)
    waitlistService := waitlist.NewService(waitlistRepo, emailClient)
    waitlistHandler := waitlist.NewHandler(waitlistService)
    
    authRepo := auth.NewRepository(dbPool)
    authService := auth.NewService(authRepo)
    authHandler := auth.NewHandler(authService)
    
    // 6. Create router
    r := router.New(middlewareConfig, authConfig, router.Handlers{
        Waitlist: waitlistHandler,
        Auth: authHandler,
    })
    
    // 7. Start server
    server := &http.Server{
        Addr:    ":8080",
        Handler: r,
    }
    server.ListenAndServe()
}

Testing Strategy

Unit Tests

// auth/service_test.go
func TestGetMe(t *testing.T) {
    mockRepo := &MockRepository{}
    service := NewService(mockRepo)
    
    userID := uuid.New()
    expectedUser := &User{
        ID:    userID,
        Email: "test@example.com",
    }
    
    mockRepo.On("GetUserByID", mock.Anything, userID).Return(expectedUser, nil)
    
    user, err := service.GetMe(context.Background(), userID)
    
    assert.NoError(t, err)
    assert.Equal(t, expectedUser, user)
}

Integration Tests

func TestCreateOrganization(t *testing.T) {
    // Setup test database
    pool := setupTestDB(t)
    defer pool.Close()
    
    repo := NewRepository(pool)
    service := NewService(repo)
    
    // Test organization creation
    org, err := service.CreateOrganization(ctx, CreateOrgRequest{
        Name: "Test Org",
    })
    
    assert.NoError(t, err)
    assert.NotNil(t, org)
    assert.Equal(t, "Test Org", org.Name)
}

Best Practices

  • Never ignore errors
  • Always return and wrap errors
  • Use custom error types for business logic errors
  • Log errors with context
  • Always pass context.Context as the first parameter
  • Use context for cancellation and timeouts
  • Store request-scoped data in context (user ID, request ID)
  • Never store context in a struct
  • Validate all inputs at the handler level (DTOs)
  • Validate business rules at the service level
  • Return descriptive error messages
  • Use InvalidInput errors for validation failures
  • Use connection pooling (pgxpool)
  • Always use prepared statements in production
  • Use transactions for multi-step operations
  • Handle pgx.ErrNoRows explicitly
  • Keep handlers thin (just HTTP concerns)
  • Put business logic in services
  • Keep repositories focused on data access
  • Use interfaces for testability

Development Workflow

Local Development with Air

cd apps/api
make dev          # Starts Air hot reload
Air watches for file changes and automatically rebuilds and restarts the server.

Common Commands

make build        # Build binary
make test         # Run tests
make lint         # Run golangci-lint
make format       # Format code with gofmt

Docker Build

The Dockerfile (apps/api/Dockerfile:1-50) uses multi-stage builds:
# Build stage
FROM golang:1.25-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o api ./cmd/server/main.go

# Runtime stage
FROM alpine:3.20
RUN apk add --no-cache ca-certificates curl
RUN adduser -D -s /bin/sh appuser
USER appuser
WORKDIR /app
COPY --from=builder /app/api .
HEALTHCHECK --interval=30s --timeout=5s CMD curl -f http://localhost:8080/health || exit 1
CMD ["./api"]
Benefits:
  • Small final image (~20MB)
  • Security (runs as non-root user)
  • Health checks built-in
  • Fast builds with layer caching

Next Steps

Frontend Architecture

Learn about Next.js structure and state management

Infrastructure

Understand Docker, Caddy, and deployment setup

Build docs developers (and LLMs) love