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

Quick examples

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

Reserved field names

The generator rejects field names that are already defined on the items table. Passing a reserved name raises a Thor::Error at generation time rather than producing broken migrations or inconsistent models. Reserved names include: family_id, institution_id, institution_name, institution_domain, institution_url, institution_color, status, scheduled_for_deletion, pending_account_setup, sync_start_date, raw_payload, raw_institution_payload. Note that family (without _id) is not reserved — the migration writes t.references :family, which creates family_id, so a field named family is not a collision.

Example: Adding a MyBank provider

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
  • ✅ Bank sync wiring: the settings controller’s FAMILY_PANELS entry and sync maps, and a provider_summary case
The last item means the provider appears under Your connections on the Bank sync page, its connection row shows a sync status, and the Sync button works — all without any manual follow-up steps. The generated item partial renders the provider’s logo with ProviderLogo. That reads the provider’s Provider::Metadata::REGISTRY entry, which the generator does not create, so add one in app/models/provider/metadata.rb. See the providers LLM guide for the fields it reads (domain, logo_text, logo_color, logo_icon) and which are used when Brandfetch has no icon or is not configured. The item partial declares visible_accounts: as a strict local. Callers must pass the accounts to display explicitly. The rendered account list uses that local; the partial can still query the item’s accounts for summary counts. Pass only the accounts you want shown:
The generated caller uses visible_card_accounts(item) to respect the current user’s account visibility. A strict local requires an explicit argument; it does not enforce authorization, so callers must not replace the filtered list with all accounts indiscriminately.

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

Example: Adding a Plaid provider

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

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. The generator writes the Rails version in use when it runs, taken from Rails::VERSION::MAJOR and Rails::VERSION::MINOR joined by a dot (for example, 8.1). The version in any generated migration is then fixed — it pins the semantics the migration was written against and is not rewritten by later upgrades.

2. Models

File: app/models/my_bank_item.rb The item model stores per-family connection credentials:
File: app/models/my_bank_account.rb The account model stores individual account data from the provider:
File: app/models/my_bank_item/provided.rb The Provided concern connects the item to its provider SDK:

3. Adapter

File: app/models/provider/my_bank_adapter.rb

4. Controller

File: app/controllers/my_bank_items_controller.rb The generated controller inherits StreamExtensions through ApplicationController and uses frame_redirect_to for missing-credential exits. The account-linking flow actions (preload_accounts, select_accounts, select_existing_account) call frame_redirect_to when a required credential check fails. A plain redirect_to inside a Turbo frame follows the redirect inside the frame, so the resulting page renders inside the modal and the flash message is never shown. frame_redirect_to detects whether the request came from a Turbo frame and, when it did, renders a frame containing a turbo-stream redirect action, which causes a full-page visit instead of a frame navigation.
The key difference from a plain redirect_to in those credential-check guards is that frame_redirect_to uses turbo_frame_request? to detect whether the request originated inside a Turbo frame. When it did, it renders a frame that carries a turbo-stream redirect action, which exits the frame and performs a full-page visit to the target path with the alert flash intact. For requests outside a frame it falls back to a normal redirect_to.

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:

2. Update the model

Add custom validations, helper methods, and business logic in app/models/my_bank_item.rb:

3. Customize the view

Edit the generated panel view app/views/settings/providers/_my_bank_panel.html.erb to add custom content. StyledFormBuilder (used via styled_form_with) accepts these options on every text-type field in addition to standard HTML attributes: Example — adding a hint to the API key field:
Fields rendered without a label (label: false or inline: true) receive the hint directly after the input, without a wrapper, so their layout stays unchanged.

Examples

Example 1: Simple API key provider

Result: Basic provider with just an API key field.

Example 2: OAuth provider

Then customize the adapter to implement OAuth flow.

Example 3: Complex provider

Then add custom validations and logic in the model:

Reserved field names

The generator rejects field names that conflict with columns already defined in the standard items table. Passing a reserved name raises an error at generation time rather than failing silently during db:migrate. Reserved names (cannot be used as custom fields): Note that family itself is not reserved — the migration uses t.references :family which creates family_id, so a field named family does not collide. If you need to customize the behavior of a standard column, do so in the model after generation rather than re-declaring it as a custom field.

Tips & best practices

1. Always run migrations

2. Test in console

3. Use proper encryption

Always check that encryption is set up:

4. Implement proper error handling

5. Add integration tests

Reserved field names

The provider:family generator rejects field names that collide with columns already present on every items table. Passing a reserved name raises an error at generation time rather than failing silently at db:migrate. Reserved names (cannot be used as custom fields): institution_id, institution_name, institution_domain, institution_url, institution_color, status, scheduled_for_deletion, pending_account_setup, sync_start_date, raw_payload, raw_institution_payload, family_id If you need to store data that overlaps with one of these columns, use the built-in column directly — it already exists on the generated table.

Troubleshooting

Reserved field name error

If the generator raises Thor::Error: '<field>' is a reserved field name, remove that field from the command. The items table already defines it, so declaring it again would produce a duplicate migration column and inconsistent model code. See Reserved field names for the full list.

Panel not showing

  1. Check that FAMILY_PANELS in settings/providers_controller.rb includes an entry for the provider:
  2. Check that the instance variable is set in the controller’s show action:
  3. Check that provider_summary in app/helpers/settings_helper.rb has a "my_bank" case.

Form not submitting

  1. Check routes are properly added: rails routes | grep my_bank
  2. Check the panel root ID matches the turbo_id in FAMILY_PANELS: the view must have id="my_bank-providers-panel" at its root, which render_provider_panel replaces via Turbo Stream.

Encryption not working

  1. Check credentials are configured: rails credentials:edit
  2. Add encryption keys if missing
  3. Or use environment variables

Reserved field name error

The generator rejects field names that collide with columns already defined on the items table. If you see a Thor::Error like:
Drop the named field from your command — the standard column covers it. Reserved names include: institution_id, institution_name, institution_domain, institution_url, institution_color, status, family_id, scheduled_for_deletion, pending_account_setup, sync_start_date, raw_payload, raw_institution_payload. Note that family (without _id) is not reserved — t.references :family creates family_id, so a field named family does not collide.

NoMethodError: undefined method 'syncable' during family sync

If a freshly generated provider raises this error and takes down the nightly family sync, the generated item model is missing the syncable scope. Add it manually:
Providers generated after the fix include this scope automatically.

Advanced: Creating a provider SDK

For complex providers, consider creating a separate SDK class:

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.