Core concepts
Contract-first
Section titled “Contract-first”Protobuf is the single source of truth. You write two things: proto contracts and logic implementations. gRPC stubs, the HTTP/JSON gateway, the OpenAPI spec and MCP tool schemas are all produced by gortexa regen, which always runs buf lint → buf breaking → buf generate.
- Everything under
gen/is an artifact: not committed, never hand-edited. - The breaking-change gate is built into the flow: proto edits that break compatibility are rejected; additive edits pass.
Single h2c port multiplexing
Section titled “Single h2c port multiplexing”One cleartext HTTP/2 port, dispatched by Content-Type and path:
| Request | Destination |
|---|---|
Content-Type: application/grpc |
gRPC (grpc-go’s ServeHTTP mode) |
Path /mcp |
MCP bridge (Streamable HTTP) |
| Everything else | grpc-gateway (HTTP/JSON) and built-in routes (/healthz, /openapi.json) |
h2c uses Go’s native http.Server.Protocols, not the deprecated x/net/h2c. Cleartext means TLS must be terminated in front of the service in production — see Deployment.
The interceptor chain
Section titled “The interceptor chain”Eight stages, fixed order:
recover → request-id → logger → load-shed → rate-limit → circuit-breaker → auth → validationAn incomplete chain fails startup (fail-loud) rather than silently dropping a stage. OTel attaches as a gRPC StatsHandler, covering unary and streaming.
The order is fixed, but the auth stage is pluggable: the default is HS256 JWT (interceptor.Config.Verifier), yet the stage actually runs an auth.Authenticator — a non-JWT scheme (static bearer, mTLS, API key) just sets Config.Authenticator, leaving the eight-stage order intact. To add your own interceptor, use kernel.WithServerOptions(...) (gRPC chains it after the framework chain, before the handler, still within recover) rather than editing the eight stages.
The defaults for the three governance stages are defined in cmd/server/main.go (not in config.yaml — to change them, edit the interceptor.NewSet parameters and recompile):
| Stage | Defaults | On limit |
|---|---|---|
| rate-limit | 200 RPS per peer, burst 100, tracking TTL 10 minutes | RESOURCE_EXHAUSTED (HTTP 429) |
| circuit-breaker | opens after 5 consecutive failures for 10s, half-open admits 2 probes | UNAVAILABLE (HTTP 503) |
| load-shed | in-flight cap 1,024 (gRPC health checks exempt) | RESOURCE_EXHAUSTED (HTTP 429) |
Measured numbers (benchmarks in the framework README, Apple M1 / go1.26.5): a full unary pass through the chain is about 1,552 ns and 27 allocs; the rate-limiter Allow hot path is 0 allocs (allocs are machine-independent).
One error model
Section titled “One error model”A single mapping table decides how the same error appears on all three protocols: gRPC status, HTTP status, MCP error envelope. The rule is that internal causes never leak — only invalid-argument and unauthenticated forward their messages to the caller; every other category returns a safe message from the registry.
The error-resolution hot path is about 105 ns and 2 allocs (using Go 1.26’s errors.AsType).
Batteries
Section titled “Batteries”| Component | Notes |
|---|---|
| Config | Layered loading, fail-loud, masked secrets. See Configuration |
| Health | Three-state health mapped to gRPC Health; app.Health().Register(...) wires real dependencies |
| Logging/Observability | Structured logs via slog + OTLP export |
| Database | PgBouncer-safe pgx pool + sqlc |
| Cache | Process-local in-memory by default; cache.driver: redis switches to distributed (zero-dependency RESP client) |
| MQ | NATS (core or JetStream); empty mq.group_id = fanout, non-empty = load-balance |