Provider generator (#364)

* Move provider config to family

* Update schema.rb

* Add provier generator

* Add table creation also

* FIX generator namespace

* Add support for global providers also

* Remove over-engineered stuff

* FIX parser

* FIX linter

* Some generator fixes

* Update generator with fixes

* Update item_model.rb.tt

* Add missing linkable concern

* Add missing routes

* Update adapter.rb.tt

* Update connectable_concern.rb.tt

* Update unlinking_concern.rb.tt

* Update family_generator.rb

* Update family_generator.rb

* Delete .claude/settings.local.json

Signed-off-by: soky srm <sokysrm@gmail.com>

* Move docs under API related folder

* Rename Rails generator doc

* Light edits to LLM generated doc

* Small Lunch Flow config panel regressions.

---------

Signed-off-by: soky srm <sokysrm@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
soky srm
2025-12-08 22:52:30 +01:00
committed by GitHub
parent 88952e4714
commit 5d6c1bc280
18 changed files with 2604 additions and 4 deletions

View File

@@ -0,0 +1,935 @@
# Rails `Provider` Generator Guide
This guide explains how to use the `Provider` generators, which make it easy to add new
integrations with either **global** or **per-family** scope credentials.
## Table of Contents
1. [Quick Start](#quick-start)
2. [Global vs Per-Family: Which to Use?](#global-vs-per-family-which-to-use)
3. [Provider:Family Generator](#providerfamily-generator)
4. [Provider:Global Generator](#providerglobal-generator)
5. [Comparison Table](#comparison-table)
6. [Examples](#examples)
---
## Quick Start
### Two Generators Available
```bash
# Per-Family Provider (each family has separate credentials)
rails g provider:family <PROVIDER_NAME> field:type[:secret] ...
# Global Provider (all families share site-wide credentials)
rails g provider:global <PROVIDER_NAME> field:type[:secret] ...
```
### Quick Examples
```bash
# Per-family: Each family has their own Lunchflow API key
rails g provider:family lunchflow api_key:text:secret base_url:string
# Global: All families share the same Plaid credentials
rails g provider:global plaid \
client_id:string:secret \
secret:string:secret \
environment:string:default=sandbox
```
---
## Global vs Per-Family: Which to Use?
### Use `provider:global` When:
- ✅ One set of credentials serves the entire application
- ✅ Provider charges per-application (not per-customer)
- ✅ You control the API account (self-hosted or managed mode)
- ✅ All families can safely share access
- ✅ Examples: Plaid, OpenAI, exchange rate services
### Use `provider:family` When:
- ✅ Each family/customer needs their own credentials
- ✅ Provider charges per-customer
- ✅ Users bring their own API keys
- ✅ Data isolation required between families
- ✅ Examples: Lunch Flow, SimpleFIN, YNAB, personal bank APIs
---
## Provider:Family Generator
### Usage
```bash
rails g provider:family <PROVIDER_NAME> field:type[:secret][:default=value] ...
```
### Example: Adding a MyBank Provider
```bash
rails g provider:family my_bank \
api_key:text:secret \
base_url:string:default=https://api.mybank.com \
refresh_token:text:secret
```
### What Gets Generated
This single command generates:
- ✅ Migration for `my_bank_items` and `my_bank_accounts` tables **with credential fields**
- ✅ Models: `MyBankItem`, `MyBankAccount`, and `MyBankItem::Provided` concern
- ✅ Adapter class for provider integration
- ✅ Simple manual panel view for provider settings
- ✅ Controller with CRUD actions and Turbo Stream support
- ✅ Routes
- ✅ Updates to settings controller and view
### Key Characteristics
- **Credentials**: Stored in `my_bank_items` table (encrypted)
- **Isolation**: Each family has completely separate credentials
- **UI**: Manual form panel at `/settings/providers`
- **Configuration**: Per-family, self-service
---
## Provider:Global Generator
### Usage
```bash
rails g provider:global <PROVIDER_NAME> field:type[:secret][:default=value] ...
```
### Example: Adding a Plaid Provider
```bash
rails g provider:global plaid \
client_id:string:secret \
secret:string:secret \
environment:string:default=sandbox
```
### What Gets Generated
This single command generates:
- ✅ Migration for `plaid_items` and `plaid_accounts` tables **without credential fields**
- ✅ Models: `PlaidItem`, `PlaidAccount`, and `PlaidItem::Provided` concern
- ✅ Adapter with `Provider::Configurable`
- ❌ No controller (credentials managed globally)
- ❌ No view (UI auto-generated by `Provider::Configurable`)
- ❌ No routes (no CRUD needed)
### Key Characteristics
- **Credentials**: Stored in `settings` table (global, not encrypted)
- **Sharing**: All families use the same credentials
- **UI**: Auto-generated at `/settings/providers` (self-hosted mode only)
- **Configuration**: ENV variables or admin settings
### Important Notes
- Credentials are shared by **all families** - use only for trusted services
- Only available in **self-hosted mode** (admin-only access)
- No per-family credential management needed
- Simpler implementation (fewer files generated)
---
## Comparison Table
| Aspect | `provider:family` | `provider:global` |
|--------|------------------|------------------|
| **Credentials storage** | `provider_items` table (per-family) | `settings` table (global) |
| **Credential encryption** | ✅ Yes (ActiveRecord encryption) | ❌ No (plaintext in settings) |
| **Family isolation** | ✅ Complete (each family has own credentials) | ❌ None (all families share) |
| **Files generated** | 9+ files | 5 files |
| **Migration includes credentials** | ✅ Yes | ❌ No |
| **Controller** | ✅ Yes (simple CRUD) | ❌ No |
| **View** | ✅ Manual form panel | ❌ Auto-generated |
| **Routes** | ✅ Yes | ❌ No |
| **UI location** | `/settings/providers` (always) | `/settings/providers` (self-hosted only) |
| **ENV variable support** | ❌ No (per-family can't use ENV) | ✅ Yes (fallback) |
| **Use case** | User brings own API key | Platform provides API access |
| **Examples** | Lunch Flow, SimpleFIN, YNAB | Plaid, OpenAI, TwelveData |
---
## What Gets Generated (Detailed)
### 1. Migration
**File:** `db/migrate/xxx_create_my_bank_tables_and_accounts.rb`
Creates two complete tables with all necessary fields:
```ruby
class CreateMyBankTablesAndAccounts < ActiveRecord::Migration[7.2]
def change
# Create provider items table (stores per-family connection credentials)
create_table :my_bank_items, id: :uuid do |t|
t.references :family, null: false, foreign_key: true, type: :uuid
t.string :name
# Institution metadata
t.string :institution_id
t.string :institution_name
t.string :institution_domain
t.string :institution_url
t.string :institution_color
# Status and lifecycle
t.string :status, default: "good"
t.boolean :scheduled_for_deletion, default: false
t.boolean :pending_account_setup, default: false
# Sync settings
t.datetime :sync_start_date
# Raw data storage
t.jsonb :raw_payload
t.jsonb :raw_institution_payload
# Provider-specific credential fields
t.text :api_key
t.string :base_url
t.text :refresh_token
t.timestamps
end
add_index :my_bank_items, :family_id
add_index :my_bank_items, :status
# Create provider accounts table (stores individual account data from provider)
create_table :my_bank_accounts, id: :uuid do |t|
t.references :my_bank_item, null: false, foreign_key: true, type: :uuid
# Account identification
t.string :name
t.string :account_id
# Account details
t.string :currency
t.decimal :current_balance, precision: 19, scale: 4
t.string :account_status
t.string :account_type
t.string :provider
# Metadata and raw data
t.jsonb :institution_metadata
t.jsonb :raw_payload
t.jsonb :raw_transactions_payload
t.timestamps
end
add_index :my_bank_accounts, :account_id
add_index :my_bank_accounts, :my_bank_item_id
end
end
```
### 2. Models
**File:** `app/models/my_bank_item.rb`
The item model stores per-family connection credentials:
```ruby
class MyBankItem < ApplicationRecord
include Syncable, Provided
enum :status, { good: "good", requires_update: "requires_update" }, default: :good
# Encryption for secret fields
if Rails.application.credentials.active_record_encryption.present?
encrypts :api_key, :refresh_token, deterministic: true
end
validates :name, presence: true
validates :api_key, presence: true, on: :create
validates :refresh_token, presence: true, on: :create
belongs_to :family
has_one_attached :logo
has_many :my_bank_accounts, dependent: :destroy
has_many :accounts, through: :my_bank_accounts
scope :active, -> { where(scheduled_for_deletion: false) }
scope :ordered, -> { order(created_at: :desc) }
scope :needs_update, -> { where(status: :requires_update) }
def destroy_later
update!(scheduled_for_deletion: true)
DestroyJob.perform_later(self)
end
def credentials_configured?
api_key.present? && refresh_token.present?
end
def effective_base_url
base_url.presence || "https://api.mybank.com"
end
end
```
**File:** `app/models/my_bank_account.rb`
The account model stores individual account data from the provider:
```ruby
class MyBankAccount < ApplicationRecord
include CurrencyNormalizable
belongs_to :my_bank_item
# Association through account_providers for linking to internal accounts
has_one :account_provider, as: :provider, dependent: :destroy
has_one :account, through: :account_provider, source: :account
validates :name, :currency, presence: true
def upsert_my_bank_snapshot!(account_snapshot)
update!(
current_balance: account_snapshot[:balance],
currency: parse_currency(account_snapshot[:currency]) || "USD",
name: account_snapshot[:name],
account_id: account_snapshot[:id]&.to_s,
account_status: account_snapshot[:status],
raw_payload: account_snapshot
)
end
end
```
**File:** `app/models/my_bank_item/provided.rb`
The Provided concern connects the item to its provider SDK:
```ruby
module MyBankItem::Provided
extend ActiveSupport::Concern
def my_bank_provider
return nil unless credentials_configured?
Provider::MyBank.new(
api_key,
base_url: effective_base_url,
refresh_token: refresh_token
)
end
end
```
### 3. Adapter
**File:** `app/models/provider/my_bank_adapter.rb`
```ruby
class Provider::MyBankAdapter < Provider::Base
include Provider::Syncable
include Provider::InstitutionMetadata
# Register this adapter with the factory
Provider::Factory.register("MyBankAccount", self)
def provider_name
"my_bank"
end
# Build a My Bank provider instance with family-specific credentials
def self.build_provider(family:)
return nil unless family.present?
# Get family-specific credentials
my_bank_item = family.my_bank_items.where.not(api_key: nil).first
return nil unless my_bank_item&.credentials_configured?
# TODO: Implement provider initialization
Provider::MyBank.new(
my_bank_item.api_key,
base_url: my_bank_item.effective_base_url,
refresh_token: my_bank_item.refresh_token
)
end
def sync_path
Rails.application.routes.url_helpers.sync_my_bank_item_path(item)
end
def item
provider_account.my_bank_item
end
def can_delete_holdings?
false
end
def institution_domain
metadata = provider_account.institution_metadata
return nil unless metadata.present?
metadata["domain"]
end
def institution_name
metadata = provider_account.institution_metadata
return nil unless metadata.present?
metadata["name"] || item&.institution_name
end
def institution_url
metadata = provider_account.institution_metadata
return nil unless metadata.present?
metadata["url"] || item&.institution_url
end
def institution_color
item&.institution_color
end
end
```
### 4. Panel View
**File:** `app/views/settings/providers/_my_bank_panel.html.erb`
A simple manual form for configuring My Bank credentials:
```erb
<div class="space-y-4">
<div class="prose prose-sm text-secondary">
<p class="text-primary font-medium">Setup instructions:</p>
<ol>
<li>Visit your My Bank dashboard to get your credentials</li>
<li>Enter your credentials below and click the Save button</li>
<li>After a successful connection, go to the Accounts tab to set up new accounts</li>
</ol>
<p class="text-primary font-medium">Field descriptions:</p>
<ul>
<li><strong>API Key:</strong> Your My Bank API key (required)</li>
<li><strong>Base URL:</strong> Your My Bank base URL (optional, defaults to https://api.mybank.com)</li>
<li><strong>Refresh Token:</strong> Your My Bank refresh token (required)</li>
</ul>
</div>
<% error_msg = local_assigns[:error_message] || @error_message %>
<% if error_msg.present? %>
<div class="p-2 rounded-md bg-destructive/10 text-destructive text-sm">
<%= error_msg %>
</div>
<% end %>
<%
# Get or initialize a my_bank_item for this family
my_bank_item = Current.family.my_bank_items.first_or_initialize(name: "My Bank Connection")
is_new_record = my_bank_item.new_record?
%>
<%= styled_form_with model: my_bank_item,
url: is_new_record ? my_bank_items_path : my_bank_item_path(my_bank_item),
scope: :my_bank_item,
method: is_new_record ? :post : :patch,
data: { turbo: true },
class: "space-y-3" do |form| %>
<%= form.text_field :api_key,
label: "API Key",
type: :password %>
<%= form.text_field :base_url,
label: "Base URL (Optional)",
placeholder: "https://api.mybank.com (default)",
value: my_bank_item.base_url %>
<%= form.text_field :refresh_token,
label: "Refresh Token",
type: :password %>
<div class="flex justify-end">
<%= form.submit is_new_record ? "Save Configuration" : "Update Configuration",
class: "inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium text-white bg-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-2 transition-colors" %>
</div>
<% end %>
<% items = local_assigns[:my_bank_items] || @my_bank_items || Current.family.my_bank_items.where.not(api_key: nil) %>
<% if items&.any? %>
<div class="flex items-center gap-2">
<div class="w-2 h-2 bg-success rounded-full"></div>
<p class="text-sm text-secondary">Configured and ready to use. Visit the <a href="<%= accounts_path %>" class="link">Accounts</a> tab to manage and set up accounts.</p>
</div>
<% end %>
</div>
```
### 5. Controller
**File:** `app/controllers/my_bank_items_controller.rb`
A simple controller with CRUD actions and Turbo Stream support:
```ruby
class MyBankItemsController < ApplicationController
before_action :set_my_bank_item, only: [:update, :destroy, :sync]
def create
@my_bank_item = Current.family.my_bank_items.build(my_bank_item_params)
@my_bank_item.name ||= "My Bank Connection"
if @my_bank_item.save
if turbo_frame_request?
flash.now[:notice] = t(".success", default: "Successfully configured My Bank.")
@my_bank_items = Current.family.my_bank_items.ordered
render turbo_stream: [
turbo_stream.replace(
"my-bank-providers-panel",
partial: "settings/providers/my_bank_panel",
locals: { my_bank_items: @my_bank_items }
),
*flash_notification_stream_items
]
else
redirect_to settings_providers_path, notice: t(".success"), status: :see_other
end
else
@error_message = @my_bank_item.errors.full_messages.join(", ")
if turbo_frame_request?
render turbo_stream: turbo_stream.replace(
"my-bank-providers-panel",
partial: "settings/providers/my_bank_panel",
locals: { error_message: @error_message }
), status: :unprocessable_entity
else
redirect_to settings_providers_path, alert: @error_message, status: :unprocessable_entity
end
end
end
def update
if @my_bank_item.update(my_bank_item_params)
if turbo_frame_request?
flash.now[:notice] = t(".success", default: "Successfully updated My Bank configuration.")
@my_bank_items = Current.family.my_bank_items.ordered
render turbo_stream: [
turbo_stream.replace(
"my-bank-providers-panel",
partial: "settings/providers/my_bank_panel",
locals: { my_bank_items: @my_bank_items }
),
*flash_notification_stream_items
]
else
redirect_to settings_providers_path, notice: t(".success"), status: :see_other
end
else
@error_message = @my_bank_item.errors.full_messages.join(", ")
if turbo_frame_request?
render turbo_stream: turbo_stream.replace(
"my-bank-providers-panel",
partial: "settings/providers/my_bank_panel",
locals: { error_message: @error_message }
), status: :unprocessable_entity
else
redirect_to settings_providers_path, alert: @error_message, status: :unprocessable_entity
end
end
end
def destroy
@my_bank_item.destroy_later
redirect_to settings_providers_path, notice: t(".success", default: "Scheduled My Bank connection for deletion.")
end
def sync
unless @my_bank_item.syncing?
@my_bank_item.sync_later
end
respond_to do |format|
format.html { redirect_back_or_to accounts_path }
format.json { head :ok }
end
end
private
def set_my_bank_item
@my_bank_item = Current.family.my_bank_items.find(params[:id])
end
def my_bank_item_params
params.require(:my_bank_item).permit(
:name,
:sync_start_date,
:api_key,
:base_url,
:refresh_token
)
end
end
```
### 6. Routes
**File:** `config/routes.rb` (updated)
```ruby
resources :my_bank_items, only: [:create, :update, :destroy] do
member do
post :sync
end
end
```
### 7. Settings Updates
**File:** `app/controllers/settings/providers_controller.rb` (updated)
- Excludes `my_bank` from global provider configurations
- Adds `@my_bank_items` instance variable
**File:** `app/views/settings/providers/show.html.erb` (updated)
- Adds My Bank section with turbo frame
---
## Customization
After generation, you'll typically want to customize three files:
### 1. Customize the Adapter
Implement the `build_provider` method in `app/models/provider/my_bank_adapter.rb`:
```ruby
def self.build_provider(family:)
return nil unless family.present?
# Get the family's credentials
my_bank_item = family.my_bank_items.where.not(api_key: nil).first
return nil unless my_bank_item&.credentials_configured?
# Initialize your provider SDK with the credentials
Provider::MyBank.new(
my_bank_item.api_key,
base_url: my_bank_item.effective_base_url,
refresh_token: my_bank_item.refresh_token
)
end
```
### 2. Update the Model
Add custom validations, helper methods, and business logic in `app/models/my_bank_item.rb`:
```ruby
class MyBankItem < ApplicationRecord
include Syncable, Provided
belongs_to :family
# Validations (the generator adds basic ones, customize as needed)
validates :name, presence: true
validates :api_key, presence: true, on: :create
validates :refresh_token, presence: true, on: :create
validates :base_url, format: { with: URI::DEFAULT_PARSER.make_regexp }, allow_blank: true
# Add custom business logic
def refresh_oauth_token!
# Implement OAuth token refresh logic
provider = my_bank_provider
return false unless provider
new_token = provider.refresh_token!(refresh_token)
update(refresh_token: new_token)
rescue Provider::MyBank::AuthenticationError
update(status: :requires_update)
false
end
# Override effective methods for better defaults
def effective_base_url
base_url.presence || "https://api.mybank.com"
end
end
```
### 3. Customize the View
Edit the generated panel view `app/views/settings/providers/_my_bank_panel.html.erb` to add custom content:
```erb
<div class="space-y-4">
<%# Add custom header %>
<div class="flex items-center gap-3">
<%= image_tag "my_bank_logo.svg", class: "w-8 h-8" %>
<h3 class="text-lg font-semibold">My Bank Integration</h3>
</div>
<%# The generated form content goes here... %>
<%# Add custom help section %>
<div class="mt-4 p-4 bg-subtle rounded-lg">
<p class="text-sm text-secondary">
Need help? Visit <a href="https://help.mybank.com" class="link">My Bank Help Center</a>
</p>
</div>
</div>
```
---
## Examples
### Example 1: Simple API Key Provider
```bash
rails g provider:family coinbase api_key:text:secret
```
Result: Basic provider with just an API key field.
### Example 2: OAuth Provider
```bash
rails g provider:family stripe \
client_id:string:secret \
client_secret:string:secret \
access_token:text:secret \
refresh_token:text:secret
```
Then customize the adapter to implement OAuth flow.
### Example 3: Complex Provider
```bash
rails g provider:family enterprise_bank \
api_key:text:secret \
environment:string \
base_url:string \
webhook_secret:text:secret \
rate_limit:integer
```
Then add custom validations and logic in the model:
```ruby
class EnterpriseBankItem < ApplicationRecord
# ... (basic setup)
validates :environment, inclusion: { in: %w[sandbox production] }
validates :rate_limit, numericality: { greater_than: 0 }, allow_nil: true
def effective_rate_limit
rate_limit || 100 # Default to 100 requests/minute
end
end
```
---
## Tips & Best Practices
### 1. Always Run Migrations
```bash
rails db:migrate
```
### 2. Test in Console
```ruby
# Check if adapter is registered
Provider::Factory.adapters
# => { ... "MyBankAccount" => Provider::MyBankAdapter, ... }
# Test provider building
family = Family.first
item = family.my_bank_items.create!(name: "Test", api_key: "test_key", refresh_token: "test_refresh")
provider = Provider::MyBankAdapter.build_provider(family: family)
```
### 3. Use Proper Encryption
Always check that encryption is set up:
```ruby
# In your model
if Rails.application.credentials.active_record_encryption.present?
encrypts :api_key, :refresh_token, deterministic: true
else
Rails.logger.warn "ActiveRecord encryption not configured for #{self.name}"
end
```
### 4. Implement Proper Error Handling
```ruby
def self.build_provider(family:)
return nil unless family.present?
item = family.my_bank_items.where.not(api_key: nil).first
return nil unless item&.credentials_configured?
begin
Provider::MyBank.new(item.api_key)
rescue Provider::MyBank::ConfigurationError => e
Rails.logger.error("MyBank provider configuration error: #{e.message}")
nil
end
end
```
### 5. Add Integration Tests
```ruby
# test/models/provider/my_bank_adapter_test.rb
class Provider::MyBankAdapterTest < ActiveSupport::TestCase
test "builds provider with valid credentials" do
family = families(:family_one)
item = family.my_bank_items.create!(
name: "Test Bank",
api_key: "test_key"
)
provider = Provider::MyBankAdapter.build_provider(family: family)
assert_not_nil provider
assert_instance_of Provider::MyBank, provider
end
test "returns nil without credentials" do
family = families(:family_one)
provider = Provider::MyBankAdapter.build_provider(family: family)
assert_nil provider
end
end
```
---
## Troubleshooting
### Panel Not Showing
1. Check that the provider is excluded in `settings/providers_controller.rb`:
```ruby
@provider_configurations = Provider::ConfigurationRegistry.all.reject do |config|
config.provider_key.to_s.casecmp("my_bank").zero?
end
```
2. Check that the instance variable is set:
```ruby
@my_bank_items = Current.family.my_bank_items.ordered.select(:id)
```
3. Check that the section exists in `settings/providers/show.html.erb`:
```erb
<%= settings_section title: "My Bank" do %>
<turbo-frame id="my-bank-providers-panel">
<%= render "settings/providers/my_bank_panel" %>
</turbo-frame>
<% end %>
```
### Form Not Submitting
1. Check routes are properly added:
```bash
rails routes | grep my_bank
```
2. Check turbo frame ID matches:
- View: `<turbo-frame id="my-bank-providers-panel">`
- Controller: Uses `"my-bank-providers-panel"` in turbo_stream.replace
### Encryption Not Working
1. Check credentials are configured:
```bash
rails credentials:edit
```
2. Add encryption keys if missing:
```yaml
active_record_encryption:
primary_key: (generate with: rails db:encryption:init)
deterministic_key: (generate with: rails db:encryption:init)
key_derivation_salt: (generate with: rails db:encryption:init)
```
3. Or use environment variables:
```bash
export ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY="..."
export ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY="..."
export ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT="..."
```
---
## Advanced: Creating a Provider SDK
For complex providers, consider creating a separate SDK class:
```ruby
# app/models/provider/my_bank.rb
class Provider::MyBank
class AuthenticationError < StandardError; end
class RateLimitError < StandardError; end
def initialize(api_key, base_url: "https://api.mybank.com")
@api_key = api_key
@base_url = base_url
@client = HTTP.headers(
"Authorization" => "Bearer #{api_key}",
"User-Agent" => "MyApp/1.0"
)
end
def get_accounts
response = @client.get("#{@base_url}/accounts")
handle_response(response)
end
def get_transactions(account_id, start_date: nil, end_date: nil)
params = { account_id: account_id }
params[:start_date] = start_date.iso8601 if start_date
params[:end_date] = end_date.iso8601 if end_date
response = @client.get("#{@base_url}/transactions", params: params)
handle_response(response)
end
private
def handle_response(response)
case response.code
when 200...300
JSON.parse(response.body, symbolize_names: true)
when 401, 403
raise AuthenticationError, "Invalid API key"
when 429
raise RateLimitError, "Rate limit exceeded"
else
raise StandardError, "API error: #{response.code} #{response.body}"
end
end
end
```
---
## Summary
The per-family `Provider` Rails generator system provides:
-**Fast development** - Generate in seconds, not hour
-**Consistency** - All providers follow the same pattern
-**Maintainability** - Clear structure and conventions
-**Flexibility** - Easy to customize for complex needs
-**Security** - Built-in encryption for sensitive fields
-**Documentation** - Self-documenting with descriptions
Use it whenever you need to add a new provider where each family needs their own credentials.