Skip to main content

Gateway Architecture

This document covers the internal architecture of the Gateway service, including the middleware stack, handler organization, and request lifecycle.

Component Overview

Middleware Stack

Middleware is applied in order, each adding functionality:
OrderMiddlewarePurpose
1CORSCross-origin request handling
2Request LoggerLog incoming requests
3RecoveryPanic recovery with logging
4Rate LimiterPer-client request throttling
5AuthAuthentication verification
6Audit LoggerOperation audit trail

Middleware Files

internal/middleware/
├── auth.go              # JWT validation
├── api_key_auth.go      # API key authentication
├── service_auth.go      # Internal service HMAC auth
├── hmac_auth.go         # POS HMAC authentication
├── partner_api_auth.go  # Partner API authentication
├── rate_limit.go        # Request rate limiting
├── cors.go              # CORS configuration
├── logging.go           # Request/response logging
├── recovery.go          # Panic recovery
├── audit.go             # Audit logging
├── roles.go             # Role-based authorization
└── combined_auth.go     # AllowServiceOrUser helper

Handler Organization

Handlers are organized by domain, each handling a specific resource type:

Handler Files

FileEndpointsCount
subscriber_handler.goRegister, lookup, update, block6
card_handler.goLink, block, unblock, upload6
wallet_handler.goBalance, payments, transfers5
agent_handler.goLookup, cashin, transfer5
pos_handler.goPayment, verify-card3
compliance_handler.goCheck, alerts, rules8
admin_handler.goLogin, users, settings10+
api_key_handler.goCreate, list, revoke keys4
audit_handler.goList, query audit logs3
processor_handler.goMerchant/processor ops6
webhook_handler.goVULT cashin webhook2
pep_access_handler.goPEP access flows2
fee_settings_handler.goFee configuration4
notifications.goPush notifications2
suspicious_transactions_handler.goFraud monitoring4

Request Lifecycle

Standard Request Flow

Authentication Flow

gRPC Client

The wallet client wraps gRPC calls to wallet-core:
// internal/wallet/client.go
type Client struct {
    conn       *grpc.ClientConn
    subscriber olive.SubscriberServiceClient
    card       olive.CardServiceClient
    wallet     wallet.WalletServiceClient
    agent      olive.AgentServiceClient
    compliance olive.ComplianceServiceClient
    // ... other service clients
}

func (c *Client) GetBalance(ctx context.Context, userID, currency string) (*Balance, error) {
    resp, err := c.wallet.GetBalance(ctx, &wallet.GetBalanceRequest{
        UserId:   userID,
        Currency: currency,
    })
    if err != nil {
        return nil, mapGRPCError(err)
    }
    return &Balance{
        UserID:   resp.UserId,
        Currency: resp.Currency,
        Balance:  resp.Balance,
    }, nil
}

Route Registration

Routes are registered in cmd/server/main.go:
func setupRoutes(r *gin.Engine, h *handler.Handler, mw *middleware.Middleware) {
    // Public routes
    r.GET("/health", h.HealthCheck)
    r.GET("/version", h.Version)
    
    // Public subscriber registration
    public := r.Group("/api/v1/public")
    {
        public.POST("/subscribers", h.RegisterSubscriberPublic)
    }
    
    // Protected routes
    api := r.Group("/api/v1")
    api.Use(mw.Auth())
    {
        // Subscribers
        api.POST("/subscribers", h.RegisterSubscriber)
        api.GET("/subscribers/lookup", h.LookupSubscriber)
        api.GET("/subscribers/:id", h.GetSubscriber)
        
        // Wallet
        api.GET("/balance/:user_id", h.GetBalance)
        api.POST("/payments", h.CreatePayment)
        api.GET("/transactions", h.ListTransactions)
        
        // ... more routes
    }
    
    // Admin routes
    admin := r.Group("/api/v1/admin")
    admin.Use(mw.JWT(), mw.RequireRole("system_admin"))
    {
        admin.GET("/users", h.ListAdminUsers)
        admin.POST("/api-keys", h.CreateAPIKey)
    }
    
    // POS routes with HMAC auth
    pos := r.Group("/pos")
    pos.Use(mw.HMACAuth())
    {
        pos.POST("/payment", h.POSPayment)
        pos.POST("/verify-card", h.POSVerifyCard)
    }
}

Error Handling

Standard Error Response

type ErrorResponse struct {
    Error   string      `json:"error"`
    Code    string      `json:"code,omitempty"`
    Details interface{} `json:"details,omitempty"`
}

func respondError(c *gin.Context, status int, err error) {
    var code string
    switch {
    case errors.Is(err, ErrNotFound):
        code = "NOT_FOUND"
    case errors.Is(err, ErrInsufficientBalance):
        code = "INSUFFICIENT_BALANCE"
    case errors.Is(err, ErrUnauthorized):
        code = "UNAUTHORIZED"
    default:
        code = "INTERNAL_ERROR"
    }
    
    c.JSON(status, ErrorResponse{
        Error: err.Error(),
        Code:  code,
    })
}

gRPC Error Mapping

func mapGRPCError(err error) error {
    st, ok := status.FromError(err)
    if !ok {
        return err
    }
    
    switch st.Code() {
    case codes.NotFound:
        return ErrNotFound
    case codes.InvalidArgument:
        return ErrBadRequest
    case codes.PermissionDenied:
        return ErrForbidden
    case codes.ResourceExhausted:
        return ErrRateLimited
    default:
        return ErrInternal
    }
}

Configuration Loading

// internal/config/config.go
type Config struct {
    Server     ServerConfig     `yaml:"server"`
    Database   DatabaseConfig   `yaml:"database"`
    WalletCore WalletCoreConfig `yaml:"wallet_core"`
    Auth       AuthConfig       `yaml:"auth"`
    RateLimit  RateLimitConfig  `yaml:"rate_limit"`
    Logging    LoggingConfig    `yaml:"logging"`
}

func Load(path string) (*Config, error) {
    cfg := &Config{}
    
    // Load YAML file
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, err
    }
    
    // Expand environment variables
    expanded := os.ExpandEnv(string(data))
    
    if err := yaml.Unmarshal([]byte(expanded), cfg); err != nil {
        return nil, err
    }
    
    // Apply overrides from environment
    applyEnvOverrides(cfg)
    
    return cfg, nil
}

Next Steps