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
Create module directory :
mkdir -p internal/modules/myfeature
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
}
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
}
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
}
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 ))
})
}
}
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 ,
}
}
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
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