Skip to content
turahePublic

About

A collection of production-ready Go packages for building web services: database, Redis (standard + cluster), JWT, crypto, object storage, structured logging, HTTP middleware, OpenTelemetry (otelx), Sentry (sentryx), mutual TLS (mtls), Prometheus metrics, graceful shutdown, and utilities.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

97 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Go Package Collection

Go Reference Test Security codecov Go Report Card Go Version Release License: MIT

A collection of production-ready Go packages for building web services: database, Redis (standard + cluster), JWT, crypto, object storage, structured logging, HTTP middleware, OpenTelemetry (otelx), Sentry (sentryx), mutual TLS (mtls), Prometheus metrics, graceful shutdown, and utilities. Follows clean architecture boundaries — domain, use-case, and infrastructure are separate.

Contents

Full guides: [docs/](docs/README.md) — getting started, architecture, environment reference, and per-package docs.


Installation

go get github.com/turahe/pkg

Requires Go 1.26+.


Packages

config

Loads all configuration from environment variables (or a .env file via godotenv). All packages read from the global Configuration singleton.

API:

config.Setup(configPath string) error   // load .env; "" uses env vars only
config.GetConfig() *Configuration       // get global config (builds from env if needed)
config.SetConfig(cfg *Configuration)    // override config (testing / manual wiring)

Configuration struct (abbreviated):

type Configuration struct {
    Server      ServerConfiguration
    Cors        CorsConfiguration
    Database    DatabaseConfiguration
    DatabaseSite DatabaseConfiguration  // optional second DB
    Redis       RedisConfiguration
    Storage     StorageConfiguration
    RateLimiter RateLimiterConfiguration
    Timezone    TimezoneConfiguration
    OpenTelemetry OpenTelemetryConfiguration
    Sentry        SentryConfiguration
    MTLS          MTLSConfiguration
}

database

Enterprise-grade GORM database layer. Supports MySQL, Postgres, SQL Server, and Google Cloud SQL (Postgres/MySQL) with IAM auth and Private IP. Includes SQL redaction in logs (passwords, tokens, card numbers).

Drivers: mysql · postgres · sqlserver · cloudsql-mysql · cloudsql-postgres

New API (recommended — dependency injection):

db, err := database.New(&cfg.Database, database.Options{
    LogLevel: logger.Warn,
})
// or with Cloud SQL options:
db, err := database.New(&cfg.Database, database.Options{
    UseIAM:       true,
    UsePrivateIP: true,
    LogLevel:     logger.Warn,
})

gormDB  := db.DB()                    // *gorm.DB
err     = db.Health(ctx)             // ping with PingTimeout-bounded context
err     = db.Close()                  // close connections + Cloud SQL connector

Options:

Option Default Description
MaxOpenConns 30 Max open DB connections
MaxIdleConns 10 Max idle DB connections
ConnMaxLife 30 min Max connection lifetime
ConnMaxIdle 10 min Max connection idle time
SlowThreshold 500 ms GORM slow query log threshold
PingTimeout 5 s Health check ping timeout
LogLevel Warn GORM log level
UseIAM false Cloud SQL IAM auth
UsePrivateIP false Cloud SQL Private IP
EnableOpenTelemetry auto GORM OTel plugin; auto when tracing enabled (OTEL_GORM_ENABLED)

Connection timezone: set DATABASE_TIMEZONE (IANA name) or leave empty to use SERVER_TIMEZONE (default UTC). Applied to MySQL loc, Postgres timezone, and Cloud SQL drivers.

OpenTelemetry GORM: when otelx.Init succeeds (OTLP or GCP exporter), GORM spans are registered automatically unless disabled with database.WithOpenTelemetry(false).

High-RPS pool preset:

db, err := database.New(&cfg.Database, database.Options{}, database.WithProductionPoolDefaults)
// sets MaxOpenConns=150, MaxIdleConns=50

Legacy API (global, backward-compatible):

database.Setup()          // init from config env vars
database.GetDB()          // *gorm.DB (panics if not initialized)
database.GetDBSite()      // secondary *gorm.DB (falls back to primary)
database.HealthCheck(ctx) // check both primary and site DB
database.IsAlive()        // bool
database.Cleanup()        // close all connections

redis

Redis client wrapper for both standalone Redis and Redis Cluster (Google Cloud Memorystore). Connection pooling, timeouts, and all common commands included.

Setup:

redis.Setup() error          // reads config; no-op if REDIS_ENABLED=false
redis.Close() error          // close client; call on shutdown
redis.IsAlive() bool         // ping check
redis.GetUniversalClient() redis.Cmdable  // works with both modes
redis.GetRedis() *redis.Client            // standard mode only
redis.GetRedisCluster() *redis.ClusterClient  // cluster mode only

String operations:

redis.Get(key string) (string, error)
redis.Set(key string, value interface{}, expiration time.Duration) error
redis.Delete(keys ...string) error
redis.MGet(keys ...string) ([]interface{}, error)
redis.MSet(pairs ...interface{}) error

Hash operations:

redis.HGet(key, field string) (string, error)
redis.HGetAll(key string) (map[string]string, error)
redis.HSet(key, field string, value interface{}) error
redis.HSetMap(key string, fields map[string]interface{}) error

List operations:

redis.LPush(key string, values ...interface{}) error
redis.RPop(key string) (string, error)
redis.LRange(key string, start, stop int64) ([]string, error)

Set operations:

redis.SAdd(key string, members ...interface{}) error
redis.SMembers(key string) ([]string, error)
redis.SRem(key string, members ...interface{}) error

Distributed lock:

redis.AcquireLock(key string, expiration time.Duration) (bool, error)
redis.ExtendLock(key string, expiration time.Duration) (bool, error)
redis.ReleaseLock(key string) error

Pipeline, Pub/Sub, Scan:

redis.Pipeline(fn func(redis.Pipeliner) error) error
redis.PipelineSet(pairs map[string]interface{}, expiration time.Duration) error
redis.PublishMessage(channel string, message interface{}) error
redis.SubscribeToChannel(channel string) *redis.PubSub
redis.ScanKeys(pattern string, count int64) ([]string, error)  // cluster-aware

logger

Structured logging built on log/slog. Outputs Google Cloud Logging-compatible JSON with severity, time, message, trace_id, correlation_id, sourceLocation, and optional fields. Logs to stderr (no log file). The underlying writer is lazy-initialized on first log write (no I/O at startup).

Plain functions:

logger.Debugf(format string, args ...interface{})
logger.Infof(format string, args ...interface{})
logger.Warnf(format string, args ...interface{})
logger.Errorf(format string, args ...interface{})
logger.Fatalf(format string, args ...interface{})  // calls os.Exit(1)

Structured fields:

logger.Debug(msg string, fields logger.Fields)
logger.Info(msg string, fields logger.Fields)
logger.Warn(msg string, fields logger.Fields)
logger.Error(msg string, fields logger.Fields)

Context-bound (includes trace_id / correlation_id automatically):

// Store IDs in context (done by RequestID / TraceMiddleware):
ctx = logger.WithTraceID(ctx, traceID)
ctx = logger.WithCorrelationID(ctx, correlationID)

// Read IDs from context:
logger.GetTraceID(ctx)
logger.GetCorrelationID(ctx)

// Context-bound logger — all log calls carry the IDs from ctx:
log := logger.WithContext(ctx)
log.Infof("user %s logged in", userID)
log.Errorf("operation failed: %v", err)

// Or use context-aware free functions:
logger.InfofContext(ctx, "processed %d records", n)
logger.ErrorfContext(ctx, "failed: %v", err)

Configuration:

logger.SetLogLevel(slog.LevelDebug)  // default: Info
logger.GetLogger() *slog.Logger
logger.GetWriter() io.Writer         // stderr (used e.g. by GORM logger)

middlewares

Gin middleware collection. Designed to be composed in a specific order for correct behaviour.

Recommended middleware order:

router := gin.New()

// Create JWT verifier (Manager for sign+verify, or Verifier for verify-only)
jwtManager, err := jwt.NewManager(context.Background(), config.GetConfig())
if err != nil {
    log.Fatal(err)
}
// Or for API-only services: jwtVerifier, _ := jwt.NewVerifier(ctx, config.GetConfig())

router.Use(
    middlewares.RecoveryHandler,              // 1. catch panics first
    middlewares.RequestID(),                  // 2. inject trace/correlation IDs
    middlewares.LoggerMiddleware(),            // 3. log with IDs in context
    middlewares.Metrics(),                    // 4. Prometheus instrumentation
    middlewares.RequestTimeout(10*time.Second), // 5. bound all downstream handlers
    middlewares.CORS(),                       // 6. CORS headers (incl. X-2FA-Code)
    middlewares.MTLSMiddleware(),           // 7. client cert check (when MTLS_ENABLED)
    middlewares.AuthMiddleware(jwtManager),   // 8. JWT auth (pass *Manager or *Verifier)
    middlewares.RateLimiter(),                // 9. rate limit (requires Redis)
)
router.NoMethod(middlewares.NoMethodHandler())
router.NoRoute(middlewares.NoRouteHandler())

Reference:

Middleware Signature Description
RecoveryHandler gin.HandlerFunc Catches panics; returns structured JSON 500 with stack trace in logs
RequestID() gin.HandlerFunc Reads X-Request-ID / X-Trace-ID or generates UUID; injects into context and response headers
TraceMiddleware() gin.HandlerFunc Reads X-Trace-Id, X-Correlation-Id, X-Request-Id separately; use when upstream sends distinct trace and correlation IDs
LoggerMiddleware() gin.HandlerFunc Structured request log (method, path, status, latency, IP, user-agent, trace IDs)
Metrics() gin.HandlerFunc Prometheus counters, histogram, and in-flight gauge; uses route pattern to avoid high-cardinality labels
RequestTimeout(d) gin.HandlerFunc Adds context.WithTimeout to every request; no-op when d <= 0
CORS() gin.HandlerFunc CORS headers; global or per-origin (CORS_FRONTEND / CORS_IPS) from config
AuthMiddleware(verifier) jwt.TokenVerifier → gin.HandlerFunc Validates Bearer JWT; sets user_id, original_user_id, is_impersonating in context. Pass *jwt.Manager or *jwt.Verifier.
MTLSMiddleware() gin.HandlerFunc Requires verified TLS client cert when MTLS_ENABLED; sets mtls_client_cn in context
RateLimiter() gin.HandlerFunc Redis Lua single-round-trip rate limiter; sets X-RateLimit-* headers; supports IP or user keying and skip-paths
NoMethodHandler() gin.HandlerFunc 405 JSON response
NoRouteHandler() gin.HandlerFunc 404 JSON response

Prometheus metrics exposed by Metrics():

Metric Type Labels
http_requests_total Counter method, path, status
http_request_duration_seconds Histogram method, path, status
http_requests_in_flight Gauge —

Register the scrape endpoint separately:

import "github.com/prometheus/client_golang/prometheus/promhttp"
router.GET("/metrics", gin.WrapH(promhttp.Handler()))

otelx

OpenTelemetry trace setup wired to config.OpenTelemetry. Supports OTLP HTTP or OTLP gRPC.

import "github.com/turahe/pkg/otelx"

shutdown, enabled := otelx.Init(ctx, config.GetConfig().OpenTelemetry)
if enabled {
    defer shutdown(ctx)
}

// GORM instrumentation (auto when tracing enabled + OTEL_GORM_ENABLED=true)
db, _ := database.New(&cfg.Database, database.Options{})

// gRPC instrumentation (after Init)
srv := grpc.NewServer(otelx.GRPCServerOption())
conn, _ := grpc.NewClient(target, otelx.GRPCDialOption(), /* credentials */)

API:

Function Description
otelx.LoadConfig() Returns config.OpenTelemetryConfiguration
otelx.Init(ctx, cfg) Configures global TracerProvider; returns shutdown func + enabled flag
otelx.RegisterGORM(db, opts) Manual GORM OTel plugin registration
otelx.TracingEnabled(cfg) True when an OTLP endpoint is configured
otelx.GRPCServerOption(...) otelgrpc stats handler for grpc.NewServer
otelx.GRPCDialOption(...) otelgrpc stats handler for grpc.NewClient / Dial

OTLP gRPC: set OTEL_TRACES_EXPORTER=otlp_grpc or OTEL_EXPORTER_OTLP_PROTOCOL=grpc (endpoint typically localhost:4317).


sentryx

Sentry error reporting wired to config.Sentry. Empty SENTRY_DSN disables Sentry with zero overhead.

import "github.com/turahe/pkg/sentryx"

if sentryx.Init(config.GetConfig().Sentry) {
    defer sentryx.Flush(config.GetConfig().Sentry.FlushTimeout)
}

srv := grpc.NewServer(sentryx.ServerOptions()...)
conn, _ := grpc.NewClient(target, append(sentryx.DialOptions(), /* credentials */)...)
Function Description
sentryx.LoadConfig() Returns config.SentryConfiguration
sentryx.Init(cfg) Initializes Sentry; returns enabled flag
sentryx.Flush(timeout) Drains buffered events on shutdown
sentryx.ServerOptions(...) Unary + stream Sentry gRPC server interceptors
sentryx.DialOptions() Unary + stream Sentry gRPC client interceptors

mtls

Mutual TLS helpers for backend HTTPS (require client cert) and gateway outbound connections. Config from config.MTLS.

Server (backend):

import "github.com/turahe/pkg/mtls"

srv := &http.Server{Addr: ":" + cfg.Server.Port, Handler: router}
_ = mtls.ConfigureServer(srv)
go mtls.ListenConfigured(srv)

router.Use(middlewares.MTLSMiddleware()) // HTTP-layer client cert enforcement

Client (gateway outbound):

transport, err := mtls.NewTransport(config.GetConfig().MTLS)
client := &http.Client{Transport: transport}
Function Description
mtls.LoadConfig() Returns config.MTLSConfiguration
mtls.ServerTLSConfig(cfg) TLS config with RequireAndVerifyClientCert
mtls.ClientTLSConfig(cfg) TLS config with client cert for outbound calls
mtls.ConfigureServer(srv) Applies server TLS when MTLS_ENABLED
mtls.ListenConfigured(srv) ListenAndServeTLS or plain HTTP
mtls.RunGin(engine, addr) Gin drop-in for engine.Run with optional mTLS
mtls.NewTransport(cfg) http.Transport for gateway upstream HTTPS

When MTLS_ENABLED=false, all helpers fall back to plain HTTP.


handler

Base handler for Gin with binding, pagination, error routing, and role checks.

type BaseHandler struct{}

// Bind request body / query / URI by content-type and method.
func (c *BaseHandler) ValidateReqParams(ctx *gin.Context, params interface{}) error

// 422 validation error response (Laravel-style field map).
func (c *BaseHandler) HandleValidationError(ctx *gin.Context, serviceCode string, err error)

// Clamp page/size to defaults (1, 10) and max (100).
func (c *BaseHandler) NormalizePagination(pageNumber, pageSize int) (int, int)

// Extract and validate URL param; writes 422 and returns false if missing.
func (c *BaseHandler) GetIDFromParam(ctx *gin.Context, paramName, serviceCode string) (string, bool)

// Prefer reqID; fall back to URL param.
func (c *BaseHandler) GetIDFromRequestOrParam(ctx *gin.Context, reqID, paramName, serviceCode string) (string, bool)

// Route domain errors to correct HTTP status. Returns true if handled.
// Checks errors.Is(ErrNotFound) → 404, errors.Is(ErrUnauthorized) → 401,
// then notFoundMessages list, then falls back to 500.
func (c *BaseHandler) HandleServiceError(ctx *gin.Context, serviceCode string, err error, notFoundMessages ...string) bool

// Build SimplePaginationResponse from a slice, page info, and total.
func (c *BaseHandler) BuildPaginationResponse(data []interface{}, pageNumber, pageSize int, total int64) response.SimplePaginationResponse

// Read "user_id" string from Gin context (set by AuthMiddleware).
func (c *BaseHandler) GetCurrentUserID(ctx *gin.Context) (string, bool)

// Read "actor_type" string from Gin context (set by AuthMiddleware).
func (c *BaseHandler) GetActorType(ctx *gin.Context) (string, bool)

// Return true if any userRole matches any requiredRole.
func (c *BaseHandler) CheckUserHasRole(userRoles, requiredRoles []string) bool

response

Standardised JSON responses with a 7-digit composite code (HTTP(3) + Service(2) + Case(2)).

Success responses:

response.Ok(ctx)
response.OkWithMessage(ctx, message)
response.OkWithData(ctx, data)
response.Created(ctx, data)
response.Updated(ctx, data)
response.Deleted(ctx)

Error responses:

response.FailWithMessage(ctx, message)
response.FailWithDetailed(ctx, httpStatus, serviceCode, caseCode, data, message)
response.ValidationError(ctx, serviceCode, err)          // 422 with field map
response.ValidationErrorSimple(ctx, serviceCode, field, message)
response.NotFoundError(ctx, serviceCode, caseCode, message)   // 404
response.UnauthorizedError(ctx, message)                       // 401
response.ForbiddenError(ctx, message)                          // 403
response.ConflictError(ctx, serviceCode, caseCode, message)    // 409

Pagination responses:

response.SimplePaginated(ctx, data, pageNumber, pageSize, hasNext, hasPrev)
response.CursorPaginated(ctx, data, nextCursor, hasNext)

Service codes: ServiceCodeCommon, ServiceCodeAuth, ServiceCodeTransaction, ServiceCodeWithdrawal, ServiceCodeUser, ServiceCodeAdmin, ServiceCodeMerchant, ServiceCodeSetting, ServiceCodeRole, ServiceCodePermission, ServiceCodeNotification, ServiceCodeIPWhitelist, ServiceCodeApiKey, ServiceCodeDeposit, ServiceCodeWallet, ServiceCodePhone, ServiceCodeEmail, ServiceCodeTwoFactor, ServiceCodeBank, ServiceCodeBankAccount, and more. Case codes cover success, validation, auth, not-found, business logic, server errors, conflicts, email/phone change, 2FA, and bank/bank-account flows (see response/codes.go).


jwt

JWT token generation and validation with RS256 (default) or ES256. Keys are loaded from config (env, file paths, or embedded PEM). No global state: use Manager (sign + verify), Signer (issue tokens only), or Verifier (validate only). Supports issuer, audience, key ID (kid), and token type (access, refresh, impersonation). HS256 is not supported.

All-in-one (single service):

manager, err := jwt.NewManager(ctx, config.GetConfig())
if err != nil {
    log.Fatal(err)
}
router.Use(middlewares.AuthMiddleware(manager))

token, _ := manager.GenerateToken(userID, "users") // actor_type=user
claims, _ := manager.ValidateToken(tokenString)

Split services (auth server issues, API server only verifies):

// Auth/login service — needs private key
signer, err := jwt.NewSigner(ctx, config.GetConfig())
token, _ := signer.GenerateToken(userID, "users")
refresh, _ := signer.GenerateRefreshToken(userID, "users")
impersonation, _ := signer.GenerateImpersonationToken(adminID, "admin", targetID, 15*time.Minute)

// API/gateway service — needs only public key
verifier, err := jwt.NewVerifier(ctx, config.GetConfig())
router.Use(middlewares.AuthMiddleware(verifier))  // TokenVerifier interface
claims, _ := verifier.ValidateToken(tokenString)

API summary:

Symbol Description
jwt.NewManager(ctx, cfg) All-in-one; loads sign + verify keys; returns error
jwt.NewSigner(ctx, cfg) Signing only (private key)
jwt.NewVerifier(ctx, cfg) Verification only (public key)
jwt.TokenVerifier Interface: ValidateToken(string) (*Claims, error); implemented by *Manager and *Verifier
manager.GenerateToken(id) Access token; actor_type=service when actor/table omitted
manager.GenerateToken(id, "users") Access token; actor_type=user (User/users table)
manager.GenerateToken(id, "admins") Access token; actor_type=admin (admins table)
manager.GenerateTokenWithExpiry(id, expiry, actor?) Access token with custom expiry
manager.GenerateRefreshToken(id, actor?) Refresh token (same actor_type rules)
manager.GenerateImpersonationToken(adminID, role, targetID, ttl) Short-lived impersonation token (actor_type=user, max 30 min)
jwt.ResolveActorType(name) Map table/actor name → user / admin / service / system
manager.ValidateToken(token) / verifier.ValidateToken(token) Parse and verify; return *Claims or error
jwt.ComparePassword(hashed, plain) bcrypt comparison
jwt.GetCurrentUserUUID(ctx) Read user_id from Gin context (set by AuthMiddleware)
jwt.GetActorType(ctx) Read actor_type from Gin context (set by AuthMiddleware)

Config / env: Default algorithm is RS256. Set JWT_PRIVATE_KEY and JWT_PUBLIC_KEY (file path or inline PEM), or embed keys and assign to config before calling NewManager/NewSigner/NewVerifier. Optional: JWT_ISSUER, JWT_AUDIENCE, JWT_KEY_ID.

Embed keys (no file paths): Put PEM files in a package (e.g. keys/) and embed them; then set config so the JWT package uses the embedded bytes instead of paths:

//go:embed keys/private.pem
var privateKeyPEM []byte

//go:embed keys/public.pem
var publicKeyPEM []byte

func main() {
    cfg := config.GetConfig()
    cfg.Server.JWTPrivateKeyPEM = privateKeyPEM
    cfg.Server.JWTPublicKeyPEM = publicKeyPEM
    manager, err := jwt.NewManager(ctx, cfg)
    // ...
}

crypto

bcrypt password hashing.

crypto.HashAndSalt(plainPassword []byte) string
crypto.ComparePassword(hashedPassword string, plainPassword []byte) bool

storage

Provider-neutral object storage facade for GCS, S3-compatible services, local RustFS, and Cloudflare R2.

storage.Setup() error
storage.SetupContext(ctx) error
storage.GetBucketName() string
storage.ReadObject(ctx, objectName string) ([]byte, error)
storage.ReadObjectAsReader(ctx, objectName string) (io.ReadCloser, error)
storage.WriteObject(ctx, objectName string, data []byte, contentType string) error
storage.DeleteObject(ctx, objectName string) error
storage.ObjectExists(ctx, objectName string) (bool, error)
storage.ListObjects(ctx, prefix string) ([]string, error)
storage.Close() error

Set STORAGE_DRIVER to gcs, s3, or r2. Leave it empty to disable storage; calls before setup or while disabled return storage.ErrNotInitialized. Operations take context.Context as the first argument so callers can cancel and deadline object-storage I/O.


types

Shared types for handlers and repositories (no infrastructure dependencies).

// Conditions is the WHERE clause map passed to repository methods.
type Conditions map[string]interface{}

// PageInfo holds offset-based pagination request (pageNumber, pageSize) with form/json tags.
type PageInfo struct {
    PageNumber int `form:"pageNumber" json:"pageNumber"`
    PageSize   int `form:"pageSize" json:"pageSize"`
}

// TimeRange holds a start/end pair (IANA or RFC3339 strings).
type TimeRange struct {
    Start string `json:"start"`
    End   string `json:"end"`
}

util

util.IsEmpty(value interface{}) bool
util.InAnySlice[T comparable](haystack []T, needle T) bool
util.RemoveDuplicates[T comparable](haystack []T) []T
util.FormatPhoneNumber(phone *string, countryCode *string) *string  // E.164 via nyaruka/phonenumbers

domain

Domain errors and port interfaces. No dependencies on infrastructure.

// Sentinel errors for handlers and use cases (use with errors.Is).
var (
    ErrNotFound     = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
)

domain/port

Port interfaces implemented by infrastructure (e.g. repositories).

type GetByID interface {
    GetByID(ctx context.Context, id string) (interface{}, bool, error)
}

usecase

Application service layer. Use cases depend only on domain and ports.

type Runner interface { Run(ctx context.Context) error }
type Func func(ctx context.Context) error  // adapts func to Runner

// Example: get item by ID; returns domain.ErrNotFound when not found.
func GetItemByID(ctx context.Context, repo port.GetByID, id string) (interface{}, error)

repositories

GORM-based base repository: CRUD, First, Find, Scan, SimplePagination, RawSQL, ExecSQL. Use with dependency injection (NewBaseRepositoryWithDB) or global DB (NewBaseRepository). Implements IBaseRepository; adapt to domain/port in your app.

repo := repositories.NewBaseRepositoryWithDB(db.DB())
repo.Create(ctx, &model)
repo.First(ctx, &out, types.Conditions{"id = ?": id})
repo.SimplePagination(ctx, &model, &out, page, size, conditions, orders, "User", "Items")

Documentation

Human-readable guides are in [docs/](docs/README.md):

Guide Description
Getting Started Install, configure, minimal Gin wiring
Architecture Clean architecture layers and dependency rules
Environment Variables Env var reference (see also [.env.example](.env.example))
Development Tests, lint, Docker, Makefile
Packages Per-package guides for all modules

All packages follow GoDoc conventions:

  • Each package has a doc.go describing its role, responsibilities, constraints, and what it must not do.
  • Exported symbols have comments that start with the symbol name and describe behavior clearly.
  • Repository, adapter, and domain packages document contracts, error behavior, and context use.

Run go doc or view pkg.go.dev for the full API.


Minimal server

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "sync/atomic"
    "syscall"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "github.com/turahe/pkg/config"
    "github.com/turahe/pkg/database"
    "github.com/turahe/pkg/middlewares"
    "github.com/turahe/pkg/mtls"
    "github.com/turahe/pkg/otelx"
    pkgredis "github.com/turahe/pkg/redis"
    "github.com/turahe/pkg/sentryx"
    "gorm.io/gorm/logger"
)

func main() {
    if err := config.Setup(""); err != nil {
        log.Fatal(err)
    }
    cfg := config.GetConfig()

    ctx := context.Background()
    otelShutdown, otelEnabled := otelx.Init(ctx, cfg.OpenTelemetry)
    sentryEnabled := sentryx.Init(cfg.Sentry)

    // Database
    db, err := database.New(&cfg.Database, database.Options{LogLevel: logger.Warn})
    if err != nil {
        log.Fatal(err)
    }

    // Redis (optional)
    if cfg.Redis.Enabled {
        if err := pkgredis.Setup(); err != nil {
            log.Printf("redis: %v", err)
        }
    }

    // Readiness gate
    var ready atomic.Bool
    ready.Store(true)

    // Router
    router := gin.New()
    router.Use(
        middlewares.RecoveryHandler,
        middlewares.RequestID(),
        middlewares.LoggerMiddleware(),
        middlewares.Metrics(),
        middlewares.RequestTimeout(10*time.Second),
        middlewares.CORS(),
        middlewares.MTLSMiddleware(),
    )
    router.GET("/metrics", gin.WrapH(promhttp.Handler()))
    router.GET("/live", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })
    router.GET("/ready", func(c *gin.Context) {
        if !ready.Load() {
            c.JSON(http.StatusServiceUnavailable, gin.H{"status": "shutting_down"})
            return
        }
        if err := db.Health(c.Request.Context()); err != nil {
            c.JSON(http.StatusServiceUnavailable, gin.H{"status": "degraded", "error": err.Error()})
            return
        }
        c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })

    // Application routes ...
    router.NoMethod(middlewares.NoMethodHandler())
    router.NoRoute(middlewares.NoRouteHandler())

    srv := &http.Server{
        Addr:              ":" + cfg.Server.Port,
        Handler:           router,
        ReadHeaderTimeout: 5 * time.Second,
        ReadTimeout:       10 * time.Second,
        WriteTimeout:      10 * time.Second,
        IdleTimeout:       60 * time.Second,
    }
    if err := mtls.ConfigureServer(srv); err != nil {
        log.Fatal(err)
    }
    go func() {
        if err := mtls.ListenConfigured(srv); err != nil && err != http.ErrServerClosed {
            log.Printf("server: %v", err)
        }
    }()

    // Graceful shutdown (25 s matches terminationGracePeriodSeconds: 30)
    sig := make(chan os.Signal, 1)
    signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
    <-sig
    ready.Store(false) // signal readiness probe → 503 immediately

    shutCtx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
    defer cancel()
    srv.Shutdown(shutCtx)

    if sentryEnabled {
        sentryx.Flush(cfg.Sentry.FlushTimeout)
    }
    if otelEnabled {
        otelShutdown(shutCtx)
    }

    if cfg.Redis.Enabled {
        pkgredis.Close()
    }
    db.Close()
}

Repository with dependency injection

import (
    "github.com/turahe/pkg/database"
    "github.com/turahe/pkg/repositories"
    "github.com/turahe/pkg/types"
)

db, _ := database.New(&cfg.Database, database.Options{})
repo := repositories.NewBaseRepositoryWithDB(db.DB())

// Create
err := repo.Create(ctx, &myModel)

// Find with conditions
var results []MyModel
err = repo.Find(ctx, &results, types.Conditions{
    "status = ?": "active",
    "tenant_id = ?": tenantID,
})

// Pagination (pass preload names to avoid N+1)
total, err := repo.SimplePagination(ctx, &MyModel{}, &results, page, size,
    types.Conditions{"status = ?": "active"},
    []string{"created_at DESC"},
    "User", "Items",  // preloads
)

Clean architecture wiring

// domain/port/repository.go
type ItemRepository interface {
    GetByID(ctx context.Context, id string) (*Item, bool, error)
}

// infrastructure/repository/item.go
type itemRepo struct{ base repositories.IBaseRepository }

func (r *itemRepo) GetByID(ctx context.Context, id string) (*Item, bool, error) {
    var item Item
    notFound, err := r.base.First(ctx, &item, types.Conditions{"id = ?": id})
    return &item, !notFound, err
}

// usecase/get_item.go
func GetItem(ctx context.Context, repo port.ItemRepository, id string) (*Item, error) {
    item, found, err := repo.GetByID(ctx, id)
    if err != nil { return nil, err }
    if !found { return nil, domain.ErrNotFound }
    return item, nil
}

Environment Variables

Copy .env.example to .env and call config.Setup(""):

cp .env.example .env

Server

Variable Default Description
SERVER_PORT 8080 HTTP listen port
SERVER_MODE debug Gin mode: debug, release, test
SERVER_ACCESS_TOKEN_EXPIRY 1 Access token lifetime (hours)
SERVER_REFRESH_TOKEN_EXPIRY 7 Refresh token lifetime (days)
SERVER_SESSION_EXPIRY 24 Session lifetime (hours)
SERVER_TIMEZONE UTC IANA timezone (also default for DB when DATABASE_TIMEZONE empty)
APP_ENV — Deployment environment; fallback for OTEL/Sentry when their env vars are empty

CORS

Variable Default Description
CORS_GLOBAL true Allow all origins (*)
CORS_FRONTEND — Frontend origin URL when CORS_GLOBAL=false (e.g. http://localhost:3000)
CORS_IPS — Comma-separated allowed origins when CORS_GLOBAL=false

Allowed headers include Authorization, X-CSRF-Token, and X-2FA-Code.

JWT

Variable Default Description
JWT_SIGNING_ALGORITHM RS256 RS256 · ES256 (HS256 removed)
JWT_PRIVATE_KEY — File path or inline PEM private key (RS256/ES256)
JWT_PUBLIC_KEY — File path or inline PEM public key (RS256/ES256)
JWT_ISSUER — Issuer (iss) claim
JWT_AUDIENCE — Audience (aud), comma-separated
JWT_KEY_ID — Key ID (kid) in JWT header

Keys can be embedded via config.Server.JWTPrivateKeyPEM / JWTPublicKeyPEM (see jwt section).

Database

Variable Default Description
DATABASE_DRIVER mysql mysql · postgres · sqlserver · cloudsql-mysql · cloudsql-postgres
DATABASE_HOST 127.0.0.1
DATABASE_PORT 3306
DATABASE_USERNAME —
DATABASE_PASSWORD —
DATABASE_DBNAME —
DATABASE_SSLMODE false
DATABASE_LOGMODE false Enable GORM query logging
DATABASE_TIMEZONE — IANA timezone for DB session (mysql/postgres/Cloud SQL); empty uses SERVER_TIMEZONE
DATABASE_MAX_IDLE_CONNS 0 (→ 10) Connection pool idle size
DATABASE_MAX_OPEN_CONNS 0 (→ 30) Connection pool max open
DATABASE_CONN_MAX_LIFETIME 0 (→ 30 min) Connection lifetime (minutes)
DATABASE_CLOUD_SQL_INSTANCE — project:region:instance for Cloud SQL
DATABASE_*_SITE — Same keys with _SITE suffix for secondary DB
DATABASE_TIMEZONE_SITE — Site DB timezone; empty uses SERVER_TIMEZONE

Redis

Variable Default Description
REDIS_ENABLED false
REDIS_HOST 127.0.0.1
REDIS_PORT 6379
REDIS_PASSWORD —
REDIS_DB 0 Database index (ignored in cluster mode)
REDIS_CLUSTER_MODE false Enable cluster client
REDIS_CLUSTER_NODES — Comma-separated host:port list
REDIS_POOL_SIZE 0 (client default) Max connections per node
REDIS_MIN_IDLE_CONNS 0 Min idle connections
REDIS_READ_TIMEOUT_SEC 0 (no timeout) Read timeout
REDIS_WRITE_TIMEOUT_SEC 0 (no timeout) Write timeout

Rate Limiter

Variable Default Description
RATE_LIMITER_ENABLED false Requires Redis
RATE_LIMITER_REQUESTS 100 Requests allowed per window
RATE_LIMITER_WINDOW 60 Window size (seconds)
RATE_LIMITER_KEY_BY ip ip or user
RATE_LIMITER_SKIP_PATHS — Comma-separated paths (e.g. /health,/metrics)

Storage

Variable Default Description
STORAGE_DRIVER - gcs, s3, or r2; empty disables storage
STORAGE_BUCKET - Required when STORAGE_DRIVER is set
STORAGE_ENDPOINT - S3-compatible or R2 endpoint URL
STORAGE_REGION us-east-1 for s3, auto for r2 Optional region override
STORAGE_ACCESS_KEY - S3-compatible / R2 access key
STORAGE_SECRET_KEY - S3-compatible / R2 secret key
STORAGE_FORCE_PATH_STYLE driver default Optional true / false path-style override
GCS_CREDENTIALS_FILE - GCS driver only; omit to use ADC

Local RustFS example:

STORAGE_DRIVER=s3
STORAGE_BUCKET=pkg
STORAGE_ENDPOINT=http://127.0.0.1:9000
STORAGE_REGION=us-east-1
STORAGE_ACCESS_KEY=rustfsadmin
STORAGE_SECRET_KEY=rustfsadmin
STORAGE_FORCE_PATH_STYLE=true

Cloudflare R2 example:

STORAGE_DRIVER=r2
STORAGE_BUCKET=my-bucket
STORAGE_ENDPOINT=https://<accountid>.r2.cloudflarestorage.com
STORAGE_ACCESS_KEY=<r2-access-key>
STORAGE_SECRET_KEY=<r2-secret-key>
STORAGE_REGION=auto
STORAGE_FORCE_PATH_STYLE=false

GCS_ENABLED and GCS_BUCKET_NAME are deprecated and are not read. Use STORAGE_DRIVER=gcs and STORAGE_BUCKET for Google Cloud Storage.

OpenTelemetry

Initialize tracing with otelx.Init(ctx, config.GetConfig().OpenTelemetry) after config.Setup. Choose an exporter with OTEL_TRACES_EXPORTER (otlp or otlp_grpc).

Shared (all exporters):

Variable Required Default Example Description
OTEL_TRACES_EXPORTER No otlp otlp_grpc otlp (HTTP) or otlp_grpc (gRPC)
OTEL_SERVICE_NAME No app payments-api Service name resource attribute
OTEL_ENVIRONMENT No APP_ENV production Deployment environment; falls back to APP_ENV
OTEL_SERVICE_VERSION No SENTRY_RELEASE 1.4.2 Service version; falls back to SENTRY_RELEASE
OTEL_TRACES_SAMPLER_ARG No 1.0 0.25 Parent-based ratio sampler (0..1)
OTEL_SHUTDOWN_TIMEOUT No 5s 10s Tracer provider shutdown timeout
OTEL_GORM_ENABLED No true false Register GORM OTel plugin when tracing is enabled

OTLP (OTEL_TRACES_EXPORTER=otlp or otlp_grpc):

Variable Required Default Example Description
OTEL_EXPORTER_OTLP_ENDPOINT Yes — localhost:4318 OTLP endpoint (host:port, no scheme); use :4317 for gRPC
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT No — otel-collector:4318 Trace-specific endpoint override
OTEL_EXPORTER_OTLP_PROTOCOL No http/protobuf grpc Transport; OTEL_EXPORTER_OTLP_TRACES_PROTOCOL overrides
OTEL_EXPORTER_OTLP_INSECURE No true false Use insecure transport for OTLP export
OTEL_EXPORTER_OTLP_HEADERS No — Authorization=Bearer token Comma-separated key=value OTLP headers

GORM is instrumented automatically when tracing is enabled and OTEL_GORM_ENABLED=true (default).

Example — OTLP (local collector / Jaeger):

OTEL_TRACES_EXPORTER=otlp
OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4318
OTEL_EXPORTER_OTLP_INSECURE=true
OTEL_SERVICE_NAME=payments-api
OTEL_ENVIRONMENT=development
OTEL_TRACES_SAMPLER_ARG=1.0
OTEL_GORM_ENABLED=true

Sentry

Variable Default Description
SENTRY_DSN — Sentry DSN; empty disables Sentry
SENTRY_ENVIRONMENT — Deployment environment; falls back to APP_ENV
SENTRY_RELEASE — Release identifier (git SHA or semver)
SENTRY_SERVER_NAME — Server name tag; sentryx defaults to app
SENTRY_DEBUG false Enable Sentry SDK debug logging
SENTRY_ATTACH_STACKTRACE true Attach stack traces to non-panic messages
SENTRY_SAMPLE_RATE 1.0 Error event sample rate (0..1)
SENTRY_TRACES_SAMPLE_RATE 0.0 Performance transaction sample rate (0..1)
SENTRY_FLUSH_TIMEOUT 2s Flush timeout at shutdown

Initialize Sentry with sentryx.Init(config.GetConfig().Sentry) after config.Setup.

mTLS

Variable Default Description
MTLS_ENABLED false Enable mTLS server listen and MTLSMiddleware
MTLS_CA_CERT /etc/mtls/ca.crt Shared CA bundle
MTLS_SERVER_CERT /etc/mtls/server.crt Backend server certificate
MTLS_SERVER_KEY /etc/mtls/server.key Backend server private key
MTLS_CLIENT_CERT /etc/mtls/gateway.crt Gateway/client certificate (outbound)
MTLS_CLIENT_KEY /etc/mtls/gateway.key Gateway/client private key
MTLS_SKIP_PATHS /live,/ready,/metrics Paths bypassing MTLSMiddleware

See [mtls](#mtls) for server/client wiring examples.


Production Wiring Example

See Getting Started for a minimal server, and the Usage section above for a fuller example. A complete production wiring typically includes:

  • config.Setup and optional otelx / sentryx initialization
  • Dependency-injected database with health check and optional GORM tracing
  • Optional Redis setup and graceful Close()
  • Optional mTLS via mtls.ConfigureServer + MTLSMiddleware
  • gin.New() with explicit middleware stack (recovery → trace → logging → metrics → timeout → CORS → mTLS)
  • /live (liveness), /ready (readiness with component checks), /metrics (Prometheus)
  • HTTP server with all timeouts set
  • Graceful shutdown: readiness gate → srv.Shutdown(25s) → Sentry flush → OTel shutdown → Redis close → DB close

Kubernetes probes

livenessProbe:
  httpGet:
    path: /live
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

terminationGracePeriodSeconds: 30

Docker

docker build -t myapp .
docker run --env-file .env -p 8080:8080 myapp

The multi-stage Dockerfile uses golang:1.26-alpine to build and gcr.io/distroless/base-debian12:nonroot as the runtime image. Runs as UID 65532 (non-root).


Testing

Run all tests:

go test ./...

With race detector (requires CGO):

CGO_ENABLED=1 go test -race ./...

With coverage:

go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out

Run tests with Docker Compose

Run the full test suite (including integration tests) without local Go or databases:

make test-docker

Or with docker compose directly:

docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from test
docker compose -f docker-compose.test.yml down -v

The test runner waits for Redis, MySQL, Postgres, and SQL Server to be healthy, then runs go test -v -race -count=1 ./.... Coverage is written to coverage.out and a summary is printed.

Integration tests (Redis · MySQL · Postgres · SQL Server) — local

Start services with Docker Compose:

docker compose up -d

Run with environment variables:

REDIS_ENABLED=true REDIS_HOST=127.0.0.1 REDIS_PORT=6379 \
DATABASE_DRIVER=mysql DATABASE_HOST=127.0.0.1 DATABASE_PORT=3306 \
DATABASE_USERNAME=root DATABASE_PASSWORD=root DATABASE_DBNAME=testdb \
SQLSERVER_HOST=127.0.0.1 SQLSERVER_PORT=1433 \
SQLSERVER_USERNAME=sa SQLSERVER_PASSWORD=Test_Password123 SQLSERVER_DBNAME=master \
go test ./...

Integration tests skip automatically when services are unavailable. CI runs the full matrix with these services via GitHub Actions.

Packages with tests: config, crypto, database, handler, jwt, logger, middlewares, mtls, otelx, redis, repositories, response, sentryx, storage, types, util.


License

MIT

About

A collection of production-ready Go packages for building web services: database, Redis (standard + cluster), JWT, crypto, object storage, structured logging, HTTP middleware, OpenTelemetry (otelx), Sentry (sentryx), mutual TLS (mtls), Prometheus metrics, graceful shutdown, and utilities.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages