Documentation Index
Fetch the complete documentation index at: https://sure-917046f5-docs-backup-restore-clarity.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
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.
Quick start
Two generators available
# 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
# 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
rails g provider:family <PROVIDER_NAME> field:type[:secret][:default=value] ...
Example: Adding a MyBank provider
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
rails g provider:global <PROVIDER_NAME> field:type[:secret][:default=value] ...
Example: Adding a Plaid provider
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:
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:
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:
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:
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
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
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:
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:
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.
Examples
Example 1: Simple API key provider
rails g provider:family coinbase api_key:text:secret
Result: Basic provider with just an API key field.
Example 2: OAuth provider
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
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:
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
2. Test in console
# 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:
# 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
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
# 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
- Check that the provider is excluded in
settings/providers_controller.rb
- Check that the instance variable is set
- Check that the section exists in
settings/providers/show.html.erb
- Check routes are properly added:
rails routes | grep my_bank
- Check turbo frame ID matches between view and controller
Encryption not working
- Check credentials are configured:
rails credentials:edit
- Add encryption keys if missing
- Or use environment variables
Advanced: Creating a provider SDK
For complex providers, consider creating a separate SDK class:
# 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 hours
- ✅ 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.