{"id":50126158,"url":"https://github.com/papadanielvi/poya","last_synced_at":"2026-06-07T22:01:18.604Z","repository":{"id":358541063,"uuid":"1241716170","full_name":"PapaDanielVi/poya","owner":"PapaDanielVi","description":"Go SDK for dynamic runtime configuration — sync values from etcd, Redis, HashiCorp Vault, MySQL, or PostgreSQL with zero polling logic for your application.","archived":false,"fork":false,"pushed_at":"2026-06-01T19:02:49.000Z","size":7644,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-01T21:05:34.480Z","etag":null,"topics":["config","config-sdk","configuration-management","dynamic","dynamic-config","etcd","feature-flags","go","go-sdk","golang","hashicorp-vault","mysql","postgresql","redis","runtime","runtime-configuration","sdk"],"latest_commit_sha":null,"homepage":"","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/PapaDanielVi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","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":null,"dco":null,"cla":null}},"created_at":"2026-05-17T18:19:40.000Z","updated_at":"2026-06-01T19:02:25.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/PapaDanielVi/poya","commit_stats":null,"previous_names":["papadanielvi/poya"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/PapaDanielVi/poya","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PapaDanielVi%2Fpoya","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PapaDanielVi%2Fpoya/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PapaDanielVi%2Fpoya/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PapaDanielVi%2Fpoya/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/PapaDanielVi","download_url":"https://codeload.github.com/PapaDanielVi/poya/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PapaDanielVi%2Fpoya/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34039495,"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-07T02:00:07.652Z","response_time":124,"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":["config","config-sdk","configuration-management","dynamic","dynamic-config","etcd","feature-flags","go","go-sdk","golang","hashicorp-vault","mysql","postgresql","redis","runtime","runtime-configuration","sdk"],"created_at":"2026-05-23T20:03:16.748Z","updated_at":"2026-06-07T22:01:18.567Z","avatar_url":"https://github.com/PapaDanielVi.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg src=\"docs/icon.png\" alt=\"poay\" style=\"border-radius: 50%; width: 300px; height: 300px; object-fit: cover;\"/\u003e\n\n# Poya\n\n[![CI](https://github.com/PapaDanielVi/poya/actions/workflows/ci.yml/badge.svg)](https://github.com/PapaDanielVi/poya/actions/workflows/ci.yml)\n[![Go Report Card](https://goreportcard.com/report/github.com/PapaDanielVi/poya)](https://goreportcard.com/report/github.com/PapaDanielVi/poya)\n[![Go Reference](https://pkg.go.dev/badge/github.com/PapaDanielVi/poya.svg)](https://pkg.go.dev/github.com/PapaDanielVi/poya)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n\n**poya** is a Go SDK for dynamic runtime configuration and configuration management. Register typed config values, connect a backend provider (etcd, Redis, HashiCorp Vault, MySQL, PostgreSQL, or local file), and the SDK keeps everything in sync in the background. Your application only calls `Get()` — no polling, no refresh logic. Supports use cases including feature flags, A/B testing, service discovery, and runtime parameter tuning.\n\n## Features\n\n- **Type-safe generics** — `DcValue[string]`, `DcValue[int]`, `DcValue[YourConfig]`, any type you need\n- **Scalar, struct, and array values** — a single `DcValue[T]` type handles all three; scalars are parsed via type switch, structs and arrays are JSON-decoded automatically\n- **Declarative config structs** — define your entire config layout in a single struct with tags; poya discovers and registers all fields via reflection\n- **Multiple providers** — etcd (prefix watch API), Redis (batch polling), HashiCorp Vault (KV v2 polling), MySQL (batch polling), PostgreSQL (batch polling), File (fsnotify / fsevents)\n- **Efficient watching** — etcd uses a single prefix watch for all keys; polling providers fetch all keys in one batch per cycle; the SDK runs one goroutine per provider, not per key\n- **Lock-free reads** — `Get()` uses `atomic.Value` for zero-contention reads on the hot path\n- **Pluggable metrics** — Prometheus (default), OpenTelemetry, or inject your own implementation\n- **Structured logging** — inject any logger; defaults to stderr via `log/slog`\n- **Prefix \u0026 nesting** — hierarchical key management with automatic prefix accumulation for nested structs\n- **Graceful shutdown** — context-based cancellation cleans up all background goroutines\n\n## Installation\n\n```bash\ngo get github.com/PapaDanielVi/poya\n```\n\nRequires Go 1.26+.\n\n## Quick Start\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/PapaDanielVi/poya\"\n\t\"github.com/PapaDanielVi/poya/provider/redis\"\n)\n\nfunc main() {\n\t// 1. Create a provider\n\trdb := redis.New(redis.Config{\n\t\tAddr:         \"localhost:6379\",\n\t\tPollInterval: 5 * time.Second,\n\t})\n\n\t// 2. Create the SDK\n\tsdk := poya.New(poya.Config{\n\t\tProvider:      rdb,\n\t\tPrefix:        \"myapp/\",\n\t\tEnableMetrics: true,\n\t})\n\n\t// 3. Register values individually\n\ttimeout := poya.NewDcValue(\"30s\")\n\tpoya.Register(sdk, \"timeout\", timeout)\n\n\t// 4. Start background sync\n\tsdk.Start()\n\tdefer sdk.Stop()\n\n\t// 5. Read values anywhere in your application\n\tfmt.Println(timeout.Get()) // always the latest value from Redis\n}\n```\n\n## Examples\n\nRunnable examples for every provider live in the [`examples/`](examples/) directory. Each example includes setup instructions (Docker commands to start the backend, seed data) and demonstrates both individual `Register` and struct-based `RegisterConfig` patterns.\n\n| Example    | Provider        | Watch Strategy                           | File                                                         |\n| ---------- | --------------- | ---------------------------------------- | ------------------------------------------------------------ |\n| etcd       | etcd            | Single prefix watch (event-driven)       | [`examples/etcd/main.go`](examples/etcd/main.go)             |\n| Redis      | Redis           | Batch `MGET` polling                     | [`examples/redis/main.go`](examples/redis/main.go)           |\n| Vault      | HashiCorp Vault | Sequential poll (KV v2)                  | [`examples/vault/main.go`](examples/vault/main.go)           |\n| MySQL      | MySQL           | Batch `SELECT ... WHERE IN` polling      | [`examples/mysql/main.go`](examples/mysql/main.go)           |\n| PostgreSQL | PostgreSQL      | Batch `SELECT ... WHERE IN ($N)` polling | [`examples/postgresql/main.go`](examples/postgresql/main.go) |\n| File       | Local file      | fsnotify / fsevents (JSON + YAML)        | [`examples/file/main.go`](examples/file/main.go)             |\n\n**Running an example:**\n\n```bash\n# Start the backend (see the example file for Docker commands)\ndocker run -d --name redis -p 6379:6379 redis:8.2.6\n\n# Run the example\ngo run examples/redis/main.go\n```\n\nAll examples use the same config keys (`timeout`, `verbose`, `db/host`, `db/port`) so you can swap providers and keep the same application code.\n\n## API Reference\n\n### Values — `DcValue[T]`\n\nA single generic type handles both scalars and structs. The SDK determines which at registration time via reflection.\n\n**Scalar values** (`string`, `int`, `bool`, `float64`, etc.):\n\n```go\nval := poya.NewDcValue(\"default_value\")\npoya.Register(sdk, \"my_key\", val)\n\ncurrent := val.Get() // returns string\n```\n\n**Struct values** — the provider stores a JSON blob; poya decodes it into your struct:\n\n```go\ntype DatabaseConfig struct {\n\tHost    string `json:\"host\"`\n\tPort    int    `json:\"port\"`\n\tMaxConn int    `json:\"max_conn\"`\n}\n\ndbDefault := DatabaseConfig{Host: \"localhost\", Port: 5432, MaxConn: 10}\ndbVal := poya.NewDcValue(dbDefault)\npoya.Register(sdk, \"database\", dbVal)\n\ncfg := dbVal.Get() // returns DatabaseConfig\nfmt.Println(cfg.Host)\n```\n\n\n**Array values** — the provider stores a JSON array; poya decodes it into your slice:\n\n```go\ntags := poya.NewDcValue([]string{\"alpha\", \"beta\"})\npoya.Register(sdk, \"tags\", tags)\n\ns := tags.Get() // returns []string\nfmt.Println(s[0]) // \"alpha\"\n\n// Works with any element type:\nports := poya.NewDcValue([]int{8080, 9090})\npoya.Register(sdk, \"ports\", ports)\n\np := ports.Get() // returns []int\n```\n\nThe provider value must be a JSON array (e.g. `[\"alpha\",\"beta\"]` or `[8080,9090]`). Any slice element type that `encoding/json` supports works.\n\n**Duration values** — `time.Duration` is supported as a scalar type, parsed from strings like `\"30s\"`, `\"1m\"`, `\"500ms\"`:\n\n```go\ntimeout := poya.NewDcValue(time.Duration(30 * time.Second))\npoya.Register(sdk, \"timeout\", timeout)\n\n// Provider value \"1m30s\" will be parsed to 90s\nt := timeout.Get() // returns time.Duration\nfmt.Println(t) // 30s (default)\n```\n\nThe provider value must be a valid `time.Duration` string (supports standard Go duration formats).\n\n### Declarative Config Structs — `RegisterConfig`\n\nDefine your entire configuration in a single struct. poya uses struct tags to discover fields:\n\n```go\ntype AppConfig struct {\n\tTimeout  poya.DcValue[string]        `poya:\"key=timeout\"`\n\tVerbose  poya.DcValue[bool]          `poya:\"key=verbose\"`\n\tDBConfig poya.DcValue[DatabaseConfig] `poya:\"key=db_config\"`\n\tDB       DBConfig                     `poya:\"prefix=db\"`\n}\n\ntype DBConfig struct {\n\tHost poya.DcValue[string] `poya:\"key=host\"`\n\tPort poya.DcValue[int]    `poya:\"key=port\"`\n}\n\ncfg := AppConfig{\n\tTimeout:  *poya.NewDcValue(\"30s\"),\n\tVerbose:  *poya.NewDcValue(false),\n\tDBConfig: *poya.NewDcValue(DatabaseConfig{Host: \"localhost\", Port: 5432}),\n\tDB: DBConfig{\n\t\tHost: *poya.NewDcValue(\"localhost\"),\n\t\tPort: *poya.NewDcValue(5432),\n\t},\n}\n\nsdk.RegisterConfig(\u0026cfg)\n// Registers: myapp/timeout, myapp/verbose, myapp/db_config, myapp/db/host, myapp/db/port\n```\n\n#### Tag Format\n\n| Tag                         | Meaning                                                 |\n| --------------------------- | ------------------------------------------------------- |\n| `poya:\"key=timeout\"`        | This field is a config value watched at key `timeout`   |\n| `poya:\"prefix=db\"`          | This nested struct contributes `db/` to child key paths |\n| `poya:\"key=host,prefix=db\"` | Both a value and a prefix for deeper nesting            |\n\nFields without a tag use their lowercased field name as the key.\n\n### Prefix Handling\n\nPrefixes accumulate hierarchically:\n\n```\nFull key = SDK Prefix + Parent Prefixes + Field Key\n\nExample with Prefix=\"myapp/\":\n  Timeout field (key=timeout) → \"myapp/timeout\"\n  DB.Host field (key=host, parent prefix=\"db/\") → \"myapp/db/host\"\n```\n\n### Metrics\n\npoya supports multiple metrics backends. Inject any via `Config.Metrics`:\n\n**Prometheus** (default when `EnableMetrics: true`):\n\n```go\nsdk := poya.New(poya.Config{\n\tProvider:      rdb,\n\tPrefix:        \"myapp/\",\n\tEnableMetrics: true,\n})\n```\n\n**OpenTelemetry**:\n\n```go\nmeter := otel.Meter(\"github.com/PapaDanielVi/poya\")\notelMetrics, _ := otel.New(meter)\nsdk := poya.New(poya.Config{Provider: rdb, Metrics: otelMetrics})\n```\n\n**Custom implementation**:\n\n```go\nsdk := poya.New(poya.Config{Provider: rdb, Metrics: myCustomMetrics})\n```\n\nAll backends implement the same interface:\n\n```go\ntype Metrics interface {\n\tIncWatchEvents(key string)\n\tIncWatchErrors(key string)\n\tObserveUpdateLatency(key string, d time.Duration)\n\tSetRegisteredKeys(n int)\n}\n```\n\nWhen metrics are disabled, a no-op stub is used — no if-checks in hot paths.\n\n**Prometheus metrics:**\n\n| Metric                             | Type      | Description                                  |\n| ---------------------------------- | --------- | -------------------------------------------- |\n| `poya_watch_events_total`          | Counter   | Total watch events received (labeled by key) |\n| `poya_watch_errors_total`          | Counter   | Total watch errors (labeled by key)          |\n| `poya_sync_update_latency_seconds` | Histogram | Value update latency (labeled by key)        |\n| `poya_registered_keys`             | Gauge     | Number of registered config keys             |\n\nEach SDK instance uses its own Prometheus registry, so multiple instances won't conflict.\n\n### Logging\n\npoya uses a simple structured-logger interface. Inject any logger via `Config.Logger`:\n\n```go\nsdk := poya.New(poya.Config{\n\tProvider: rdb,\n\tLogger:   myCustomLogger,\n})\n```\n\nThe default logger writes to stderr via `log/slog`. The interface:\n\n```go\ntype Logger interface {\n\tDebug(msg string, keysAndValues ...any)\n\tInfo(msg string, keysAndValues ...any)\n\tWarn(msg string, keysAndValues ...any)\n\tError(msg string, keysAndValues ...any)\n}\n```\n\n## Provider Setup\n\n### etcd\n\nUses etcd's native Watch API for event-driven updates (no polling):\n\n```go\netcdProvider, err := etcd.New(etcd.Config{\n\tEndpoints:   []string{\"localhost:2379\"},\n\tDialTimeout: 5 * time.Second,\n})\nif err != nil {\n\tlog.Fatal(err)\n}\ndefer etcdProvider.Close()\n\nsdk := poya.New(poya.Config{Provider: etcdProvider, Prefix: \"myapp/\"})\n```\n\n### Redis\n\nPolls at a configurable interval. Best for simple setups without etcd:\n\n```go\nrdb := redis.New(redis.Config{\n\tAddr:         \"localhost:6379\",\n\tPassword:     \"\",       // no auth\n\tDB:           0,\n\tPollInterval: 5 * time.Second,\n})\ndefer rdb.Close()\n\nsdk := poya.New(poya.Config{Provider: rdb, Prefix: \"myapp/\"})\n```\n\n### HashiCorp Vault\n\nPolls the KV v2 secrets engine. The key is the secret path within the mount:\n\n```go\nv, err := vault.New(vault.Config{\n\tAddress:      \"http://localhost:8200\",\n\tToken:        \"root-token\",\n\tMountPath:    \"secret\",\n\tPollInterval: 10 * time.Second,\n})\nif err != nil {\n\tlog.Fatal(err)\n}\n\nsdk := poya.New(poya.Config{Provider: v, Prefix: \"myapp/\"})\n```\n\n### MySQL\n\nPolls a database table at a configurable interval. Accepts an existing `*sql.DB` connection (you manage the lifecycle):\n\n**Using the default repository** (simple key-value table):\n\n```go\nimport (\n\t\"database/sql\"\n\t\"github.com/PapaDanielVi/poya/provider/mysql\"\n\t_ \"github.com/go-sql-driver/mysql\"\n)\n\ndb, _ := sql.Open(\"mysql\", \"user:pass@tcp(localhost:3306)/configdb\")\nprovider, _ := mysql.New(mysql.Config{\n\tDB:           db,\n\tTableName:    \"config\",\n\tKeyColumn:   \"config_key\",\n\tValueColumn: \"config_value\",\n\tPollInterval: 5 * time.Second,\n})\n\nsdk := poya.New(poya.Config{Provider: provider, Prefix: \"myapp/\"})\n```\n\n**Using a custom repository** (any table schema):\n\n```go\ntype MyRepository struct {\n\tdb *sql.DB\n}\n\nfunc (r *MyRepository) Get(ctx context.Context, key string) (string, error) {\n\t// Custom query logic for your schema\n\tvar value string\n\terr := r.db.QueryRowContext(ctx, \"SELECT value FROM my_table WHERE name = ?\", key).Scan(\u0026value)\n\treturn value, err\n}\n\nprovider, _ := mysql.New(mysql.Config{\n\tRepository:   \u0026MyRepository{db: db},\n\tPollInterval: 5 * time.Second,\n})\n```\n\n### PostgreSQL\n\nSame interface as MySQL, with PostgreSQL-specific placeholder syntax:\n\n```go\nimport (\n\t\"database/sql\"\n\t\"github.com/PapaDanielVi/poya/provider/postgresql\"\n\t_ \"github.com/lib/pq\"\n)\n\ndb, _ := sql.Open(\"postgres\", \"postgres://user:pass@localhost/configdb?sslmode=disable\")\nprovider, _ := postgresql.New(postgresql.Config{\n\tDB:           db,\n\tTableName:    \"config\",\n\tKeyColumn:   \"config_key\",\n\tValueColumn: \"config_value\",\n\tPollInterval: 5 * time.Second,\n})\n\nsdk := poya.New(poya.Config{Provider: provider, Prefix: \"myapp/\"})\n```\n\nCustom repositories work identically to MySQL:\n\n```go\nprovider, _ := postgresql.New(postgresql.Config{\n\tRepository:   \u0026MyRepository{db: db},\n\tPollInterval: 5 * time.Second,\n})\n```\n\n### File\n\nWatches a local JSON or YAML file for changes using fsnotify (fsevents on macOS, inotify on Linux). On every change the file is re-read and all registered values are updated via compare-and-swap. Supports flat `key: value` format (not nested):\n\n```go\nfp, err := file.New(file.Config{\n\tPath: \"/etc/myapp/config.json\",\n\t// Format: file.FormatAuto, // auto-detects from extension\n})\nif err != nil {\n\tlog.Fatal(err)\n}\n\nsdk := poya.New(poya.Config{Provider: fp, Prefix: \"myapp/\"})\n```\n\n**JSON file format** (`config.json`):\n```json\n{\n\t\"timeout\": \"30s\",\n\t\"verbose\": true,\n\t\"max_conn\": 100\n}\n```\n\n**YAML file format** (`config.yaml`):\n```yaml\ntimeout: 30s\nverbose: true\nmax_conn: 100\n```\n\nFormat is auto-detected from the file extension (`.json`, `.yaml`, `.yml`) or can be set explicitly via `Config.Format`.\n\n## Use Cases\n\n- **Feature flags** — toggle features at runtime without redeployment\n- **Database credentials** — rotate connection strings dynamically\n- **Service discovery** — update endpoint lists as services scale\n- **Rate limits \u0026 thresholds** — adjust operational parameters in real time\n- **A/B testing** — change experiment parameters on the fly\n- **Multi-tenant config** — per-tenant settings with hierarchical key prefixes\n\n## Project Structure\n\n```\npoya/\n├── poya.go                    # SDK: New, Start, Stop, Register, RegisterConfig\n├── dcvalue.go                 # DcValue[T] — unified scalar + struct config value\n├── metrics/\n│   ├── metrics.go             # Metrics interface + NoopMetrics stub\n│   ├── prometheus/            # Prometheus implementation\n│   └── otel/                  # OpenTelemetry implementation\n├── logger/\n│   └── logger.go              # Logger interface + slog default + noop stub\n├── provider/\n│   ├── provider.go            # Provider interface\n│   ├── etcd/                  # etcd provider (prefix watch API)\n│   ├── redis/                 # Redis provider (batch MGET polling)\n│   ├── vault/                 # HashiCorp Vault provider (KV v2 polling)\n│   ├── mysql/                 # MySQL provider (batch polling, Repository interface)\n│   ├── postgresql/            # PostgreSQL provider (batch polling, Repository interface)\n│   └── file/                  # File provider (fsnotify / fsevents, JSON + YAML)\n└── ...\n```\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on adding providers, value types, and submitting pull requests.\n\n## Keywords\n\nGo, Golang, SDK, dynamic config, runtime configuration, configuration management, feature flags, A/B testing, service discovery, etcd, Redis, HashiCorp Vault, MySQL, PostgreSQL, file config, fsnotify, fsevents, type-safe config, generic config, Go SDK\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpapadanielvi%2Fpoya","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpapadanielvi%2Fpoya","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpapadanielvi%2Fpoya/lists"}