{"id":30615636,"url":"https://github.com/alexfalkowski/go-service","last_synced_at":"2026-06-03T07:01:25.260Z","repository":{"id":37890908,"uuid":"363670615","full_name":"alexfalkowski/go-service","owner":"alexfalkowski","description":"A framework to build services in go.","archived":false,"fork":false,"pushed_at":"2026-05-31T14:39:24.000Z","size":31524,"stargazers_count":34,"open_issues_count":0,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2026-05-31T15:15:32.937Z","etag":null,"topics":["cache","cloudevents","golang","grpc","http","mvc","postgres","redis","rpc"],"latest_commit_sha":null,"homepage":"https://alexfalkowski.github.io/go-service","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alexfalkowski.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2021-05-02T14:28:46.000Z","updated_at":"2026-05-31T14:39:27.000Z","dependencies_parsed_at":"2026-04-20T06:02:38.160Z","dependency_job_id":null,"html_url":"https://github.com/alexfalkowski/go-service","commit_stats":null,"previous_names":[],"tags_count":1706,"template":false,"template_full_name":null,"purl":"pkg:github/alexfalkowski/go-service","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexfalkowski%2Fgo-service","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexfalkowski%2Fgo-service/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexfalkowski%2Fgo-service/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexfalkowski%2Fgo-service/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alexfalkowski","download_url":"https://codeload.github.com/alexfalkowski/go-service/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexfalkowski%2Fgo-service/sbom","scorecard":{"id":181408,"data":{"date":"2025-08-11","repo":{"name":"github.com/alexfalkowski/go-service","commit":"cab09cf2fb30e1c7b90af4e18a7c6bbad8165163"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":6,"checks":[{"name":"Maintained","score":10,"reason":"30 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Code-Review","score":0,"reason":"Found 0/19 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":10,"reason":"SAST tool is run on all commits","details":["Info: all commits (18) are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-16T18:56:04.260Z","repository_id":37890908,"created_at":"2025-08-16T18:56:04.260Z","updated_at":"2025-08-16T18:56:04.260Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33852295,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-03T02:00:06.370Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cache","cloudevents","golang","grpc","http","mvc","postgres","redis","rpc"],"created_at":"2025-08-30T08:06:11.186Z","updated_at":"2026-06-03T07:01:25.246Z","avatar_url":"https://github.com/alexfalkowski.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"![Gopher](assets/gopher.png)\n[![CircleCI](https://circleci.com/gh/alexfalkowski/go-service.svg?style=shield)](https://circleci.com/gh/alexfalkowski/go-service)\n[![codecov](https://codecov.io/gh/alexfalkowski/go-service/graph/badge.svg?token=AGP01JOTM0)](https://codecov.io/gh/alexfalkowski/go-service)\n[![Go Report Card](https://goreportcard.com/badge/github.com/alexfalkowski/go-service/v2)](https://goreportcard.com/report/github.com/alexfalkowski/go-service/v2)\n[![Go Reference](https://pkg.go.dev/badge/github.com/alexfalkowski/go-service/v2.svg)](https://pkg.go.dev/github.com/alexfalkowski/go-service/v2)\n[![Stability: Active](https://masterminds.github.io/stability/active.svg)](https://masterminds.github.io/stability/active.html)\n\n# 🧰 Go Service\n\n`github.com/alexfalkowski/go-service/v2` is an opinionated framework/library for building Go services with consistent wiring for configuration, DI, transports, telemetry, crypto, etc.\n\nThis repo is primarily a **library of packages** (no top-level `cmd/` binary). Services built on top typically define their own `main` package elsewhere and import this module.\n\nMost services are expected to be bootstrapped from [`go-service-template`](https://github.com/alexfalkowski/go-service-template) and to compose the high-level module bundles from this repository. That is the primary supported path. Lower-level package-by-package composition is still available, but it is an advanced mode and may require extra manual registration.\n\n---\n\n## 🧩 Dependency Injection (Fx)\n\nThe framework is designed around dependency injection and uses [Uber Fx](https://github.com/uber-go/fx) (and Dig under the hood). Most subsystems expose Fx modules that you compose into your service.\n\nIf you are new to Fx, their docs/examples are worth reading first.\n\n### Module bundles\n\nThe module package exposes three top-level bundles:\n\n- `module.Library` for shared foundations (env, compress, encoding, crypto, time, sync buffer-pool wiring, id)\n- `module.Server` for server processes (Library + config, transports, telemetry, debug, health, etc.)\n- `module.Client` for short-lived/batch/client processes (Library + config, telemetry, sql, hooks, etc.)\n\nThese bundles are the intended default for services generated from `go-service-template`. They handle the internal registration expected by the framework so most services do not need to wire lower-level transport or lifecycle helpers manually.\n\n### Minimal CLI bootstrap example\n\nThis repository is a library, so your binary is usually in another module. A typical `main` uses `cli.Application` and composes module bundles:\n\n```go\npackage main\n\nimport (\n    \"github.com/alexfalkowski/go-service/v2/cli\"\n    \"github.com/alexfalkowski/go-service/v2/context\"\n    \"github.com/alexfalkowski/go-service/v2/module\"\n    \"github.com/alexfalkowski/go-service/v2/os\"\n)\n\nfunc main() {\n    app := cli.NewApplication(func(commander cli.Commander) {\n        serve := commander.AddServer(\"serve\", \"Run the service\", module.Server)\n        serve.AddConfig(\"file:./config.yml\") // adds the `-config` / `-c` config flag with this default\n    })\n\n    os.Exit(app.RunCode(context.Background()))\n}\n```\n\nUse `app.RunCode(context.Background())` from `main` when exiting the process. It\nreturns `os.ExitCodeSuccess` on success, returns a requested non-zero shutdown\nexit code such as `os.ExitCodeServeFailure`, and returns `os.ExitCodeFailure`\nfor other errors. Use `app.Run(context.Background())` in tests or embedding code\nthat needs to inspect the returned error.\n\n---\n\n## 🖥️ CLI\n\nServices commonly expose two command shapes:\n\n- **Server**: long-running daemon process\n- **Client**: short-lived control/admin process\n\nThe framework uses [acmd](https://github.com/cristalhq/acmd). Your service’s `main` typically wires Fx modules + commands.\n\n\u003e This repo intentionally does not ship a ready-to-run `main` — it provides the building blocks. In normal usage those building blocks are consumed through `go-service-template` plus `module.Server` / `module.Client`, not by wiring every subsystem manually.\n\n---\n\n## 🗂️ Repository layout\n\nThe repo is intentionally split between high-level service composition and lower-level reusable helpers:\n\n- `module/` exposes the opinionated Fx bundles (`Library`, `Server`, `Client`)\n- `config/` defines the standard top-level config shape plus projections used by module wiring\n- feature packages such as `cache/`, `crypto/`, `database/sql/`, `feature/`, `telemetry/`, `time/`, and `id/` provide config, constructors, and Fx modules for a subsystem\n- `net/...` contains lower-level protocol helpers and reusable primitives (`net/http`, `net/grpc`, metadata/header helpers, gRPC health protocol aliases, and `net/server`)\n- `transport/...` contains the higher-level service transport layer: composed HTTP/gRPC stacks, policy middleware, operational endpoints, and transport-specific modules\n- `internal/test/` contains the shared test world and fixtures used across packages\n\nAs a rule of thumb: if you want protocol primitives or shared helpers, start in `net/...`; if you want service wiring and middleware policy, start in `transport/...`. Shared metadata, header, and lifecycle helpers live under `net/...`, including `net/http/meta`, `net/grpc/meta`, `net/header`, and `net/server.Register`.\n\nFor most service authors, the right starting point is still the high-level module bundles rather than these lower-level packages directly.\n\n---\n\n## ⚙️ Configuration\n\n### Supported config formats\n\nThe config decoder supports:\n\n- JSON\n- HJSON (`github.com/hjson/hjson-go/v4`)\n- TOML (`github.com/BurntSushi/toml`)\n- YAML (`go.yaml.in/yaml/v3`)\n\n### Selecting the config source (`-config` / `-c` flags)\n\nConfig input is routed by flags called `-config` and `-c`:\n\n- `file:\u003cpath\u003e`\n  Read config from a file at `\u003cpath\u003e`; parser is selected from the file extension (`.json`, `.hjson`, `.yaml`, `.yml`, `.toml`).\n\n- `env:\u003cENV_VAR\u003e`\n  Read config from env var `\u003cENV_VAR\u003e`. The env var value must be formatted as:\n\n  `\"\u003cextension\u003e:\u003cbase64-content\u003e\"`\n\n  Example format: `yaml:ZW52aXJvbm1lbnQ6IGRldmVsb3BtZW50Cg==`\n\n  Example commands:\n\n  ```sh\n  # Linux (GNU base64)\n  export SERVICE_CONFIG=\"yaml:$(base64 -w 0 \u003c ./config.yml)\"\n  ./your-service serve -config env:SERVICE_CONFIG\n  ```\n\n  ```sh\n  # macOS/BSD base64\n  export SERVICE_CONFIG=\"yaml:$(base64 \u003c ./config.yml | tr -d '\\n')\"\n  ./your-service serve -c env:SERVICE_CONFIG\n  ```\n\n  HJSON works the same way, for example `hjson:\u003cbase64-content\u003e`.\n\n  The repository helper `make kind=status encode-config` uses GNU `base64 -w 0`; on macOS/BSD, use `base64 | tr -d '\\n'` for the equivalent single-line payload.\n\n- Otherwise (no `file:`/`env:` prefix), the decoder falls back to **default lookup**, searching for:\n\n  `\u003cserviceName\u003e.{yaml,yml,hjson,toml,json}`\n\n  Default lookup checks extensions first (`.yaml`, `.yml`, `.hjson`, `.toml`, `.json`), and for each extension checks:\n  - executable directory\n  - `$XDG_CONFIG_HOME/\u003cserviceName\u003e/` (via `os.UserConfigDir()`)\n  - `/etc/\u003cserviceName\u003e/`\n\n\u003e [!IMPORTANT]\n\u003e Because the user config directory is part of that search, runtimes using default lookup are expected to provide `HOME` or `XDG_CONFIG_HOME`. Services that cannot rely on those environment variables should pass an explicit `-config file:\u003cpath\u003e` or `-config env:\u003cENV_VAR\u003e` source.\n\n### Typed decoding and validation\n\nAt runtime, services typically decode into a struct (often embedding `config.Config`) and validate it using `go-playground/validator`.\n\nThe library provides a helper `config.NewConfig[T]` which:\n\n- decodes into `*T`\n- rejects an “empty” decoded value (guards against starting with a zero-value config)\n- validates the decoded config\n\nExample:\n\n```go\ntype AppConfig struct {\n    config.Config `yaml:\",inline\" json:\",inline\" toml:\",inline\"`\n}\n\nfunc loadConfig(decoder config.Decoder, validator *config.Validator) (*AppConfig, error) {\n    return config.NewConfig[AppConfig](decoder, validator)\n}\n```\n\n### The standard top-level config shape\n\nThe canonical top-level config type is `config.Config` (in `config/config.go`). It contains:\n\n- `debug`, `cache`, `crypto`, `feature`, `hooks`, `id`, `sql`, `telemetry`, `time`, `transport`, `environment`\n\nMost sub-configs are optional pointers. Conventionally, `nil` means **disabled**.\n\n---\n\n## 🔐 Source strings (secrets, DSNs, paths)\n\nMany fields accept a *source string* rather than only a literal:\n\n- `env:NAME` → read from environment variable `NAME` (fails if `NAME` is unset; resolves to an empty value if `NAME` is explicitly set to `\"\"`)\n- `file:/path/to/thing` → read from filesystem\n- otherwise → treat as literal string\n\nThis is used for secrets and key material (TLS keys, HMAC keys, webhook secrets, SQL DSNs, etc).\n\nExample:\n\n```yaml\nhooks:\n  secret: env:WEBHOOK_SECRET\n```\n\n---\n\n## 🌍 Environment\n\nTop-level environment is:\n\n```yaml\nenvironment: development\n```\n\nThis is an `env.Environment` value used to drive environment-specific behavior in services.\n\n---\n\n## 🗜️ Compression\n\nCompression kinds used by subsystems that support compression:\n\n- `none`\n- `zstd`\n- `s2`\n- `snappy`\n\n---\n\n## 🧾 Encoders\n\nEncoding kinds used by subsystems that support encoding:\n\n- `json`\n- `hjson`\n- `toml`\n- `yaml`, `yml`\n- `msgpack`\n- `proto`, `protobuf`, `pb`, `protobin`, `pbbin`\n- `protojson`, `pbjson`\n- `prototext`, `prototxt`, `pbtxt`\n- `gob`\n- `plain`, `octet-stream`, `markdown`\n\n\u003e [!NOTE]\n\u003e - `plain`, `octet-stream`, and `markdown` all map to the bytes passthrough encoder.\n\u003e - Protobuf binary/text/JSON kinds have multiple aliases; the list above reflects the built-in registry.\n\n---\n\n## 💾 Cache\n\nCache configuration is defined in `cache/config.Config`:\n\n```yaml\ncache:\n  kind: redis\n  compressor: zstd\n  encoder: json\n  max_size: 4MB\n  options:\n    url: env:CACHE_URL\n```\n\n\u003e [!NOTE]\n\u003e - Built-in driver kinds in this repo are `redis` and `sync`.\n\u003e - Unknown `kind` values return `cache/driver.ErrNotFound`.\n\u003e - Unknown or empty `compressor` values fall back to `none`.\n\u003e - For normal values, unknown or empty `encoder` values fall back to `json`.\n\u003e - Cache operations use `plain` for `io.WriterTo`/`io.ReaderFrom` stream values and `proto` for protobuf messages, regardless of the configured `encoder`.\n\u003e - `max_size` limits encoded cache values before compression, after compression, and after decompression. A zero value uses the default `4MB`.\n\u003e - `options` is backend-specific and decoded as `map[string]any`.\n\n---\n\n## 🚩 Feature flags (OpenFeature)\n\nThe `feature.Config` embeds client-side config (`config/client.Config`), so it supports:\n\n- `address`\n- `timeout`\n- `retry`\n- `limiter`\n- `tls`\n- `token`\n- `options`\n\nExample:\n\n```yaml\nfeature:\n  address: localhost:9000\n  timeout: 10s\n  retry:\n    backoff: 100ms\n    timeout: 1s\n    attempts: 3\n  tls:\n    cert: file:test/certs/client-cert.pem\n    key: file:test/certs/client-key.pem\n    ca: file:test/certs/rootCA.pem\n    server_name: localhost\n```\n\n\u003e [!NOTE]\n\u003e - Presence enables the feature subsystem configuration-wise, but this repository does not construct a built-in OpenFeature provider from this config.\n\u003e - Services that need a remote or custom provider should use `feature.Config` in their own provider constructor and provide the resulting `openfeature.FeatureProvider` in DI; `feature.Module` registers that supplied provider with the OpenFeature SDK lifecycle.\n\n---\n\n## 🪝 Webhooks (Standard Webhooks)\n\nConfigured via `hooks.Config`:\n\n```yaml\nhooks:\n  secret: file:test/secrets/hooks\n```\n\n`secret` is a source string.\n\nInbound verification checks Standard Webhooks signatures and timestamps, but\ndoes not store or reject previously seen webhook ids. Receivers that perform\nnon-idempotent work should deduplicate or process idempotently using\n`Webhook-Id` or the event id, backed by durable shared storage when running more\nthan one receiver instance.\n\n\u003e [!IMPORTANT]\n\u003e Webhook-protected CloudEvents must use structured HTTP encoding. Binary-mode\n\u003e CloudEvents with `ce-*` headers are rejected before signature verification.\n\n---\n\n## 🆔 ID generation\n\nSupported ID kinds:\n\n- `uuid`\n- `ksuid`\n- `nanoid`\n- `ulid`\n- `xid`\n\nConfig:\n\n```yaml\nid:\n  kind: uuid\n```\n\n---\n\n## 🚀 Runtime enhancements\n\nServer commands created through `cli.Application.AddServer` include `runtime.Module`, which currently enables:\n\n- [automemlimit](https://github.com/KimMachineGun/automemlimit)\n\n\u003e [!NOTE]\n\u003e This registration is best-effort and does not fail startup if a memory limit cannot be applied. Direct Fx compositions and client-style commands should include `runtime.Module` explicitly when they want this behavior.\n\n---\n\n## 🐘 SQL (Postgres)\n\nSQL root config is `database/sql.Config`, with Postgres under `sql.pg`.\n\nPostgres config embeds common pool + DSN config (`database/sql/config.Config`), including master/slave DSNs and pool sizes.\n\n`module.Server` and `module.Client` both include `sql.Module`, which currently wires PostgreSQL support via `database/sql/pg.Module`.\n\nEnablement is presence-based: a nil `sql` block or a nil `sql.pg` block disables SQL wiring. When enabled, the pgx stdlib driver is registered under the name `pg`, master/slave DSNs are resolved using the source-string rules described above, OpenTelemetry `database/sql` stats metrics are registered, and the resulting pools are closed on lifecycle stop.\n\nExample (with source strings for DSNs):\n\n```yaml\nsql:\n  pg:\n    masters:\n      - url: env:PG_MASTER_DSN\n    slaves:\n      - url: env:PG_SLAVE_DSN\n    max_open_conns: 5\n    max_idle_conns: 5\n    conn_max_lifetime: 1h\n```\n\nExample (literal DSN; not recommended for production secrets):\n\n```yaml\nsql:\n  pg:\n    masters:\n      - url: postgres://user:pass@localhost:5432/dbname?sslmode=disable\n    max_open_conns: 10\n```\n\n### Dependencies\n\n![Dependencies](./assets/database.png)\n\n---\n\n## 🩺 Health\n\nHealth checks are based on [go-health](https://github.com/alexfalkowski/go-health).\n\nThe framework provides Kubernetes-style endpoints:\n\n- `/\u003cname\u003e/healthz` — general serving health status\n- `/\u003cname\u003e/livez` — liveness probe\n- `/\u003cname\u003e/readyz` — readiness probe\n\nSuccessful health responses return HTTP 200 with the plain-text body `SERVING`.\nMissing or failing observers return HTTP 503 with the standard go-service error response.\n\nThese are modeled after [Kubernetes API health endpoints](https://kubernetes.io/docs/reference/using-api/health-checks/).\n\n---\n\n## 📡 Telemetry\n\nTelemetry config root is `telemetry.Config`:\n\n```yaml\ntelemetry:\n  logger: ...\n  metrics: ...\n  tracer: ...\n```\n\n### Logging\n\nLogging uses `log/slog`.\n\nSupported built-in logger kinds:\n\n- `json`\n- `text`\n- `tint`\n- `otlp`\n\n#### JSON logger\n\n```yaml\ntelemetry:\n  logger:\n    kind: json\n    level: info\n```\n\n#### Text logger\n\n```yaml\ntelemetry:\n  logger:\n    kind: text\n    level: info\n```\n\n#### OTLP logger\n\n```yaml\ntelemetry:\n  logger:\n    kind: otlp\n    level: info\n    url: http://localhost:4318/v1/logs\n    headers:\n      Authorization: env:OTLP_LOGS_AUTH\n```\n\n\u003e [!NOTE]\n\u003e - `headers` values are source strings.\n\u003e - Telemetry header maps are resolved during config projection; unset `env:` values and unreadable `file:` values fail fast (panic during startup).\n\n\u003e [!WARNING]\n\u003e OTLP exporters reject non-loopback `http://` endpoints when headers are configured. Use HTTPS for remote collectors that require authorization headers; cleartext with headers is accepted only for local loopback endpoints.\n\n### Metrics\n\nSupported metrics kinds:\n\n- `prometheus`\n- `otlp`\n\n#### Prometheus\n\n```yaml\ntelemetry:\n  metrics:\n    kind: prometheus\n```\n\nWhen Prometheus is enabled on HTTP transport, metrics are exposed at `/\u003cname\u003e/metrics`.\n\n#### OTLP metrics\n\n```yaml\ntelemetry:\n  metrics:\n    kind: otlp\n    url: http://localhost:9009/otlp/v1/metrics\n    headers:\n      Authorization: env:OTLP_METRICS_AUTH\n```\n\n### Tracing\n\nTracing supports OTLP exporter config:\n\n```yaml\ntelemetry:\n  tracer:\n    kind: otlp\n    url: http://localhost:4318/v1/traces\n    headers:\n      Authorization: env:OTLP_TRACES_AUTH\n```\n\n\u003e [!NOTE]\n\u003e Current tracer wiring exports via OTLP/HTTP when `telemetry.tracer.kind` is `otlp` and `url` is configured.\n\n### Telemetry libraries used\n\n- \u003chttps://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/runtime\u003e\n- \u003chttps://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/host\u003e\n- \u003chttps://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\u003e\n- \u003chttps://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc\u003e\n- \u003chttps://github.com/redis/go-redis/tree/master/extra/redisotel\u003e\n- \u003chttps://github.com/XSAM/otelsql\u003e\n\n### Telemetry Dependencies\n\n![Dependencies](./assets/telemetry.png)\n\n---\n\n## 🎫 Tokens\n\nToken configuration is rooted at `token.Config`, usually nested under transport config as `transport.http.token` and/or `transport.grpc.token` (via the shared server-side transport config).\n\nSupported token `kind` values:\n\n- `jwt`\n- `paseto`\n- `ssh`\n\n### Access control (Casbin)\n\nAccess control is configured inside transport token config:\n\n```yaml\ntransport:\n  http:\n    token:\n      access:\n        model: file:./config/rbac.conf\n        policy: file:./config/rbac.csv\n```\n\nThe model is based on Casbin RBAC:\n\u003chttps://github.com/casbin/casbin/blob/master/examples/rbac_model.conf\u003e\n\n\u003e [!NOTE]\n\u003e `access.model` and `access.policy` are resolved through `os.FS.ReadSource`; use `file:` for files, `env:` for environment-provided content, or literal content.\n\n### JWT\n\nJWT config:\n\n```yaml\ntransport:\n  http:\n    token:\n      kind: jwt\n      jwt:\n        iss: my-service\n        exp: 1h\n        kid: my-key-id\n```\n\nImportant behavior:\n\n- JWT verification requires the `kid` header to exist and match `kid` in config exactly.\n- `exp` is parsed as a Go duration string; invalid values can fail fast.\n\n\u003e [!IMPORTANT]\n\u003e JWT generation and verification use Ed25519 signing and verification key material supplied through DI, typically from the crypto subsystem and standard module wiring.\n\n### Paseto\n\nPaseto config:\n\n```yaml\ntransport:\n  http:\n    token:\n      kind: paseto\n      paseto:\n        iss: my-service\n        exp: 1h\n```\n\n\u003e [!NOTE]\n\u003e The current PASETO implementation issues **v4 public** tokens using Ed25519 key material provided via wiring (not directly from `paseto.secret`). If you want config-driven key material, load it via the crypto subsystem and wire signer/verifier appropriately.\n\n### SSH tokens\n\nSSH token verification keys are name-addressable and support rotation.\n\nVerification-only example:\n\n```yaml\ntransport:\n  http:\n    token:\n      kind: ssh\n      ssh:\n        exp: 5m\n        keys:\n          - name: active\n            public: file:/keys/active.pub\n```\n\nSigning + verification example:\n\n```yaml\ntransport:\n  http:\n    token:\n      kind: ssh\n      ssh:\n        exp: 5m\n        key:\n          name: active\n          private: file:/keys/active\n        keys:\n          - name: active\n            public: file:/keys/active.pub\n          - name: old\n            public: file:/keys/old.pub\n```\n\n\u003e [!NOTE]\n\u003e - `ssh.key` is used for minting tokens (requires private key).\n\u003e - `ssh.keys` is used for verification (public keys).\n\u003e - `ssh.exp` sets the token validity window; SSH keys remain long-lived, while generated tokens are short-lived.\n\u003e - The config does not enforce that the signing key name exists in the verification set; include it if you want round-trip.\n\n---\n\n## 🚦 Limiter\n\nLimiter config is `transport/limiter.Config` and is typically applied at transport level.\n\nSupported key kinds (built-in):\n\n- `user-id`\n- `transport-service-method`\n- `service-method`\n- `ip`\n- `user-agent`\n\nExample:\n\n```yaml\ntransport:\n  http:\n    limiter:\n      kind: user-agent\n      tokens: 10\n      interval: 1s\n```\n\n\u003e [!NOTE]\n\u003e - `interval` is parsed as a Go duration string. Invalid values can fail fast.\n\u003e - The built-in limiter is an in-memory, per-process safeguard. Use it as a last resort and prefer an external edge, gateway, ingress, load balancer, or service-mesh limiter for production abuse protection.\n\u003e - The `user-id` key uses the verified principal stored in metadata. For JWT/PASETO tokens this is the subject claim; for SSH tokens this is the verified key name. Prefer it when authenticated identity is available.\n\u003e - The `transport-service-method` key prefixes the service-method value with the transport name, such as `http:GET /users/{id}` or `grpc:/users.v1.Users/Get`, so HTTP and gRPC operations use separate buckets.\n\u003e - The `service-method` key uses HTTP route/path metadata or the gRPC full method name. Prefer `transport-service-method` unless cross-transport operations intentionally share quota.\n\u003e - Server-side HTTP and gRPC limiters run after metadata extraction and token verification, so missing, malformed, or invalid authorization is rejected before it reaches the limiter. This is intentional; enforce quotas for those attempts with an external edge, gateway, ingress, load balancer, or service-mesh limiter.\n\n---\n\n## 🕒 Time (network time)\n\nTime config:\n\n```yaml\ntime:\n  kind: nts\n  address: time.cloudflare.com\n```\n\nSupported kinds:\n\n- `ntp`\n- `nts`\n\n---\n\n## 🌐 Transport\n\nThe transport layer provides higher-level wiring and middleware policy for communication in/out of the service.\n\nAt a high level:\n\n- `transport/...` contains the opinionated service transport layer: Fx wiring, composed HTTP/gRPC server and client stacks, retries, breakers, token middleware, health wiring, and related policy.\n- `net/...` contains lower-level protocol helpers and reusable primitives such as `net/http`, `net/grpc`, `net/http/meta`, `net/grpc/meta`, `net/http/strings`, `net/grpc/strings`, `net/grpc/health`, `net/header`, and `net/server`.\n\nSupported stacks include:\n\n- gRPC (\u003chttps://grpc.io/\u003e)\n- HTTP REST abstraction (`net/http/rest`) using content negotiation\n- HTTP RPC abstraction (`net/http/rpc`) using content negotiation\n- HTTP MVC helpers (`net/http/mvc`)\n- CloudEvents (\u003chttps://github.com/cloudevents/sdk-go\u003e)\n\n### HTTP content types\n\nThe HTTP REST and RPC helpers resolve encoders from the request `Content-Type`, falling back to the first `Accept` media type when `Content-Type` is absent.\n\nBuilt-in text/object payload media types include:\n\n- `application/json`\n- `application/hjson`\n- `application/yaml`, `application/yml`\n- `application/toml`\n- `application/octet-stream`, `text/plain`, `text/markdown`\n\nInternal binary payload media types include:\n\n- `application/vnd.msgpack`\n- `application/gob`\n\nBuilt-in protobuf-oriented media type aliases include:\n\n- `application/proto`, `application/pb`, `application/protobuf`, `application/protobin`, `application/pbbin`\n- `application/protojson`, `application/pbjson`\n- `application/prototext`, `application/prototxt`, `application/pbtxt`\n\n\u003e [!NOTE]\n\u003e - `application/hjson` maps to the built-in `hjson` encoder kind.\n\u003e - Unknown or invalid request media types fall back to JSON selection.\n\u003e - `text/error` is reserved for error responses and should not be sent by clients as a request content type.\n\u003e\n\u003e `application/vnd.msgpack` and `application/gob` can be resolved as media types, but REST/RPC request-body decoding rejects them with HTTP 415.\n\n### HTTP route misses\n\nThe HTTP transport wraps the mux with `net/http.NewNotFoundHandler` so generated 404 responses can be rendered consistently while preserving other mux responses such as 405 Method Not Allowed.\n\n- REST/RPC-style missing routes use `net/http/content.NotFoundHandler`, which writes the standard `status.WriteError` response.\n- MVC missing routes can use `net/http/mvc.NotFoundHandler` to render the registered MVC not-found view when the request accepts HTML (`Accept: text/html`) or is an HTMX request (`Hx-Request: true`).\n- Routes that match and write their own status are not replaced by this mux-level not-found handler.\n\n### HTTP MVC errors\n\nWhen an MVC controller returns an error, `net/http/mvc.Route` renders the returned view with a client-safe `mvc.Error` model. The model contains the HTTP status `Code` and safe client-visible `Message`.\n\nThe raw error string remains available to templates as `mvcModelError` metadata for compatibility. Rendering that metadata can expose diagnostic details, so prefer `.Model.Message` for client-visible error pages.\n\n### Transport configuration (servers)\n\nTransport config root is `transport.Config`:\n\n- `transport.http` embeds `config/server.Config`\n- `transport.grpc` embeds `config/server.Config`\n\nMinimal example:\n\n```yaml\ntransport:\n  http:\n    address: tcp://localhost:8000\n    timeout: 10s\n  grpc:\n    address: tcp://localhost:9000\n    timeout: 10s\n```\n\n\u003e [!NOTE]\n\u003e - Address may use `\u003cnetwork\u003e://\u003caddress\u003e` (for example `tcp://:8000`) or a raw listen address such as `:8000`, which defaults to the `tcp` network.\n\u003e - If address is omitted, defaults are `tcp://:8080` (HTTP) and `tcp://:9090` (gRPC).\n\u003e - `max_receive_size` limits inbound payload size. A zero value uses the default `4MB`.\n\u003e - For HTTP, `max_receive_size` applies per request body. For gRPC, it applies per inbound unary request and per inbound stream message.\n\u003e - MVC does not enforce its own body-size caps; supported HTTP server wiring applies `max_receive_size` before MVC handlers run, and go-service HTTP clients apply their configured response-size cap when reading responses.\n\nReceive-limit example:\n\n```yaml\ntransport:\n  http:\n    max_receive_size: 2MB\n  grpc:\n    max_receive_size: 3MB\n```\n\nWith low-level server options:\n\n```yaml\ntransport:\n  http:\n    address: tcp://localhost:8000\n    timeout: 10s\n    options:\n      read_timeout: 10s\n      write_timeout: 10s\n      idle_timeout: 10s\n      read_header_timeout: 10s\n  grpc:\n    address: tcp://localhost:9000\n    timeout: 10s\n    options:\n      keepalive_enforcement_policy_ping_min_time: 10s\n      keepalive_max_connection_idle: 10s\n      keepalive_max_connection_age: 10s\n      keepalive_max_connection_age_grace: 10s\n      keepalive_ping_time: 10s\n```\n\n### TLS for transports\n\nTLS config uses `crypto/tls/config.Config` and fields are source strings:\n\n```yaml\ntransport:\n  http:\n    tls:\n      cert: file:test/certs/cert.pem\n      key: file:test/certs/key.pem\n      ca: file:test/certs/rootCA.pem\n  grpc:\n    tls:\n      cert: file:test/certs/cert.pem\n      key: file:test/certs/key.pem\n      ca: file:test/certs/rootCA.pem\n```\n\nSet `ca` on server TLS config to require and verify client certificates for mTLS. Set `ca` on client TLS\nconfig to verify server certificates issued by the same local or private CA. `server_name` is only needed\non clients when the dial address differs from the certificate DNS name.\n\n\u003e [!IMPORTANT]\n\u003e If you are using `go-service-template` or composing server transport bundles such as `module.Server` or `transport.Module`, the required transport registration is handled for you by DI.\n\u003e\n\u003e `module.Client` does not wire transports by default. Add `transport.Module`, the relevant transport submodule, or call the transport-level `Register(...)` functions when a client process constructs HTTP or gRPC TLS config from source strings such as `file:`.\n\u003e\n\u003e You only need to call transport-level `Register(...)` functions yourself when you intentionally wire transports manually or compose lower-level packages outside the transport module graph.\n\u003e\n\u003e If you are wiring server lifecycle manually, use `net/server.Register(...)`.\n\n### Forwarded IPs and reflection\n\n\u003e [!WARNING]\n\u003e HTTP and gRPC metadata extraction intentionally trusts common forwarded IP headers/metadata such as `X-Forwarded-For`, `X-Real-IP`, `CF-Connecting-IP`, and `True-Client-IP`. Services that rely on extracted IPs for logging, policy, or rate limiting should only receive traffic through trusted edge infrastructure that strips or overwrites client-supplied forwarding headers.\n\n\u003e [!WARNING]\n\u003e gRPC server reflection is intentionally always registered by `net/grpc.NewServer` so internal tooling can discover services. Services that should not expose reflection publicly should restrict access with bind addresses, TLS/client authentication, ingress policy, firewall rules, or service-mesh authorization.\n\n### Transport Dependencies\n\n![Dependencies](./assets/transport.png)\n\n### Circuit breakers (client-side)\n\nThe transport client wrappers include optional circuit breakers:\n\n- HTTP breaker (`transport/http/breaker`):\n  - Scope is per `\"\u003cMETHOD\u003e \u003cHOST\u003e\"`.\n  - Default failure statuses are `\u003e=500` and `429`.\n  - Transport errors are counted as failures.\n  - Failure status responses are still returned to callers (while breaker accounting records a failure).\n\n- gRPC breaker (`transport/grpc/breaker`):\n  - Scope is per `fullMethod`.\n  - Default failure codes are `Unavailable`, `DeadlineExceeded`, `ResourceExhausted`, and `Internal`.\n  - Errors with other gRPC codes are treated as successful for breaker accounting.\n\n---\n\n## 🔑 Cryptography\n\nThe crypto root config is `crypto.Config` and supports multiple key types. Most fields are source strings.\n\nExample:\n\n```yaml\ncrypto:\n  aes:\n    key: file:test/secrets/aes\n  ed25519:\n    public: file:test/secrets/ed25519_public\n    private: file:test/secrets/ed25519_private\n  hmac:\n    key: file:test/secrets/hmac\n  rsa:\n    public: file:test/secrets/rsa_public\n    private: file:test/secrets/rsa_private\n  ssh:\n    public: file:test/secrets/ssh_public\n    private: file:test/secrets/ssh_private\n```\n\n\u003e [!NOTE]\n\u003e - AES keys must be 16/24/32 bytes after resolving the source string.\n\u003e - RSA keys expect PKCS#1 PEM blocks (`RSA PUBLIC KEY` / `RSA PRIVATE KEY`) and must be at least 4096 bits.\n\u003e - Ed25519 expects PKIX `PUBLIC KEY` and PKCS#8 `PRIVATE KEY` PEM blocks.\n\n### Crypto Dependencies\n\n![Dependencies](./assets/crypto.png)\n\n---\n\n## 🛠️ Debug endpoints\n\nDebug server config:\n\n```yaml\ndebug:\n  address: tcp://localhost:6060\n  timeout: 10s\n```\n\nEnable TLS:\n\n```yaml\ndebug:\n  tls:\n    cert: file:test/certs/cert.pem\n    key: file:test/certs/key.pem\n    ca: file:test/certs/rootCA.pem\n```\n\nAll debug endpoints are namespaced by service name: `/\u003cname\u003e/debug/...`.\n\n\u003e [!WARNING]\n\u003e If `debug.address` is omitted while debug is enabled, the debug server binds to `tcp://:6060`. Set an explicit address, TLS/mTLS, and network or policy controls appropriate for the deployment.\n\n### statsviz\n\n```http\nGET http://localhost:6060/\u003cname\u003e/debug/statsviz\n```\n\n\u003chttps://github.com/arl/statsviz\u003e\n\n### pprof\n\n```http\nGET http://localhost:6060/\u003cname\u003e/debug/pprof/\nGET http://localhost:6060/\u003cname\u003e/debug/pprof/cmdline\nGET http://localhost:6060/\u003cname\u003e/debug/pprof/profile\nGET http://localhost:6060/\u003cname\u003e/debug/pprof/symbol\nGET http://localhost:6060/\u003cname\u003e/debug/pprof/trace\n```\n\n\u003chttps://pkg.go.dev/net/http/pprof\u003e\n\n### fgprof\n\n```http\nGET http://localhost:6060/\u003cname\u003e/debug/fgprof?seconds=10\n```\n\n\u003chttps://pkg.go.dev/github.com/felixge/fgprof\u003e\n\n### gopsutil\n\n```http\nGET http://localhost:6060/\u003cname\u003e/debug/psutil\n```\n\n\u003chttps://github.com/shirou/gopsutil\u003e\n\n---\n\n## 🧑‍💻 Development\n\n### Style\n\nThis repo generally follows the [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md).\n\nExported Go identifiers should have GoDoc comments, and each comment should start with the identifier name or `Deprecated:`.\n\n### Development Dependencies\n\nFor local TLS fixtures:\n\n- \u003chttps://github.com/FiloSottile/mkcert\u003e\n\n### Setup (repo)\n\nThis repo uses a `bin/` git submodule for `make` targets.\n\n```sh\ngit submodule sync\ngit submodule update --init\n\nmkcert -install\nmake create-certs\n\nmake dep\n```\n\nIf submodule fetch fails, ensure GitHub SSH access is configured (`.gitmodules` uses `git@github.com:...` URLs).\n\n### Discover targets\n\n```sh\nmake help\n```\n\n### Dependencies (`vendor/` workflow)\n\n```sh\nmake dep\n```\n\n`make dep` runs:\n\n- `go mod download`\n- `go mod tidy`\n- `go mod vendor`\n\nTests are run with `-mod vendor`, so after dependency changes run `make dep` before `make specs`.\n\n### Local integration dependencies\n\nStart required services:\n\n```sh\nmake start\n```\n\nStop them:\n\n```sh\nmake stop\n```\n\n### Tests\n\nRun unit tests with race + coverage:\n\n```sh\nmake specs\n```\n\nArtifacts:\n\n- JUnit XML: `test/reports/specs.xml`\n- Coverage profile: `test/reports/profile.cov`\n\n### Lint and format\n\n```sh\nmake lint\nmake fix-lint\nmake format\n```\n\n### Security checks\n\n```sh\nmake sec\n```\n\n### Benchmarks\n\n```sh\nmake benchmarks\nmake http-benchmarks\nmake grpc-benchmarks\nmake sql-benchmarks\nmake cache-benchmarks\nmake bytes-benchmarks\nmake strings-benchmarks\nmake id-benchmarks\nmake http-content-benchmarks\n```\n\n### Coverage reports\n\n```sh\nmake coverage\nmake html-coverage\nmake func-coverage\n```\n\n### Code generation (Buf)\n\n```sh\nmake generate\n```\n\n### Architecture diagrams\n\n```sh\nmake diagrams\nmake crypto-diagram\nmake database-diagram\nmake telemetry-diagram\nmake transport-diagram\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexfalkowski%2Fgo-service","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falexfalkowski%2Fgo-service","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexfalkowski%2Fgo-service/lists"}