The project is in a healthy, maintained state
Settings stores that fetch values from password managers like 1Password and Bitwarden, or from a local JSON, TOML, or YAML file, for use with Hanami::Settings.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

~> 0.3
 Project Readme

Hanami::Settings::Stores

Pluggable settings stores for Hanami.

Fetch settings from password managers like 1Password and Bitwarden, or from a local JSON, TOML, or YAML file.

Requirements

CompositeStore and FileStore need nothing beyond the gem.

Installation

Using Gem.coop

Add to your Gemfile:

gem "hanami-settings-stores", source: "https://beta.gem.coop/@aaron"

Using Rubygems

Add to your Gemfile:

gem "hanami-settings-stores", source: "https://rubygems.org"

Usage

OnePasswordStore

# config/app.rb

require "hanami-settings-stores"

module MyApp
  class App < Hanami::App
    config.settings_store = Hanami::Settings::CompositeStore.new(
      Hanami::Settings::EnvStore.new,
      Hanami::Settings::OnePasswordStore.new(vault: "Production")
    )
  end
end

Fetch settings from 1Password items. Define mappings with op_setting in your settings class:

# config/settings.rb

module MyApp
  class Settings < Hanami::Settings
    # Looks up field "database_url" on item "MyApp"
    op_setting :database_url, item: "MyApp", constructor: Types::String

    # Looks up field "secret_key" on a different item
    op_setting :api_key, item: "ExternalService", key: :secret_key, constructor: Types::String

    # Regular settings still work — these come from ENV via the default store
    setting :some_flag, default: false, constructor: Types::Params::Bool
  end
end

BitwardenStore

# config/app.rb

require "hanami-settings-stores"

module MyApp
  class App < Hanami::App
    config.settings_store = Hanami::Settings::CompositeStore.new(
      Hanami::Settings::EnvStore.new,
      Hanami::Settings::BitwardenStore.new
    )
  end
end

Fetch settings from Bitwarden items. Define mappings with bw_setting in your settings class:

# config/settings.rb

module MyApp
  class Settings < Hanami::Settings
    bw_setting :database_url, item: "MyApp", constructor: Types::String
    bw_setting :api_key, item: "ExternalService", key: :secret_key, constructor: Types::String

    setting :some_flag, default: false, constructor: Types::Params::Bool
  end
end

FileStore

# config/app.rb

require "hanami-settings-stores"

module MyApp
  class App < Hanami::App
    config.settings_store = Hanami::Settings::CompositeStore.new(
      Hanami::Settings::EnvStore.new,
      Hanami::Settings::FileStore.new("config/settings.yml")
    )
  end
end

Read settings from a local JSON, TOML, or YAML file. Name the file with a .json, .toml, .yaml, or .yml extension so the store knows how to parse it:

# config/settings.yml

database_url: postgres://localhost/myapp
session_secret: <%= ENV.fetch("SESSION_SECRET") %>
pool_size: 5
# config/settings.rb

module MyApp
  class Settings < Hanami::Settings
    setting :database_url, constructor: Types::String
    setting :session_secret, constructor: Types::String
    setting :pool_size, constructor: Types::Params::Integer
  end
end

Settings match file keys by name, so :database_url reads the database_url key. Only top level keys resolve; nested values come back whole, as a hash with symbol keys.

The file runs through ERB before parsing, so it can read from ENV or call Ruby. This also means the file can run any code it contains. Keep it under your own control, and keep real secrets in ENV or a password manager store.

The store reads and parses the file once, on the first fetch, then memoizes it. Later edits to the file need a restart.

Because the file is parsed rather than read as text, values keep their types: pool_size above arrives as the integer 5, not "5". YAML files are parsed in safe mode, with Date, DateTime, Symbol, and Time permitted, and aliases enabled:

# config/settings.yml

adapter: :postgres
launched_on: 2026-01-01
expires_at: 2026-01-01 12:30:00

A file that does not exist, or that is empty, gives an empty store, so an optional file can sit in a CompositeStore without a guard. A file that parses to something other than a mapping raises a TypeError.

Pass a block instead of a path to resolve the path lazily, on the first fetch — useful when the path depends on config that is not yet set when the store is built:

config.settings_store = Hanami::Settings::CompositeStore.new(
  Hanami::Settings::EnvStore.new,
  Hanami::Settings::FileStore.new { Hanami.app.root.join("config/settings/#{Hanami.env}.yml") },
  Hanami::Settings::FileStore.new { Hanami.app.root.join("config/settings/default.yml") }
)

CompositeStore

Chain multiple stores with fallback resolution. The first store to return a value wins. Settings resolve from ENV first, falling back to another store for anything not found.

Stores

Store Status
CompositeStore Available
FileStore Available
OnePasswordStore Available
BitwardenStore Available

Development

bundle install
mise run spec
mise run ruby

License

MIT