Skip to main content

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

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.