> ## Documentation Index
> Fetch the complete documentation index at: https://sure-917046f5-docs-cloudflare-tunnel-self-hosting.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Rails provider generator

> Generate new provider integrations with global or per-family credentials

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

```bash theme={null}
# 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 theme={null}
# 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 theme={null}
rails g provider:family <PROVIDER_NAME> field:type[:secret][:default=value] ...
```

### Example: Adding a MyBank provider

```bash theme={null}
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 theme={null}
rails g provider:global <PROVIDER_NAME> field:type[:secret][:default=value] ...
```

### Example: Adding a Plaid provider

```bash theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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`:

```ruby theme={null}
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 theme={null}
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

```bash theme={null}
rails g provider:family coinbase api_key:text:secret
```

Result: Basic provider with just an API key field.

### Example 2: OAuth provider

```bash theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
rails db:migrate
```

### 2. Test in console

```ruby theme={null}
# 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 theme={null}
# 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 theme={null}
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 theme={null}
# 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:

```ruby theme={null}
# 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.
