Password manager-backed settings stores for Hanami
  • Ruby 93.9%
  • Shell 6.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Aaron Allen 0c852a6a67
feat(file_store): permit dates, times, and symbols in YAML
Bring FileStore closer to Hanami::Settings::YamlFileStore, introduced in
hanami/hanami#1627:

- parse YAML with Date, DateTime, Symbol, and Time permitted, so files
  containing dates, times, or symbols no longer raise
  Psych::DisallowedClass
- accept a block returning the path, resolved lazily on the first fetch,
  and expose it as #path
- read the file on the first fetch rather than when the store is built
- key settings by symbol, so nested values come back symbol-keyed
- treat a missing or empty file as an empty store, and raise TypeError
  when the file parses to something other than a mapping
2026-09-02 10:40:06 -05:00
.config feat: add BitwardenStore with bw_setting DSL 2026-03-17 11:09:54 -05:00
.github Initial commit 2026-03-17 10:32:50 -05:00
bin Initial commit 2026-03-17 10:32:50 -05:00
docs docs: add policy README 2026-03-17 11:38:05 -05:00
lib feat(file_store): permit dates, times, and symbols in YAML 2026-09-02 10:40:06 -05:00
spec feat(file_store): permit dates, times, and symbols in YAML 2026-09-02 10:40:06 -05:00
.editorconfig Initial commit 2026-03-17 10:32:50 -05:00
.gitignore Initial commit 2026-03-17 10:32:50 -05:00
.rspec Initial commit 2026-03-17 10:32:50 -05:00
CHANGELOG.md feat(file_store): permit dates, times, and symbols in YAML 2026-09-02 10:40:06 -05:00
Gemfile Initial commit 2026-03-17 10:32:50 -05:00
hanami-settings-stores.gemspec chore: prepare 0.1.1 release 2026-08-20 09:58:45 -05:00
LICENSE Initial commit 2026-03-17 10:32:50 -05:00
README.md feat(file_store): permit dates, times, and symbols in YAML 2026-09-02 10:40:06 -05:00

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