This is the full developer documentation for Gortexa # Gortexa ## Features ### Three protocols, one port h2c multiplexing on Go's native `http.Server.Protocols`. One port accepts gRPC, HTTP/JSON and MCP. ### Fixed-order interceptor chain recover → request-id → logger → load-shed → rate-limit → circuit-breaker → auth → validation. Eight stages in a fixed order; a missing stage fails startup. ### One error model A single mapping table produces the gRPC status, HTTP status and MCP error envelope. Internal causes never leak. ### AI tool calls go through the same chain MCP `tools/call` dispatches over an in-process loopback, so AI calls pass auth and validation like any other request. ### Batteries included Layered config, three-state health, slog + OTLP, pgx + sqlc, cache (in-memory by default), NATS/JetStream. ### Importable as a module `go get` the framework packages directly — kernel/interceptor/mq for plain services, plus the mcp bridge and gortexa.ai.v1 annotations for AI ones. No scaffold required. See [Components](/en/components/). ## Get started To have a coding agent build a runnable service scaffold from a prompt, use [Quickstart with AI](/en/ai-quickstart/). It pins the release, verifies the toolchain, removes the usable secret generated by the scaffold, and runs HTTP/MCP smoke checks in one shell. For a manual walkthrough, use [Quickstart](/en/quickstart/). Both paths create a prototype only; see [Deployment](/en/deployment/) for the work required before production. Or consume the framework as a Go module — `go get` it and import the packages directly, no scaffold required: ```bash go get github.com/yshengliao/gortexa@latest ``` See [Components](/en/components/) for each package's usage, or [Use as a module](/en/quickstart/#use-as-a-module-no-scaffold) for a minimal app. After either flow starts the server on :8080, hit each protocol once: ```bash curl localhost:8080/healthz # 200: health check is open curl -XPOST localhost:8080/mcp \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' # MCP: list AI tools defined in proto curl localhost:8080/v1/resources/x # 401: auth is shared across all three ``` # Quickstart with AI Gortexa lets a coding agent with terminal and file-system access quickly create a runnable, contract-first API prototype: proto, gRPC, HTTP/JSON, MCP, validation, and server wiring are generated together. To use the framework packages as a plain library instead (no scaffolded project), see [Use as a module](/en/quickstart/#use-as-a-module-no-scaffold). **Prototype is not production** The generated logic is an in-memory stub. It does not automatically provide persistence, tenant isolation, authorization, secret lifecycle, real readiness checks, or a deployable production design. A service that starts is not automatically safe to launch. ## Prerequisites - The agent can run shell commands and read/write the local project. - Go 1.21+ (as the bootstrap; the framework downloads Go 1.26.5), Git, curl, OpenSSL, and Perl are installed, and GitHub plus the Go module proxy are reachable. - The shell may use `GOTOOLCHAIN=auto` so Go can download the toolchain required by the framework. - Start with an empty workspace. The agent must not overwrite projects, push, deploy, or commit secrets unless you explicitly authorize it. ## Paste this: create a reliable prototype This prompt builds the toolchain, CLI, and project layout from the **same verified release tag**. That avoids drift between an @latest CLI and a floating main layout. It records the checkout SHA and avoids make bootstrap inside the generated project because the toolchain is already installed from the framework checkout. ~~~text Help me create a runnable Gortexa prototype, not a production service. If I have not supplied SERVICE_NAME, GO_MODULE, API_DOMAIN/API_VERSION, and ENTITY, ask only for those four values first. Do not overwrite an existing directory, git push, deploy, commit, or reveal any secret. Execute the following steps in one shell session, in order, and report each result. Run set -euo pipefail first; stop on any failure rather than guessing or skipping it. set -euo pipefail WORKSPACE="$HOME/src" SERVICE_NAME="myapp" GO_MODULE="github.com/me/myapp" API_DOMAIN="billing" API_VERSION="v1" ENTITY="Invoice" FRAMEWORK_REF="v0.27.0" # change only after testing a newer release FRAMEWORK="$WORKSPACE/gortexa-framework" PROJECT="$WORKSPACE/$SERVICE_NAME" 1. mkdir -p "$WORKSPACE" git clone --depth 1 --branch "$FRAMEWORK_REF" https://github.com/yshengliao/gortexa.git "$FRAMEWORK" cd "$FRAMEWORK" export GOTOOLCHAIN=auto bash install.sh export PATH="$(go env GOPATH)/bin:$PATH" mkdir -p "$FRAMEWORK/bin" go build -o "$FRAMEWORK/bin/gortexa" ./cmd/gortexa export PATH="$FRAMEWORK/bin:$PATH" CLI="$FRAMEWORK/bin/gortexa" "$CLI" doctor FRAMEWORK_COMMIT="$(git rev-parse HEAD)" printf 'framework ref=%s commit=%s\n' "$FRAMEWORK_REF" "$FRAMEWORK_COMMIT" 2. "$CLI" create "$PROJECT" --module "$GO_MODULE" --repo "file://$FRAMEWORK" --ref "$FRAMEWORK_REF" cd "$PROJECT" Without printing the old value, reset the JWT secret written by create to the placeholder that refuses startup: perl -0pi -e 's/(jwt_secret:\s*")[^"]*(")/${1}dev-only-insecure-secret-change-me-please${2}/' etc/config.yaml grep -Fq 'jwt_secret: "dev-only-insecure-secret-change-me-please"' etc/config.yaml Read all four skill files under .skills/ before changing code. Do not run make bootstrap in this new project; reuse the toolchain verified above. 3. "$CLI" gen "$API_DOMAIN/$API_VERSION" "$ENTITY" Run go build ./... && go test -race ./... && go vet ./... Explain that generated logic is an in-memory prototype stub and identify what must be replaced for a real service. 4. For a local prototype only, generate a short-lived runtime secret. To clean up reliably from one shell, start the generated server binary directly ("$CLI" run is the foreground development command): export GORTEXA_AUTH__JWT_SECRET="$(openssl rand -hex 32)" go build -o "$PROJECT/.gortexa-prototype-server" ./cmd/server "$PROJECT/.gortexa-prototype-server" >"$PROJECT/gortexa-prototype.log" 2>&1 & SERVER_PID=$! cleanup() { kill -TERM "$SERVER_PID" 2>/dev/null || true; wait "$SERVER_PID" 2>/dev/null || true; rm -f "$PROJECT/.gortexa-prototype-server" "$PROJECT/gortexa-prototype.log"; } trap cleanup EXIT INT TERM for _ in $(seq 1 60); do if curl -fsS http://127.0.0.1:8080/healthz >/dev/null 2>&1; then break; fi sleep 0.5 done curl -fsS http://127.0.0.1:8080/healthz test "$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/v1/resources/x)" = 401 curl -fsS -X POST localhost:8080/mcp -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' cleanup trap - EXIT INT TERM 5. Stop after prototype verification. Do not git add or build a Docker image until I confirm that etc/config.yaml remains a placeholder, secrets are externalized, and persistence plus authorization are done. Rules: proto is the single source of truth; after proto changes run "$CLI" regen; never hand-edit gen/. Read https://gortexa.sheng.page/llms.txt first, then only the documentation pages needed for the task. ~~~ Success means doctor, build, race tests, and vet pass; healthz returns 200; an unauthorized HTTP-gateway request returns 401; and MCP tools/list exposes the generated tool. That validates the **development scaffold, HTTP auth surface, and MCP discovery**. It does not perform a live gRPC E2E test or complete the data and authorization models. ## Paste this: plan a production foundation Use this when you intend to ship the service. It forces the agent to collect the inputs that materially change the design instead of guessing tenants, authorization, or retention rules. ~~~text Move the current Gortexa prototype toward a production foundation, but do not write code yet. First confirm: actors and tenant model, role/permission matrix, database and migrations, transactions/idempotency/pagination, cache/MQ retry and DLQ semantics, secret rotation, TLS ingress/trusted proxies/rate limits, SLOs/observability, replicas/readiness, rollback, and acceptance tests. Ownership must be derived from an authenticated principal/tenant, never trusted from an owner value in the request body. After I answer, propose a phased plan; do not retain generated in-memory logic as the production implementation. Include external secret injection, persistent repositories/migrations, tenant-aware authorization, real readiness, cross-tenant negative tests, integration/E2E/failure tests, a non-root Docker runtime, and TLS-ingress verification. Run tests after each phase and report remaining risks. ~~~ See [Deployment](/en/deployment/) and [Security & boundaries](/en/examples/security/). ## Skills and agent docs Every gortexa create project contains .skills/: | Skill | It helps with | It does not replace | | --- | --- | --- | | bootstrapping-environment | Installing and checking the pinned toolchain | release and production readiness | | scaffolding-projects | Creating the fixed project layout | product requirements and an upgrade strategy | | generating-apis | proto, validation, AI annotations, logic stub, server wiring | persistence, authorization, business policy | | proto-regen | regenerating stubs, gateway, OpenAPI, MCP code | schema and data-migration strategy | In the same shell used by the prompt, `"$CLI" skills install` can wire these skills into Claude Code, Codex, Copilot, and Antigravity. Reading .skills/ directly in the project is sufficient for an agent to follow the workflow. - [/llms.txt](/llms.txt): the site index; read it first, then select the needed pages. - [/llms-full.txt](/llms-full.txt): complete English documentation; use it only when the larger context is justified. ## Let an agent call the local service The service is an MCP server. /mcp exposes proto methods annotated with gortexa.ai.v1; tools/call goes through auth and validation, but application authorization remains your responsibility. ~~~bash curl -X POST localhost:8080/mcp \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ~~~ Export a tool schema with: ~~~bash "$CLI" export --format=mcp ~~~ Details are in [the MCP transport](/en/transports/mcp/). # Quickstart ## Install The dev toolchain (buf, protoc plugins, sqlc, govulncheck — all pinned): ```bash GORTEXA_VERSION="v0.27.0" # update only after testing a newer release export GORTEXA_REF="$GORTEXA_VERSION" export GOTOOLCHAIN=auto /bin/bash -c "$(curl -fsSL "https://raw.githubusercontent.com/yshengliao/gortexa/$GORTEXA_VERSION/install.sh")" go install github.com/yshengliao/gortexa/cmd/gortexa@"$GORTEXA_VERSION" ``` Run outside a checkout, the script clones the repo into `./gortexa` (override with `GORTEXA_DIR`, delete afterwards if unwanted); it only writes persistent `go env` settings when it detects the broken dev-container values — a normal machine's Go config is left untouched. `go install` puts the binary in `$(go env GOPATH)/bin`, which most shells do not have on PATH by default — without it, the next step fails with `command not found: gortexa`: ```bash export PATH="$(go env GOPATH)/bin:$PATH" # add to your shell profile ``` Run `gortexa doctor` afterwards to check the Go toolchain and proto tools. ## Create a project ```bash gortexa create myapp --module github.com/me/myapp \ --repo https://github.com/yshengliao/gortexa.git --ref v0.27.0 cd myapp # Do not print the old value: remove the usable secret generated by create. # The placeholder refuses startup without an environment override. perl -0pi -e 's/(jwt_secret:\s*")[^"]*(")/${1}dev-only-insecure-secret-change-me-please${2}/' etc/config.yaml grep -Fq 'jwt_secret: "dev-only-insecure-secret-change-me-please"' etc/config.yaml ``` `create` copies the framework layout, rewrites the module path and drops the git history. The result ships a sample resource, the full interceptor chain, config, health and the MCP bridge. **This is a runnable demo, not a data service** The next gen command creates process-local in-memory logic; data disappears on restart and is not shared between replicas. Treat it as a contract and transport scaffold. A real service must add persistence, authorization, and real readiness. If the toolchain was installed from a framework checkout, validate it in the new project with gortexa doctor rather than assuming make bootstrap can reinstall it there. ## Generate your first API ```bash gortexa gen billing/v1 Invoice ``` This produces the proto contract (with validation and AI annotations), an in-memory logic stub and the server wiring, then runs buf lint → breaking → generate. After later proto edits, run `gortexa regen`. Everything under `gen/` is generated code: never hand-edited, produced only by `make gen` (committed, following the framework convention — the copied CI has a drift guard). ## Run and verify The following single-shell smoke test uses a short-lived runtime secret, starts the same server entrypoint used by `gortexa run`, waits for readiness, and registers cleanup for its binary and log. The server remains for the next token flow; do not change shells between these commands. ```bash set -euo pipefail export GORTEXA_AUTH__JWT_SECRET="$(openssl rand -hex 32)" go build -o ./.gortexa-prototype-server ./cmd/server ./.gortexa-prototype-server >./gortexa-prototype.log 2>&1 & SERVER_PID=$! cleanup() { kill -TERM "$SERVER_PID" 2>/dev/null || true; wait "$SERVER_PID" 2>/dev/null || true; rm -f ./.gortexa-prototype-server ./gortexa-prototype.log; } trap cleanup EXIT INT TERM for _ in $(seq 1 60); do if curl -fsS http://127.0.0.1:8080/healthz >/dev/null 2>&1; then break; fi sleep 0.5 done curl -fsS http://127.0.0.1:8080/healthz test "$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/v1/resources/x)" = 401 curl -fsS -X POST http://127.0.0.1:8080/mcp \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` The final HTTP-gateway request returns 401 by design; it shares authentication with gRPC and MCP. This smoke test does not replace a live gRPC E2E or authorization test. If you will not continue to the token flow, run `cleanup; trap - EXIT INT TERM` now to stop the server and remove temporary files. For normal interactive development, run `gortexa run` in the same shell with the exported secret, then stop it with Ctrl-C. ## Getting a dev token The server remains running in the same shell from the previous block. First ensure the temporary command does not already exist, then create its directory: ```bash test ! -e cmd/devtoken mkdir -p cmd/devtoken ``` Save the following secret-free program as `cmd/devtoken/main.go`. Replace the import if your module is not `github.com/me/myapp`: ```go package main import ( "fmt" "os" "time" "github.com/me/myapp/auth" ) func main() { secret := os.Getenv("GORTEXA_AUTH__JWT_SECRET") v, err := auth.NewVerifier([]byte(secret), "gortexa") if err != nil { panic(err) } tok, err := v.Sign("dev", []string{"admin"}, time.Hour) if err != nil { panic(err) } fmt.Println(tok) } ``` Run it, verify an authenticated request, then remove the temporary command and stop the server: ```bash TOKEN="$(go run ./cmd/devtoken)" AUTH_STATUS="$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/v1/resources/x -H "Authorization: Bearer $TOKEN")" test "$AUTH_STATUS" = 200 || test "$AUTH_STATUS" = 404 printf 'authenticated HTTP status=%s\n' "$AUTH_STATUS" rm cmd/devtoken/main.go rmdir cmd/devtoken cleanup trap - EXIT INT TERM ``` Do not copy, commit, or bake a real secret from `etc/config.yaml` into an image. In production, inject and rotate it through a secret manager, mounted file, or runtime environment. ## Use as a module (no scaffold) Every framework package is importable directly with `go get` — no scaffolded project required: ```bash go get github.com/yshengliao/gortexa@latest ``` A minimal app — one h2c port with gRPC health, `/healthz`, `/readyz` — needs no code generation at all: ```go package main import ( "context" "log" "os/signal" "syscall" "github.com/yshengliao/gortexa/health" "github.com/yshengliao/gortexa/kernel" ) func main() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() app, err := kernel.New() // defaults: h2c on :8080, graceful shutdown if err != nil { log.Fatal(err) } app.Health().Register("self", func(context.Context) health.State { return health.Healthy }) if err := app.Run(ctx); err != nil { log.Fatal(err) } } ``` From there, both the AI and the non-AI path are first-class: - Plain services: `config` for layered config, `interceptor` for the governed chain, `httpcompat` for the gateway mux, `apperr` for the three-transport error model, plus `mq` / `cache` / `storage` / `client`. - AI services: annotate your protos with `gortexa/ai/v1/annotations.proto` (its Go bindings ship in the module at `gen/gortexa/ai/v1`) and wire `mcp.NewBridge` to expose the annotated RPCs as MCP tools behind the same interceptor chain. For each public component's purpose, import path and a minimal example, see [Components](/en/components/). Note: a project created by `gortexa create` already contains the framework source under its own module path — never also `go get` the framework there, or both copies register `gortexa/ai/v1/annotations.proto` and the binary panics at init. ## Working inside the framework checkout When developing the framework itself, use make: ```bash make bootstrap # install the pinned toolchain make gen # buf lint → breaking → generate make test # go test -race make run # start the sample server (injects a local dev JWT secret) make dev # docker compose with Jaeger, Grafana, Prometheus ``` ## Next steps - [Core concepts](/en/concepts/): how single-port multiplexing, the interceptor chain and the error model work. - [Configuration](/en/configuration/): every config.yaml field and the environment-variable overrides. - [Examples](/en/examples/): 11 runnable verification examples. # Core concepts ## 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 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](/en/deployment/). ## The interceptor chain Eight stages, fixed order: ``` recover → request-id → logger → load-shed → rate-limit → circuit-breaker → auth → validation ``` An 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 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 | Component | Notes | | --- | --- | | Config | Layered loading, fail-loud, masked secrets. See [Configuration](/en/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 | **Before relying on a battery** - Cache: the default in-memory backend is process-local — not shared across replicas, with no size cap or eviction. Fine for a single instance or local development; switch to `cache.driver: redis` when replicas must share a cache. See [Configuration](/en/configuration/). - Config: the placeholder `jwt_secret` is refused at startup, and secret fields are masked in logs and error output. Real secrets come in via environment variables. - MQ: `group_id` sets the delivery mode — empty is fanout (every subscriber receives each message), non-empty is load-balance (one handler per message). # gRPC gRPC is Gortexa's native protocol. External connections arrive on the same h2c port; the mux dispatches on `Content-Type: application/grpc` to grpc-go's `ServeHTTP` mode. ## Service definition and generated code Services are defined in proto. The built-in sample resource: ```protobuf service ResourceService { rpc CreateResource(CreateResourceRequest) returns (Resource); rpc GetResource(GetResourceRequest) returns (Resource); rpc ListResources(ListResourcesRequest) returns (ListResourcesResponse); rpc UpdateResource(UpdateResourceRequest) returns (Resource); rpc DeleteResource(DeleteResourceRequest) returns (google.protobuf.Empty); } ``` `gortexa regen` writes stubs to `gen/`; logic implementations live in internal packages. The two never mix. ## Calling the service For local grpcurl testing, enable reflection first (it is off by default): ```bash GORTEXA_SERVER__REFLECTION=true gortexa run ``` ```bash grpcurl -plaintext \ -H 'authorization: Bearer ' \ -d '{"id":"x"}' \ localhost:8080 resource.v1.ResourceService/GetResource ``` Without a token you get `UNAUTHENTICATED`: auth treats all three protocols the same way. ## Health The three-state health model maps to the standard gRPC Health protocol. Register real dependency checks with `app.Health().Register(...)` so `/readyz` and gRPC Health reflect actual DB, cache and MQ reachability. Each check runs under a 5-second timeout, so one slow dependency cannot stall the whole query. ## Reflection gRPC reflection is off by default. Enable it via config or an environment variable when needed: ```bash GORTEXA_SERVER__REFLECTION=true ``` Keep it off in production — see [Deployment](/en/deployment/). # HTTP/JSON The HTTP/JSON surface comes from grpc-gateway: routes and payload shapes are generated from `google.api.http` annotations in the proto. No handlers are written by hand. ## Route mapping Annotations sit on each rpc, for example in the sample resource: ```protobuf rpc GetResource(GetResourceRequest) returns (Resource) { option (google.api.http) = {get: "/v1/resources/{id}"}; } ``` The full mapping for the sample resource: | RPC | HTTP | | --- | --- | | CreateResource | `POST /v1/resources` | | GetResource | `GET /v1/resources/{id}` | | ListResources | `GET /v1/resources` | | UpdateResource | `PATCH /v1/resources/{id}` | | DeleteResource | `DELETE /v1/resources/{id}` | ## Calling and auth ```bash curl localhost:8080/v1/resources/x \ -H 'Authorization: Bearer ' ``` Without a token the response is 401. The gateway shares the interceptor chain with gRPC and MCP, and error bodies come from the unified error model — internal causes are not included. ## Request body limits The gateway caps request bodies at 1 MB with a 30-second read timeout: oversized bodies get 413, slow reads get 408. ## CORS and OpenAPI - `server.enable_cors: true` is only the master switch; the actual allowlist is `server.cors_origins`. Origins not listed there receive no CORS headers at all — enabling `enable_cors` without setting origins still blocks browsers. Use `"*"` to allow all, or list explicit origins (`GORTEXA_SERVER__CORS_ORIGINS` accepts a comma-separated override). Preflight OPTIONS requests are short-circuited. - With `server.openapi: true`, the OpenAPI spec is served at `/openapi.json`. The spec is a build artifact of `make gen`, read once at startup and served from memory. ## Response headers Response headers pass through an allowlist: internal headers do not leak, and `X-Request-Id` is preserved for correlation. # MCP The MCP (Model Context Protocol) bridge exposes proto methods annotated with `gortexa.ai.v1` as tools. An AI agent sends JSON-RPC to `/mcp`, and the bridge calls the original service over an in-process gRPC loopback — so AI calls pass through the same interceptor chain as everything else, auth and validation included. ## Annotating methods Add the `gortexa.ai.v1.ai_tool` annotation to an rpc: ```protobuf rpc GetResource(GetResourceRequest) returns (Resource) { option (google.api.http) = {get: "/v1/resources/{id}"}; option (gortexa.ai.v1.ai_tool) = { expose: true name: "get_resource" description: "Fetch a single resource by id and return its current state." read_only: true }; } ``` `read_only` and `destructive` feed the behaviour hints in the tool schema. Methods without `expose: true` do not appear in the tools list. ## Endpoint - Path: `/mcp` (Streamable HTTP) - Protocol versions: `2025-03-26` (default) and `2024-11-05` - Request bodies capped at 1 MB, read timeout 30 seconds - For browser-based clients, enable `server.enable_cors` and list the allowed origins in `server.cors_origins` — that allowlist is the DNS-rebinding guard; unlisted origins get no CORS headers ## tools/list ```bash curl -XPOST localhost:8080/mcp \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` Returns the tool schema for every annotated method. The list is memoized (about 285 ns per call), not recomputed on every request. ## tools/call Arguments to `tools/call` are serialized into a protobuf message and dispatched to the original service over the loopback; the result is converted back to JSON-RPC. A call that fails auth gets an unauthenticated error envelope, consistent with gRPC and HTTP behaviour. ## Schema export The same `gortexa.ai.v1` annotations can be exported offline in provider-ready formats: ```bash gortexa export --format=mcp gortexa export --format=openai # OpenAI strict function calling gortexa export --format=gemini ``` All three outputs are locked by golden tests: when the proto changes, the schemas change, and unexpected drift fails the tests. # Components Every framework package is importable directly after `go get`. Fetch the module: ```bash go get github.com/yshengliao/gortexa@v0.27.0 ``` This section lists the public components by category — each with its purpose, import path and a minimal usage snippet. All snippets are compile-verified against v0.27.0. - [Core](/en/components/core/): `kernel`, `config`, `interceptor`, `auth`, `apperr` — assembly, config, the interceptor chain, JWT and the error model. - [HTTP & MCP](/en/components/http-mcp/): `httpcompat`, `mcp` — the grpc-gateway HTTP/JSON layer and the MCP bridge. - [Batteries](/en/components/batteries/): `mq`, `cache`, `storage`, `client` — messaging, caching, the database pool and outbound clients. - [Observability & health](/en/components/observability/): `health`, `observability` — three-state health and OTLP logs/traces/metrics. For a bare minimal app (no gateway or MCP), see [Use as a module](/en/quickstart/#use-as-a-module-no-scaffold). Note: a project created by `gortexa create` already contains the framework source, and must never also `go get` the framework. This section is for using the framework as a plain library, without the scaffold. ## Full wiring `cmd/server/main.go` in the framework repo is the canonical example of assembling every component into a service, in this order: 1. `config.MustBuild(...)` — fail-loud config load. 2. `observability.SetupLogs / SetupTracing / SetupMetrics` — each returns a `ShutdownFunc`; `observability.NewGovernanceMetrics()`. 3. `auth.NewVerifier(...)`. 4. `interceptor.NewSet(interceptor.Config{...})`. 5. `kernel.New(WithConfig, WithLogger, WithInterceptors, WithStatsHandler(observability.ServerStatsHandler()), WithHTTPWrap(...), WithShutdownHook×3)`. 6. register services on `app.GRPCServer()`; `app.Health().Register(...)`. 7. `conn := app.Loopback()` → `httpcompat.NewServeMux` → `app.SetGateway(...)`. 8. `mcp.ServiceDescriptors(...)` → `mcp.NewBridge(conn, descs, apperr.Default, cfg.Observ)` → `app.SetMCPHandler(bridge.Handler())`. 9. `app.Run(ctx)`. `app.Loopback()` must be called after `kernel.New` and before `Run`; both the gateway and the MCP bridge dial it, so they share the one interceptor chain. ## testutil — test helpers (test-only) In-process test helpers: a bufconn gRPC server wired with the full interceptor chain, golden-file comparison, and goroutine-leak assertions. ```go import "github.com/yshengliao/gortexa/testutil" conn := testutil.NewTestServer(t, func(s *grpc.Server) { pb.RegisterThingServiceServer(s, myService) }, testutil.WithAuthSecret(secret)) // testutil.Golden(t, name, got) compares against testdata/.golden // set GORTEXA_UPDATE_GOLDEN to rewrite golden files (there is no -update flag) ``` # Core Fetch the module first: `go get github.com/yshengliao/gortexa@v0.27.0`. Every snippet below is compile-verified against v0.27.0. ## kernel 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. ```go 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)`. ## config 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`. ```go 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](/en/configuration/)). ## interceptor `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`. ```go 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)`. ## auth HS256 JWT verification/signing plus context helpers. The secret must be ≥ 32 bytes. ```go 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. ## apperr A single mapping table drives the gRPC status, HTTP status and MCP error envelope. `*apperr.Error` implements `GRPCStatus()`, so a handler just returns it. ```go 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). # HTTP & MCP Fetch the module first: `go get github.com/yshengliao/gortexa@v0.27.0`. Every snippet below is compile-verified against v0.27.0. ## httpcompat Builds a ServeMux whose error handler funnels through `apperr`, with a protojson marshaler and Authorization-header forwarding. ```go import ( "github.com/yshengliao/gortexa/apperr" "github.com/yshengliao/gortexa/httpcompat" ) gateway := httpcompat.NewServeMux(apperr.Default) // register grpc-gateway handlers against gateway + the loopback conn handler := httpcompat.MaxBodyBytes(gateway) // caps the body at 1 MiB handler = httpcompat.CORS(handler, cfg.Server) ``` `OpenAPIRoute(next, cfg, specPath)` mounts an OpenAPI spec. ## mcp Exposes RPCs annotated with `gortexa.ai.v1` as MCP tools, dispatched over the loopback through the same interceptor chain. ```go import ( "github.com/yshengliao/gortexa/apperr" "github.com/yshengliao/gortexa/mcp" ) descs, err := mcp.ServiceDescriptors("resource.v1.ResourceService") if err != nil { return err } bridge, err := mcp.NewBridge(conn, descs, apperr.Default, cfg.Observ) if err != nil { return err } bridge.SetAllowedOrigins(cfg.Server.CORSOrigins) app.SetMCPHandler(bridge.Handler()) ``` Export tool schemas offline with `mcp.ExportSchemas(format, descs)`, where format is `mcp` / `openai` / `gemini`. # Batteries Fetch the module first: `go get github.com/yshengliao/gortexa@v0.27.0`. Every snippet below is compile-verified against v0.27.0. ## mq NATS-backed pluggable pub/sub. `cfg.MQ.Driver` selects `nats` (core, at-most-once) or `jetstream` (durable, at-least-once, handlers must be idempotent). An empty `group_id` fans out; a non-empty one load-balances. ```go import "github.com/yshengliao/gortexa/mq" pub, sub, err := mq.New(cfg.MQ) if err != nil { return err } err = sub.Subscribe(ctx, "orders", func(ctx context.Context, m mq.Message) error { return handle(m.Value) }) err = pub.Publish(ctx, "orders", mq.Message{Value: []byte("hello")}) ``` `Message` carries `Key` / `Value` / `Headers`. You can also call `mq.NewNATS` / `mq.NewJetStream` to pick a driver directly. ## cache Process-local in-memory by default; `cfg.Cache.Driver: redis` opts into a distributed backend. ```go import ( "errors" "github.com/yshengliao/gortexa/cache" ) c, err := cache.New(cfg.Cache) if err != nil { return err } defer c.Close() if err := c.Set(ctx, "k", []byte("v"), time.Minute); err != nil { return err } b, err := c.Get(ctx, "k") if errors.Is(err, cache.ErrCacheMiss) { // key absent } ``` ## storage Builds a PgBouncer-safe pgx connection pool plus a query tracer that emits OTel DB spans. It returns a `*pgxpool.Pool`. ```go import ( "go.opentelemetry.io/otel" "github.com/yshengliao/gortexa/storage" ) tracer := storage.NewDBTracer(otel.GetTracerProvider()) pool, err := storage.NewPool(ctx, cfg.DB, tracer) // pass nil for no spans if err != nil { return err } defer pool.Close() ``` ## client Outbound client constructors with OTel instrumentation already wired in. ```go import "github.com/yshengliao/gortexa/client" conn, err := client.NewGRPCConn(client.GRPCClientConfig{Target: "svc:50051", Insecure: true}) if err != nil { return err } defer conn.Close() hc := client.NewHTTPClient(client.HTTPClientConfig{Timeout: 5 * time.Second}) // zero defaults to 30s ``` `Insecure: true` is for local development only; use TLS in production. # Observability & health Fetch the module first: `go get github.com/yshengliao/gortexa@v0.27.0`. Every snippet below is compile-verified against v0.27.0. ## health A three-state registry (Healthy / Degraded / Unhealthy; Degraded still counts as serving, only Unhealthy does not) plus a bridge to the standard gRPC health protocol. Normally you use the App's own registry via `app.Health()` rather than building your own. ```go import "github.com/yshengliao/gortexa/health" app.Health().Register("db", func(ctx context.Context) health.State { return health.Healthy }) overall := app.Health().Overall(ctx) // health.Healthy / Degraded / Unhealthy ``` Registered checks are aggregated by `/readyz`; `Names()`, `Snapshot(ctx)` and `State(ctx, name)` query them. ## observability The gRPC instrumentation is a StatsHandler (not interceptors). Tracing/metrics export via OTLP when an endpoint is configured, and are no-ops otherwise. Each `Setup*` returns a `ShutdownFunc`. ```go import "github.com/yshengliao/gortexa/observability" log, logShutdown, err := observability.SetupLogs(ctx, cfg.Log, cfg.Observ) traceShutdown, err := observability.SetupTracing(ctx, cfg.Observ) metricShutdown, err := observability.SetupMetrics(ctx, cfg.Observ) // pass observability.ServerStatsHandler() to kernel.WithStatsHandler, // and the three ShutdownFuncs to kernel.WithShutdownHook. ``` To export to an OTLP backend that needs an auth header (Uptrace, for example), the OTel SDK reads the standard `OTEL_EXPORTER_OTLP_HEADERS` env var (e.g. `uptrace-dsn=`), or relay through a collector. # Configuration Configuration lives in `etc/config.yaml` by default; if the file is absent, that layer is skipped and built-in defaults plus environment variables apply. Loading is otherwise fail-loud: a mistyped field or a non-compliant secret fails startup instead of running degraded. ## How configuration loads Configuration is three layers, each overriding the previous: 1. Built-in defaults (in code; e.g. `server.addr` is `:8080`, `cache.driver` is `memory`). 2. `etc/config.yaml` — point elsewhere with `GORTEXA_CONFIG=/path/to/config.yaml`; the layer is skipped when the file is absent. 3. Environment variables, in the form `GORTEXA_
__` (double underscore between section and field). When the same field is set in more than one layer, the environment variable wins. Real secrets are injected only through environment variables — never written into `config.yaml`, never committed. ## config.yaml ```yaml server: addr: ":8080" shutdown_timeout: 20s enable_cors: true cors_origins: [] # CORS allowlist; enable_cors is only the master switch — origins not listed here get no CORS headers openapi: true reflection: false # gRPC reflection off by default auth: jwt_secret: "dev-only-insecure-secret-change-me-please" issuer: "gortexa" # audience: "myapp" # optional: stamps aud into signed tokens and requires it — isolates services sharing a secret ttl: 1h db: dsn: "" # PostgreSQL DSN (Secret, masked in logs); inject via env: GORTEXA_DB__DSN max_conns: 10 cache: driver: "memory" # "memory" (default, process-local) or "redis" mq: driver: "nats" # "nats" (core, at-most-once) or "jetstream" (durable, at-least-once) url: "" # comma-separated servers, e.g. "nats://a:4222,nats://b:4222" group_id: "" # empty = fanout; non-empty = load-balance log: level: "info" format: "json" observ: service_name: "gortexa" service_version: "dev" # tracing_otlp: "localhost:4317" # OTLP trace endpoint; empty disables # metrics_otlp: "localhost:4317" # OTLP metrics endpoint; empty disables (rate-limit/circuit-breaker metrics need it) # logs_otlp: "localhost:4317" # OTLP log endpoint; empty disables otlp_insecure: true # local docker compose only; never true for remote sample_ratio: 1.0 genai_capture_content: false # when true, MCP tool-call arguments are recorded in telemetry # genai_mask_fields: ["password", "token", "secret", "authorization", "api_key"] ``` `genai_capture_content: true` sends AI tool-call arguments into telemetry; the default mask list only covers common field names. Combined with `otlp_insecure: true`, that content leaves the host in cleartext — do not enable both in production. ## Cache: memory and redis `cache.driver` selects the backend. The default `memory` needs no external service; `redis` is the only driver that reads `addr` and the fields below it. Any value other than `memory` or `redis` fails startup with `cache: unsupported driver`. #### memory (default) ```yaml cache: driver: "memory" ``` Process-local, no external dependency. Good for single-instance deployments and local development. #### redis ```yaml cache: driver: "redis" addr: "localhost:6379" password: "" # inject via env: GORTEXA_CACHE__PASSWORD db: 0 # The client tunables below are all optional; omit to use the built-in default dial_timeout: 5s # default 5s read_timeout: 3s # default 3s write_timeout: 3s # default 3s pool_size: 10 # default 10; must not be negative ``` On redis, the framework's built-in zero-dependency RESP client connects — no third-party redis package. Each tunable's zero value means "use the built-in default", so set only the one you need to change: | Field | Default | Purpose | | --- | --- | --- | | `dial_timeout` | 5s | Timeout for establishing a connection | | `read_timeout` | 3s | Timeout for reading a single reply | | `write_timeout` | 3s | Timeout for writing a single command | | `pool_size` | 10 | Connection pool size; `0` uses the default, negative is rejected | Behind a load balancer, shortening the timeouts fails fast instead of stalling the whole request; a busy service can raise `pool_size`. ## Database (PostgreSQL) `db.dsn` is the pgx connection string (a Secret field, always masked in logs); `db.max_conns` defaults to 10. Inject the real DSN via an environment variable: ```bash GORTEXA_DB__DSN=postgres://user:pass@pgbouncer:6432/app ``` The pool is PgBouncer-safe: `QueryExecModeExec` with statement and description caches disabled, so it works behind PgBouncer in transaction-pooling mode without prepared-statement errors. The development flow: schema goes in `db/migrations/`, SQL in `db/queries/`, and `make sqlc` generates type-safe query code into `internal/storage/db/`. `deploy/docker-compose.yaml` already ships postgres and PgBouncer for local verification. ## Environment overrides Any field can be overridden with an environment variable, using `GORTEXA_
__`: ```bash GORTEXA_SERVER__ADDR=:9090 GORTEXA_AUTH__JWT_SECRET= GORTEXA_CACHE__DRIVER=redis GORTEXA_CACHE__ADDR=redis.internal:6379 GORTEXA_SERVER__REFLECTION=true ``` ## Secret rules - The placeholder `dev-only-insecure-secret-change-me-please` is refused at startup — it cannot ship by accident. - JWT secrets shorter than 32 bytes are also refused. - Services sharing a secret and the default issuer accept each other's tokens — set a distinct `auth.audience` per service to isolate them (a token minted for A is rejected by B). - Secret fields such as the redis password and the MQ URL (which can embed credentials) are masked in logs and error output. - Real secrets are injected via environment variables, never committed. ## MQ semantics `mq.group_id` selects the delivery mode, identically on both drivers: | group_id | NATS (core) | JetStream | Semantics | | --- | --- | --- | --- | | Empty (default) | Subscribe | Ephemeral consumer | Fanout: every subscriber receives each message | | Non-empty | QueueSubscribe | Durable consumer (named after group_id, acked only after the handler succeeds) | Load-balance: one handler per message | The drivers differ in the delivery guarantee: core NATS is at-most-once (no redelivery — a message whose handler fails is gone), while `driver: "jetstream"` is at-least-once — publish blocks on the server's storage ack and a handler error triggers a delayed redelivery, so handlers must be idempotent. JetStream needs the server started with `-js`; the framework lazily creates a stream per topic (24h retention), and an operator may pre-create a stream with different retention — the framework adopts it and never rewrites an existing stream. ## Diagnostics ```bash gortexa doctor # check the Go toolchain and proto tools ``` # Deployment The framework repo ships reference material under `deploy/`: a multi-stage distroless Dockerfile (proto regenerated inside the image with pinned tools) and a docker-compose with otelcol, Prometheus, Grafana and PgBouncer configuration. The three items below are deliberately left to the operator — the framework does not do them for you. **Wire these before going live** TLS termination, authorization and readiness dependency checks are not done for you. Skip any one and a service facing the public internet silently loses transport security or authorization — no error, no warning. ## Scaffold secret preflight A new scaffold carries local development configuration. Before the first git add, Docker build, or CI handoff, verify that every secret is externalized. **Do not leave a usable JWT signing secret in version control or an image layer.** - Supply real secrets through a secret manager, mounted file, or runtime environment using GORTEXA_AUTH__JWT_SECRET. - Keep only an unusable placeholder or a secret-free example configuration in the repository; ignore local overrides. - Make sure both the Docker build context and runtime image exclude local secret configuration, and verify the image starts only with runtime injection. - Plan rotation, issuer, and audience; services sharing a secret should use different audiences. This is a deployment gate, not optional later work. Likewise, the scaffold's in-memory resource is only a demo; establish real storage, cache, and MQ lifecycle before treating readiness as meaningful. ## Terminate TLS in front The service port is cleartext h2c; the server does no TLS itself. Bearer tokens crossing this port are unencrypted. Put the service behind a reverse proxy, service mesh or load balancer that terminates TLS. Do not expose the h2c port to the public internet. ## Wire readiness to real dependencies `/readyz` only reflects that the process is alive until you register real checks with `app.Health().Register(...)`: ```go app.Health().Register("postgres", func(ctx context.Context) health.State { if err := pool.Ping(ctx); err != nil { return health.Unhealthy } return health.Healthy }) ``` Health probes can target a private ops port, separate from the public one: `kernel.WithAdminListener(addr)` serves only `/healthz` and `/readyz` on a separate plain-HTTP port, so an orchestrator can probe health without reaching the public h2c port (`app.AdminAddr()` reports the bound address). To expose pprof or a custom ops handler on a separate port, use `kernel.WithExtraListener(lis, h)`. Both are opt-in; the main h2c port's single-port multiplexing is unchanged. Each check runs under a 5-second timeout, so a single slow dependency cannot stall the query. ## Authorization is the application's job The framework does authentication only: it verifies the JWT signature and puts the claims into the context. Who may do what (roles, permission policy) is decided by the application, in handlers or its own interceptor: ```go claims, ok := auth.ClaimsFrom(ctx) if !ok || !slices.Contains(claims.Roles, "admin") { // reject } ``` ## Graceful shutdown On SIGTERM the server drains in-flight requests within `server.shutdown_timeout` (default 20s) before exiting. Interruption under authenticated load is verified by [example 05](/en/examples/resilience/). ## Production flags - Keep `server.reflection` at `false`: do not expose the service structure. - `observ.otlp_insecure` must be `false` for any remote collector. - Real secrets use runtime injection and must not exist in Git or an image layer — see [Configuration](/en/configuration/). # Examples overview These examples are the framework's verification matrix: 11 independent scenarios, each proving one concrete behaviour with a `run.sh`. The sub-pages below list the key checks and copyable snippets, so you can verify the same properties in your own project. ## Shared setup - **Test token**: scenarios that pass auth need a JWT. Sign one with your project's `auth` — see [Getting a dev token](/en/quickstart/#getting-a-dev-token). The example set also ships a `tools/mint` HS256 minter, resolving the secret from `-secret` → `GORTEXA_AUTH__JWT_SECRET` → `etc/config.yaml`. - **Ports**: server scenarios run from `:18080` up (each uses a distinct port to avoid clashes), polling `/readyz` before probing. - **Cleanup**: each scenario tears down its server via `trap cleanup EXIT` or `docker compose stop`; all are re-runnable. - **Dependencies**: only the two NATS-family scenarios (core and JetStream) need docker; the rest are pure local binaries. ## Sub-pages - [Developer flow](/en/examples/workflow/): the full scaffold loop, one entity across three protocols. - [Security & boundaries](/en/examples/security/): auth rejections leak nothing, secret startup safety, the header allowlist, the reflection gate. - [Contract & resilience](/en/examples/resilience/): the buf breaking gate, graceful drain on SIGTERM. - [AI & messaging](/en/examples/messaging/): export tool schemas, NATS/JetStream integration. - [Full example](/en/examples/full-example/): after `go get`, wire the public packages into a service with a complete, annotated `main.go`. ## Suggested path 1. [Developer flow](/en/examples/workflow/) — walk the full development loop once. 2. See one proto become three protocols. 3. [Security & boundaries](/en/examples/security/) — the error model and interceptor chain under adversarial input. # Developer flow ## 01 scaffold-smoke — the full developer loop Proves create → gen → build → test runs end to end and still holds after adding a second API. This is a scaffold transport smoke test; checking that create replaces the placeholder does **not** authorize committing or baking the resulting usable secret into an image. ```bash gortexa create myapp --module github.com/me/myapp cd myapp make gen # buf lint → breaking → generate go build ./... && go test ./... gortexa gen shop/v1 Product # add a second API make gen && go build ./... && go test ./... ``` Key assertions: `proto/shop/v1/product.proto` and `internal/logic/product.go` are generated, `cmd/server/main.go` is wired for product, and finding `dev-only-insecure-secret-change-me-please` in `etc/config.yaml` fails the run. That only proves local startup; before Git or Docker, move to external secret injection and replace the in-memory logic with a real data layer. ## 02 multi-protocol-e2e — one entity across three protocols On a single h2c port (`:18080`), an entity created over gRPC is fetchable over HTTP, and one created over MCP shows up in the list — all three share one store. `$TOK` is a dev token, `$SCHEMA` is the descriptor from `buf build`. Create over gRPC: ```bash buf curl --protocol grpc --http2-prior-knowledge --schema $SCHEMA \ -H "authorization: Bearer $TOK" \ -d '{"resource":{"name":"grpc-entity","owner":"owner-e2e"}}' \ http://localhost:18080/resource.v1.ResourceService/CreateResource ``` Fetch the same id over the HTTP gateway, then create a third over MCP `tools/call`: ```bash curl -H "authorization: Bearer $TOK" http://localhost:18080/v1/resources/$RID # 200, same entity curl -XPOST http://localhost:18080/mcp -H 'Content-Type: application/json' \ -H "authorization: Bearer $TOK" \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call", "params":{"name":"create_resource", "arguments":{"resource":{"name":"mcp-entity","owner":"owner-e2e"}}}}' ``` Listing `owner-e2e` at the end shows the entities created over gRPC, HTTP and MCP together — proof the three surfaces sit on one store. # Security & boundaries ## 03 security-probe — rejection paths leak nothing Adversarial input across all three protocols, asserting no error body carries internal detail. ```bash # missing/bad/expired token → 401 UNAUTHENTICATED curl -H "authorization: Bearer xxx" http://localhost:18080/v1/resources # 401 # evil origin → response carries no Access-Control-Allow-Origin curl -D - -H "Origin: https://evil.example" http://localhost:18080/v1/resources # >1MB body to /mcp → 413, clean curl -XPOST http://localhost:18080/mcp -H 'Content-Type: application/json' \ --data-binary @big.json # 413 # bad version / unknown tool / huge id / not-JSON → structured JSON-RPC error, no panic curl -XPOST http://localhost:18080/mcp -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"no_such_tool","arguments":{}}}' ``` A final scan over every rejection body asserts no stack traces, internal paths, or unmasked causes. ## 06 config-matrix — JWT secret startup safety A placeholder or too-short secret is refused at startup (fail-loud), and the error never echoes the secret itself. ```bash GORTEXA_AUTH__JWT_SECRET="dev-only-insecure-secret-change-me-please" ./server # exit 1, no port bound GORTEXA_AUTH__JWT_SECRET='tooshort' ./server # exit 1 GORTEXA_AUTH__JWT_SECRET="<45-byte real secret>" ./server # starts; env overrides file secret ``` Rejection happens before listen, so (a) and (b) never bind a port. ## 08 header-leak-probe — response header allowlist Response headers stay within a fixed allowlist: no `Grpc-Metadata-*` passthrough, while `X-Request-Id` must survive (the allowlist must not overshoot). ```bash curl -D - -H "authorization: Bearer $TOK" http://localhost:18082/v1/resources/x # asserts: no grpc-metadata-* headers, nothing outside the allowlist, X-Request-Id present ``` ## 09 reflection-probe — reflection off by default, config-gated gRPC reflection is off by default; the same call succeeds only once it is enabled via env. ```bash buf curl --protocol grpc --http2-prior-knowledge --reflect ... # default: fails (service not found) GORTEXA_SERVER__REFLECTION=true ./server buf curl --protocol grpc --http2-prior-knowledge --reflect ... # enabled: succeeds ``` Keep it off in production — see the production flags in [Deployment](/en/deployment/). # Contract & resilience ## 04 breaking-gate — the contract-breaking gate Proves `gortexa regen`'s buf breaking gate blocks compatibility-breaking proto edits and allows additive ones. This scenario also exercises `regen`'s `isGitToplevel()` path (a macOS `/tmp` → `/private/tmp` symlink once made the gate skip silently; fixed). ```bash gortexa create bg --module github.com/me/bg --repo "$REPO" --ref main cd bg && make gen # first gen: no main ref yet, gate skips (expected) git add -A && git commit -m base && git branch -M main # delete Resource's owner field (tag 3) — a breaking edit gortexa regen # non-zero exit: gate BLOCKS the breaking edit ``` An additive edit (a new field) passes `regen` cleanly. The gate reads proto from the git `main` ref, not from `gen/`. ## 05 resilience-shutdown — graceful drain on SIGTERM Authenticated mixed load is interrupted mid-flight with SIGTERM; the server must drain in-flight requests within `shutdown_timeout`, exit 0, not panic, and recover on restart. ```bash ./server & # a real binary, so SIGTERM lands on the server (not a go run wrapper) ./loadgen ... # authenticated load kill -TERM $SRV # SIGTERM while load is in flight wait $SRV # exit 0, drain complete, drain time <= shutdown_timeout ``` A real binary (not `go run`) is used so the exit code is unambiguous — `go run` wraps the server in an extra process. # AI & messaging ## 10 export-cli — AI tool schema export `gortexa export` emits valid JSON schemas in three provider formats for every `gortexa.ai.v1`-annotated method, without a running server. ```bash for fmt in mcp openai gemini; do gortexa export --format "$fmt" # each emits valid JSON containing the resource tools done gortexa export --format nope # unknown format: non-zero exit, clean failure ``` All three outputs are locked by golden tests: change the proto and the schemas change, so drift is caught. ## 11 nats-integration — against a real NATS broker Starts a real NATS broker from the scenario's own `docker-compose.yaml` and runs the mq core-NATS integration tests. With `NATS_URL` set, the tests connect to the container's broker rather than an in-process embedded server, and a connection failure fails — a skip counts as failure too. ```bash docker compose -f docker-compose.yaml up -d nats NATS_URL=nats://127.0.0.1:4222 go test -tags integration -race -count=1 \ -run 'TestNATS' ./mq/ ``` Asserts `TestNATSPubSub` and `TestNATSQueueGroupLoadBalance` PASS — the latter verifies the load-balance semantics of a non-empty `group_id` (see [Configuration](/en/configuration/#mq-semantics)). ## 12 jetstream-integration — against a real JetStream broker Same structure as 11 with the JetStream driver: the broker starts with `-js` and the tests use the `JETSTREAM_URL` env var. Beyond the fanout and load-balance semantics shared with core NATS, it verifies the JetStream-specific at-least-once property — a handler error triggers a delayed redelivery. ```bash docker compose -f docker-compose.yaml up -d nats # nats:2 with command: ["-js"] JETSTREAM_URL=nats://127.0.0.1:4222 go test -tags integration -race -count=1 \ -run 'TestJetStream' ./mq/ ``` Asserts `TestJetStreamPubSub`, `TestJetStreamQueueGroupLoadBalance` and `TestJetStreamRedeliveryOnHandlerError` PASS — the last one pins the at-least-once redelivery (which is why handlers must be idempotent, see [Configuration](/en/configuration/#mq-semantics)). # Full example: assembling a service as a module Wire the public packages into one runnable service — after `go get`, no scaffold. For each package's usage see [Components](/en/components/). Fetch the module: ```bash go get github.com/yshengliao/gortexa@v0.27.0 ``` How each component is enabled (all driven by config): - **config**: `MustBuild` layers `etc/config.yaml` and the environment (`GORTEXA_` prefix). - **observability**: exports OTLP only when `observ.tracing_otlp` / `metrics_otlp` / `logs_otlp` is set; a no-op otherwise. - **auth**: set `auth.jwt_secret` (≥ 32 bytes) and `auth.issuer`. - **interceptor**: `NewSet` builds the fixed eight-stage chain. - **health**: `app.Health().Register` adds a check that `/readyz` aggregates. - **gateway (HTTP/JSON)**: `httpcompat.NewServeMux` plus your gateway handler. - **mcp**: `mcp.NewBridge` turns `gortexa.ai.v1`-annotated RPCs into MCP tools. - **mq / cache / storage / client**: once `mq.url` / `cache.driver: redis` / `db.dsn` are set, use `mq.New` / `cache.New` / `storage.NewPool` / `client.NewGRPCConn` inside your handlers. The complete `main.go` (your own generated service is a commented placeholder; the framework wiring in this snippet is compile-verified against v0.27.0): ```go package main import ( "context" "fmt" "net/http" "os" "os/signal" "strings" "syscall" "time" "google.golang.org/protobuf/reflect/protoreflect" "github.com/yshengliao/gortexa/apperr" "github.com/yshengliao/gortexa/auth" "github.com/yshengliao/gortexa/config" "github.com/yshengliao/gortexa/health" "github.com/yshengliao/gortexa/httpcompat" "github.com/yshengliao/gortexa/interceptor" "github.com/yshengliao/gortexa/kernel" "github.com/yshengliao/gortexa/mcp" "github.com/yshengliao/gortexa/observability" // billingpb "github.com/me/myapp/gen/billing/v1" // your own generated service ) func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, "fatal:", err) os.Exit(1) } } func run() error { // 1. Config — layered: defaults < YAML < .env < env (GORTEXA_ prefix). // MustBuild fails loud on a missing/short jwt_secret or blank issuer. cfg := config.MustBuild(config.WithConfigFile("etc/config.yaml")) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() // 2. Observability — logs/traces/metrics. Each exports over OTLP only when // its observ.*_otlp endpoint is set, and is a no-op otherwise. Each // returns a ShutdownFunc that flushes buffered telemetry on exit. log, logShutdown, err := observability.SetupLogs(ctx, cfg.Log, cfg.Observ) if err != nil { return fmt.Errorf("setup logs: %w", err) } traceShutdown, err := observability.SetupTracing(ctx, cfg.Observ) if err != nil { return fmt.Errorf("setup tracing: %w", err) } metricShutdown, err := observability.SetupMetrics(ctx, cfg.Observ) if err != nil { return fmt.Errorf("setup metrics: %w", err) } govMetrics, err := observability.NewGovernanceMetrics() if err != nil { return fmt.Errorf("governance metrics: %w", err) } // 3. Auth — HS256 verifier from the configured secret / issuer / audience. verifier, err := auth.NewVerifier([]byte(cfg.Auth.JWTSecret.Reveal()), cfg.Auth.Issuer, cfg.Auth.Audience) if err != nil { return fmt.Errorf("auth verifier: %w", err) } // 4. Interceptor chain — recover → request-id → logger → load-shed → // rate-limit → circuit-breaker → auth → validation. Health probes carry // no token, so skip auth for them. set, err := interceptor.NewSet(interceptor.Config{ Logger: log, Verifier: verifier, Metrics: govMetrics, AuthSkip: func(method string) bool { return strings.HasPrefix(method, "/grpc.health.") }, RateLimit: interceptor.RateLimitConfig{RPS: 200, Burst: 100, TTL: 10 * time.Minute}, CircuitBreaker: interceptor.CBConfig{MaxFailures: 5, OpenInterval: 10 * time.Second, HalfOpenMax: 2}, LoadShedding: interceptor.LoadSheddingConfig{MaxInflight: 1024}, }) if err != nil { return fmt.Errorf("interceptors: %w", err) } // 5. Kernel — the App owns the single h2c port. StatsHandler carries OTel // (not interceptors); HTTPWrap adds CORS around the HTTP surface; // shutdown hooks flush the three telemetry providers on exit. app, err := kernel.New( kernel.WithConfig(cfg), kernel.WithLogger(log), kernel.WithInterceptors(set), kernel.WithStatsHandler(observability.ServerStatsHandler()), kernel.WithHTTPWrap(func(h http.Handler) http.Handler { return httpcompat.CORS(h, cfg.Server) }), kernel.WithShutdownHook(traceShutdown), kernel.WithShutdownHook(metricShutdown), kernel.WithShutdownHook(logShutdown), ) if err != nil { return fmt.Errorf("build app: %w", err) } // 6. Register your generated gRPC service, plus a health check that /readyz // aggregates. StartMetricsExport publishes component health as an OTel gauge. // billingpb.RegisterInvoiceServiceServer(app.GRPCServer(), billing.NewInvoiceService()) app.Health().Register("self", func(context.Context) health.State { return health.Healthy }) app.Health().StartMetricsExport(ctx, govMetrics, 0) // 7. Loopback — the gateway and MCP bridge dial the in-process server, so // HTTP/JSON and MCP calls go through the same interceptor chain as gRPC. conn, err := app.Loopback() if err != nil { return fmt.Errorf("loopback: %w", err) } // 8. HTTP/JSON via grpc-gateway. Register your service's gateway handler, // then cap the body at 1 MiB. gateway := httpcompat.NewServeMux(apperr.Default) // _ = billingpb.RegisterInvoiceServiceHandler(ctx, gateway, conn) app.SetGateway(httpcompat.MaxBodyBytes(gateway)) // 9. MCP bridge — exposes every gortexa.ai.v1-annotated RPC in the listed // services as an MCP tool. SetAllowedOrigins is the DNS-rebinding guard. descs, err := mcp.ServiceDescriptors(mcpServices()...) if err != nil { return fmt.Errorf("mcp descriptors: %w", err) } bridge, err := mcp.NewBridge(conn, descs, apperr.Default, cfg.Observ) if err != nil { return fmt.Errorf("mcp bridge: %w", err) } bridge.SetAllowedOrigins(cfg.Server.CORSOrigins) app.SetMCPHandler(bridge.Handler()) // 10. Run until SIGINT/SIGTERM; graceful shutdown drains in-flight work and // runs the shutdown hooks. Optional batteries — mq.New(cfg.MQ), // cache.New(cfg.Cache), storage.NewPool(ctx, cfg.DB, tracer), // client.NewGRPCConn(...) — are wired here as your handlers need them. log.Info("starting", "addr", cfg.Server.Addr) return app.Run(ctx) } // mcpServices lists the services exposed over the MCP bridge (and schema export). func mcpServices() []protoreflect.FullName { return []protoreflect.FullName{ "billing.v1.InvoiceService", } } ```