Skip to main content

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