Skip to main content
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

Aspectprovider:familyprovider:global
Credentials storageprovider_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 generated9+ files5 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 caseUser brings own API keyPlatform provides API access
ExamplesLunch Flow, SimpleFIN, YNABPlaid, 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

rails db:migrate

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

  1. Check that the provider is excluded in settings/providers_controller.rb
  2. Check that the instance variable is set
  3. Check that the section exists in settings/providers/show.html.erb

Form not submitting

  1. Check routes are properly added: rails routes | grep my_bank
  2. Check turbo frame ID matches between view and controller

Encryption not working

  1. Check credentials are configured: rails credentials:edit
  2. Add encryption keys if missing
  3. 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.