{"id":29733857,"url":"https://github.com/tenjininc/invar","last_synced_at":"2025-07-25T10:38:13.694Z","repository":{"id":64470846,"uuid":"52191442","full_name":"TenjinInc/invar","owner":"TenjinInc","description":"Single source of truth for environmental configuration.","archived":false,"fork":false,"pushed_at":"2024-03-03T21:40:17.000Z","size":316,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-07-03T14:09:43.851Z","etag":null,"topics":["config","configuration-files","environment","environment-configuration","environment-variables","experimental","ruby","ruby-gem"],"latest_commit_sha":null,"homepage":"","language":"Ruby","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/TenjinInc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2016-02-21T04:57:33.000Z","updated_at":"2024-01-12T18:07:21.000Z","dependencies_parsed_at":"2023-01-26T12:15:48.929Z","dependency_job_id":"7cb4d879-fff8-4610-9d60-388e9de7da07","html_url":"https://github.com/TenjinInc/invar","commit_stats":{"total_commits":61,"total_committers":1,"mean_commits":61.0,"dds":0.0,"last_synced_commit":"5c1dba9aed4e6d07f64a9eb2271702eb65c9c9de"},"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"purl":"pkg:github/TenjinInc/invar","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TenjinInc%2Finvar","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TenjinInc%2Finvar/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TenjinInc%2Finvar/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TenjinInc%2Finvar/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TenjinInc","download_url":"https://codeload.github.com/TenjinInc/invar/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TenjinInc%2Finvar/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266991277,"owners_count":24017740,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-25T02:00:09.625Z","response_time":70,"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","configuration-files","environment","environment-configuration","environment-variables","experimental","ruby","ruby-gem"],"created_at":"2025-07-25T10:38:08.799Z","updated_at":"2025-07-25T10:38:13.611Z","avatar_url":"https://github.com/TenjinInc.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Invar\n\nSingle source of immutable truth for managing application configs, secrets, and environment variable data.\n\n## Big Picture\n\nInvar's main purpose is to enhance and simplify how applications know about their system environment and config.\n\nUsing it in code looks like this:\n\n```ruby\ninvar = Invar.new(namespace: 'my-app')\n\ndb_host = invar / :config / :database / :host\n```\n\n### Background\n\nManaging application config in a [12 Factor](http://12factor.net/config) style is a good idea, but simple environment\nvariables have some downsides:\n\n* They are not groupable / nestable\n* Secrets might be leaked to untrustable subprocesses or 3rd party logging services (eg. an error dump including the\n  whole ENV)\n* Secrets might be stored in plaintext (eg. in cron or scripts). It's better to store secrets as encrypted.\n* Cannot be easily checked against a schema for early error detection\n* Ruby's core ENV does not accept symbols as keys (a minor nuisance, but it counts)\n\n\u003e **Fun Fact:** Invar is named for an [alloy used in clockmaking](https://en.wikipedia.org/wiki/Invar) - it's short\n\u003e for \"**invar**iable\".\n\n### Features\n\nHere's what this Gem provides:\n\n* File location defaults from\n  the [XDG Base Directory](https://en.wikipedia.org/wiki/Freedesktop.org#Base_Directory_Specification)\n  file location standard\n* File schema using [dry-schema](https://dry-rb.org/gems/dry-schema/main/)\n* Distinction between configs and secrets\n* Secrets encrypted using [Lockbox](https://github.com/ankane/lockbox)\n* Access configs and ENV variables using symbols or case-insensitive strings.\n* Enforced key uniqueness\n* Helpful Rake tasks\n* Meaningful error messages with suggestions\n* Immutable\n\n### Anti-Features\n\nThings that Invar intentionally does **not** support:\n\n* Multiple config sources\n    * No subtle overrides\n    * No frustration about file edits not working... because you're editing the wrong file\n    * No remembering finicky precedence order\n* Modes\n    * No forgetting to set the mode before running rake, etc\n    * No proliferation of files irrelevant to the current situation\n* Config file code interpretation (eg. ERB in YAML)\n    * Reduced security hazard\n    * No value ambiguity\n\n### But That's Bonkers\n\nIt might be! This is a bit of an experiment. Some things may appear undesirable at first glance, but it's usually for a\nreason.\n\nSome situations might legitimately need a more complex configuration setup. But perhaps reflect on whether it's a code\nsmell nudging you to:\n\n* Reduce your application into smaller parts (eg. microservices etc)\n* Reduce the number of service providers\n* Improve your collaboration or deployment procedures and automation\n\nYou know your situation better than this README can.\n\n## Concepts and Jargon\n\n### Configurations\n\nConfigurations are values that depend on the *local system environment*. They do not generally change depending on what\nyou're *doing*. Examples include:\n\n* Database name\n* API options\n* Gem or library configurations\n\n### Secrets\n\nThis is stuff you don't want to be read by anyone. Rails calls this concept \"credentials.\" Examples include:\n\n* Usernames and passwords\n* API keys\n\n**This is not a replacement for a password-manager**. Use a\nproper [password sharing tool](https://en.wikipedia.org/wiki/List_of_password_managers) as the primary method for\nsharing passwords within your team. This is especially true for the master encryption key used to secure the secrets\nfile.\n\nSimilarly, you should use a unique encryption key for each environment (eg. your development laptop vs a server).\n\n### XDG Base Directory\n\nThis is a standard that defines where applications should store their files (config, data, etc). The relevant part is\nthat it looks in a couple of places for config files, declared in a pair of environment variables:\n\n1. `XDG_CONFIG_HOME`\n    - Note: Ignored when `$HOME` directory is undefined. Often the case for system daemons.\n    - Default: `~/.config/`\n2. `XDG_CONFIG_DIRS`\n    - Fallback locations, declared as a colon-separated list in priority order.\n    - Default: `/etc/xdg/`\n\nYou can check your current values by running this in a terminal:\n\n    env | grep XDG\n\n### Namespace\n\nThe name of the app's subdirectory under the relevant XDG Base Directory.\n\neg:\n\n`~/.config/name-of-app`\n\nor\n\n`/etc/xdg/name-of-app`\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'invar'\n```\n\nAnd then run in a terminal:\n\n    bundle install\n\n## Usage\n\n### Testing\n\nInvar automatically loads the normal runtime configuration from the config files created by the Rake tasks (details in\nnext section), but tests may need to override some of those values.\n\nCall `#pretend` on the relevant selector:\n\n```ruby\n# Your application uses Invar as normal:\nrequire 'invar'\n\ninvar = Invar.new(namespace: 'my-app')\n\n# ... but in your test suite, require the testing extension\nrequire 'invar/test'\n\n# And override the values as needed. Usually this would be in a test\n# suite hook, like Cucumber's `BeforeAll` or RSpec's `before(:all)`\ninvar[:config][:mysql].pretend database: 'myapp_test'\n```\n\n**Note**: Calling `#pretend` without requiring `invar/test` will raise an `ImmutableRealityError`.\n\nTo override values immediately after the config files are read, use an `Invar.after_load` block:\n\n```ruby\nInvar.after_load do |invar|\n   invar[:config][:postgres].pretend database: 'my_app_test'\nend\n\n# This Invar will return database name 'my_app_test'\ninvar = Invar.new(namespace: 'my-app')\n\nputs invar / :config / :postgres\n```\n\n### Rake Tasks\n\nIn your `Rakefile`, add:\n\n```ruby\nrequire 'invar/rake/tasks'\n\nInvar::Rake::Tasks.define namespace: 'app-name-here'\n```\n\nThen you can use the rake tasks as reported by `rake -T`\n\n#### Show Search Paths\n\nThis will show you the XDG search locations.\n\n    bundle exec rake invar:paths\n\n#### Create Config and Secrets Files\n\nTo create a `config.yml` file and an encrypted `secrets.yml` file:\n\n    bundle exec rake invar:init\n\nIt will also output to terminal the generated master key used to decrypt the secrets file. Save this key in a password\nmanager. If you do not want it to be displayed (eg. you're in public), you can pipe STDOUT to a file:\n\n    bundle exec rake invar:init \u003e master_key\n\nThen handle the `master_key` file as needed.\n\nIf either a pre-existing config or secrets file is found in any of the search path locations, it will yell at you with\nan `AmbiguousSourceError`.\n\n#### Edit Config File\n\nTo open the file your system's default text editor (eg. nano):\n\n    bundle exec rake invar:config\n\n\u003e **Automation tip** You can provide new content over STDIN\n\u003e\n\u003e     bundle exec rake invar:config \u003c config_template.yml\n\n#### Edit Secrets File\n\nTo edit the secrets file, run this and provide the file's encryption key:\n\n    bundle exec rake invar:secrets\n\nThe file will be decrypted and opened in your default editor like the config file. Once you have exited the editor, it\nwill be re-encrypted. **Remember to save your changes!**\n\n\u003e **Automation tip** You can set the current encryption key in the `LOCKBOX_MASTER_KEY` environment variable:\n\u003e\n\u003e     LOCKBOX_MASTER_KEY=sooper_sekret_key_here bundle exec rake invar:secrets\n\n\u003e **Automation tip** Like invar:config, you can provide new content over STDIN\n\u003e\n\u003e     bundle exec rake invar:secrets \u003c secrets_template.yml\n\n#### Rotating the Secrets File Encryption Key\n\nTo re-encrypt the secrets file, run this and provide the file's current encryption key:\n\n    bundle exec rake invar:rotate\n\nA new encryption key will be generated just like using `invar:init`.\n\n\u003e **Automation tip** you can set the current encryption key in the `LOCKBOX_MASTER_KEY` environment variable:\n\u003e\n\u003e     LOCKBOX_MASTER_KEY=sooper_sekret_key_here bundle exec rake invar:rotate\n\n### Code\n\nAssuming file `~/.config/my-app/config.yml` with:\n\n```yml\n---\ndatabase:\n  name: 'my_app_development'\n  host: 'localhost'\n```\n\nAnd an encrypted file `~/.config/my-app/secrets.yml` with:\n\n```yml\n---\ndatabase:\n  user: 'my_app'\n  pass: 'sekret'\n```\n\nThen in `my-app.rb`, you can fetch those values:\n\n```ruby\nrequire 'invar'\n\ninvar = Invar.new 'my-app'\n\n# Use the slash operator to fetch values (sort of like Pathname)\nputs invar / :config / :database / :host\n\n# String keys are okay, too. Also it's case-insensitive\nputs invar / 'config' / 'database' / 'host'\n\n# Secrets are kept in a separate tree \nputs invar / :secret / :database / :username\n\n# And you can get ENV variables. This should print your HOME directory.\nputs invar / :config / :home\n\n# You can also use [] notation, which may be nicer in some situations (like #pretend)\nputs invar[:config][:database][:host]\n```\n\n\u003e **FAQ**: Why not support a dot syntax like `invar.config.database.host`?\n\u003e\n\u003e **A**: Because key names could collide with method names, like `inspect`, `dup`, or `tap`.\n\n### Validation\n\nYou may provide `:configs_schema` and `:secrets_schema` keyword arguments to `Invar.new` and it will use those schema to\nvalidate your `Invar::Reality`.\n\n\u003e **Note** Invar uses dry-schema to validate its internal `configs` and `secrets` trees, not the raw file contents.\n\n`dry-schema` has a lot of shorthand which can add a lot of complexity, but here's a crash course:\n\n* Keys are declared with the `required` method while the value's validator is declared to be a general `schema` type\n  with an accompanying block.\n* In that block are the units of validation logic (*\"predicates\"*)\n    * They get unioned together with a **single** `\u0026`\n      operator, **not** double `\u0026\u0026` like a boolean expression.\n    * There are predefined predicates for a bunch of simple properties\n\nGenerally when used with Invar it will look like:\n\n```ruby\nrequire 'invar'\n\ncnf_schema = Dry::Schema.define do\n   required(:upload).schema do\n      required(:mysql) { str? \u0026 filled? }\n   end\n\n   required(:upload).schema do\n      required(:max_bytes) { int? \u0026 filled? \u0026 gt?(0) }\n      required(:timeout) { int? \u0026 filled? \u0026 gt?(0) }\n   end\n   # ...\nend\n\nsec_schema = Dry::Schema.define do\n   required(:email).schema do\n      required(:username) { str? \u0026 filled? }\n      required(:password) { str? \u0026 filled? }\n   end\n   # ...\nend\n\ninvar = Invar.new 'my-app', configs_schema: cnf_schema, secrets_schema: sec_schema\n# ...\n```\n\nIf there are any unexpected or invalid keys in the *configs* or *secrets* files, Invar will complain about it with\na `SchemaValidationError`.\n\n### Custom Locations\n\nYou can customize the search paths by setting the environment variables `XDG_CONFIG_HOME` and/or `XDG_CONFIG_DIRS` any\ntime you run a Rake task or your application.\n\n    # Looks in /tmp instead of ~/.config/ \n    XDG_CONFIG_HOME=/tmp bundle exec rake invar:paths\n\nYou can also specify the Lockbox decryption keyfile (eg. for automated production environments) by using the\n`:decryption_keyfile` keyword argument to `Invar.new`. Be aware that your secrets are only as secure as the file that\nkeeps the master key, so double check that its file permissions are as restricted as possible.\n\n```ruby\nrequire 'invar'\n\ninvar = Invar.new 'my-app', decryption_keyfile: '/etc/my-app/master_key'\n```\n\n## Alternatives\n\nSome other gems with different approaches:\n\n- [RubyConfig](https://github.com/rubyconfig/config)\n- [Anyway Config](https://github.com/palkan/anyway_config)\n- [AppConfig](https://github.com/Oshuma/app_config)\n- [Fiagro](https://github.com/laserlemon/figaro)\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/TenjinInc/invar.\n\nValued topics:\n\n* Error messages (clarity, hinting)\n* Documentation\n* API\n* Security correctness\n\nThis project is intended to be a friendly space for collaboration, and contributors are expected to adhere to the\n[Contributor Covenant](http://contributor-covenant.org) code of conduct. Play nice.\n\n### Core Developers\n\nAfter checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests. You\ncan also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the\nversion number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,\npush git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\nDocumentation is produced by Yard. Run `bundle exec rake yard`. The goal is to have 100% documentation coverage and 100%\ntest coverage.\n\nRelease notes are provided in `RELEASE_NOTES.md`, and should vaguely\nfollow [Keep A Changelog](https://keepachangelog.com/en/1.0.0/) recommendations.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftenjininc%2Finvar","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftenjininc%2Finvar","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftenjininc%2Finvar/lists"}