Skip to content

Core

Fetch the module first: go get github.com/yshengliao/gortexa@v0.27.0. Every snippet below is compile-verified against v0.27.0.

The composition root: App owns the single h2c listener (multiplexing gRPC, HTTP/JSON and MCP), the gRPC server, the health registry, an in-process loopback, and graceful shutdown. Build it with kernel.New(opts...); options include WithConfig, WithLogger, WithInterceptors (takes an interceptor.Set by value, not a pointer), WithStatsHandler, WithHTTPWrap, WithGateway, WithMCPHandler, WithShutdownHook, WithServerOptions (append gRPC ServerOptions to add your own interceptors/stats, chained after the framework’s), WithAdminListener (opt-in separate ops port serving only /healthz + /readyz), WithExtraListener (serve any handler on a caller-owned listener). The main h2c port stays single-port-multiplexed; app.AdminAddr() reports the bound admin address.

import (
"context"
"github.com/yshengliao/gortexa/config"
"github.com/yshengliao/gortexa/health"
"github.com/yshengliao/gortexa/kernel"
)
cfg := config.MustBuild()
app, err := kernel.New(kernel.WithConfig(cfg))
if err != nil {
return err
}
// register your gRPC services on app.GRPCServer() here
app.Health().Register("self", func(context.Context) health.State { return health.Healthy })
return app.Run(ctx) // /healthz and /readyz are built in

App methods: GRPCServer(), Health(), Loopback(), Logger(), SetGateway(), SetMCPHandler(), Run(ctx), Shutdown(ctx).

Fixed precedence: built-in defaults < YAML file < .env file < environment (GORTEXA_ prefix, __ for nesting). The entry points are MustBuild (fail-loud: it panics when jwt_secret is empty, the dev placeholder, or < 32 bytes, and when issuer is blank) and BuildUnvalidated. There is no Load.

import "github.com/yshengliao/gortexa/config"
cfg := config.MustBuild(config.WithConfigFile("etc/config.yaml"))
dsn := cfg.DB.DSN.Reveal() // a Secret is revealed only at the point of use

The config.Secret type masks itself to **** in String(), logs and JSON; call Reveal() for the real value. Config’s sub-structs: Server, Auth, DB, Cache, MQ, Log, Observ (all fields documented in Configuration).

NewSet(Config) builds the eight-stage fixed-order chain (recover → request-id → logger → load-shed → rate-limit → circuit-breaker → auth → validation), which you pass to kernel.WithInterceptors.

import (
"time"
"github.com/yshengliao/gortexa/auth"
"github.com/yshengliao/gortexa/interceptor"
)
set, err := interceptor.NewSet(interceptor.Config{
Verifier: auth.MustNewVerifier([]byte(secret), "gortexa"),
RateLimit: interceptor.RateLimitConfig{RPS: 200, Burst: 100, TTL: 10 * time.Minute},
})
// pass set to kernel.WithInterceptors(set)

Config also carries Logger, AuthSkip, CircuitBreaker, LoadShedding, and Metrics. Set exposes UnaryChain() / StreamChain().

The auth stage is pluggable: Verifier uses the built-in HS256 JWT; a non-JWT scheme sets Authenticator instead (an auth.Authenticator, Authenticate(ctx) (ctx, error)) — static bearer, mTLS, API key. Authenticator wins when both are set. JWT is also available explicitly via auth.NewJWTAuthenticator(v).

HS256 JWT verification/signing plus context helpers. The secret must be ≥ 32 bytes.

import "github.com/yshengliao/gortexa/auth"
v := auth.MustNewVerifier([]byte(secret), "gortexa")
tok, err := v.Sign("user-123", []string{"admin"}, time.Hour)
// later, inside a handler, retrieve the verified claims:
if claims, ok := auth.ClaimsFrom(ctx); ok {
_ = claims.Subject // "user-123"
}

Verify(token) returns *Claims (with Roles plus embedded jwt.RegisteredClaims); BearerToken(header) extracts a token from the Authorization header.

A single mapping table drives the gRPC status, HTTP status and MCP error envelope. *apperr.Error implements GRPCStatus(), so a handler just returns it.

import "github.com/yshengliao/gortexa/apperr"
func (s *svc) GetThing(ctx context.Context, req *pb.GetThingRequest) (*pb.Thing, error) {
t, err := s.repo.Find(ctx, req.GetId())
if err != nil {
return nil, apperr.Wrap(apperr.CatNotFound, "thing not found", err)
}
return t, nil
}

Only CatInvalidArgument and CatUnauthenticated forward the developer message to the caller; every other category exposes only the registry SafeMessage, so internal causes never leak. The category constants cover the full gRPC set (CatNotFound, CatPermissionDenied, CatFailedPrecondition, CatInternal, and so on).