Account Provider Architecture#
Sure uses a provider-specific storage + polymorphic join pattern to connect its internal Account records to external banking data providers. Every provider gets its own isolated table (e.g., plaid_accounts, simplefin_accounts, akahu_accounts) and the canonical account_providers join table bridges those tables to Sure's unified accounts table.
This separation keeps provider data (raw payloads, provider-native IDs, credentials) contained in provider tables while the Account record remains a clean internal domain object.
Data Model#
The account_providers Join Table#
account_providers is a polymorphic join table with three columns of interest:
| Column | Type | Notes |
|---|---|---|
account_id | uuid | FK → accounts |
provider_type | string | AR polymorphic discriminator (e.g. "PlaidAccount", "SimplefinAccount") |
provider_id | uuid | FK into the provider table identified by provider_type |
Two compound unique indexes enforce integrity :
[account_id, provider_type]— one Sure account can only be linked to one record of each provider type[provider_type, provider_id]— one provider account record can only be linked to one Sure account
AccountProvider Model#
AccountProvider is the AR model for this join table. Key associations and validations:
belongs_to :account/belongs_to :provider, polymorphic: truehas_many :holdings, dependent: :nullify— holdings carry anaccount_provider_idFK so provider-synced holdings can be distinguished from manual onesvalidates :account_id, uniqueness: { scope: :provider_type }andvalidates :provider_id, uniqueness: { scope: :provider_type }adapter— delegates toProvider::Factoryto instantiate a provider-specific adapter object
Provider Account Tables#
Each provider maintains its own intermediate model and table that stores raw upstream data:
| Model | Table | Parent Item |
|---|---|---|
PlaidAccount | plaid_accounts | PlaidItem |
SimplefinAccount | simplefin_accounts | SimplefinItem |
AkahuAccount | akahu_accounts | AkahuItem |
EnableBankingAccount | enable_banking_accounts | EnableBankingItem |
WiseAccount | wise_accounts | WiseItem |
CoinbaseAccount | coinbase_accounts | CoinbaseItem |
CoinstatsAccount | coinstats_accounts | CoinstatsItem |
BinanceAccount, KrakenAccount, IbkrAccount, Trading212Account, SnaptradeAccount, BrexAccount, MercuryAccount, UpAccount, RedbarkAccount, SophtronAccount, IndexaCapitalAccount, LunchflowAccount | respective tables | respective Items |
All provider account models follow the same structural pattern: they store encrypted raw API payloads and declare has_one :account_provider, as: :provider / has_one :linked_account, through: :account_provider to reach Sure's internal account .
Traversal Pattern#
ProviderItem (e.g. PlaidItem)
→ has_many :provider_accounts (e.g. PlaidAccount)
→ has_one :account_provider, as: :provider
→ has_one :linked_account (Account)
From the Account side, the Account::Linkable concern (included in Account) provides:
has_many :account_providers— all provider linkslinked?— true if anyaccount_providersrecord exists (or legacy FK is set)provider/providers— primary adapter or all adapters; supports multi-provider accountsprovider_for(provider_type)/provider_account_for(provider_type)— look up a specific providerlinked_to?(provider_type)— predicate check for a specific provider type
Legacy FK Migration#
Plaid and SimpleFIN originally linked to accounts via direct foreign keys on the accounts table (plaid_account_id, simplefin_account_id). These are being migrated out in favor of account_providers, but both paths coexist during the transition.
Each legacy provider account model exposes a dual-read helper that prefers the new path :
def current_account
linked_account || account # AccountProvider path first, legacy FK fallback
end
The Account.manual scope must check both paths to correctly identify fully unlinked accounts :
scope :manual, -> {
left_joins(:account_providers)
.where(account_providers: { id: nil })
.where(plaid_account_id: nil, simplefin_account_id: nil)
}
SimplefinAccount#ensure_account_provider! is an idempotent migration helper that creates the AccountProvider record for an already-linked SimpleFIN account . The Account::Linkable#linked? method and the linked scope also include legacy FK checks until the migration is complete .
Key Source Files#
| File | Purpose |
|---|---|
app/models/account_provider.rb | Join model, adapter factory, uniqueness validations |
app/models/account/linkable.rb | Account-side concern: linked?, provider, providers |
app/models/plaid_account.rb | Plaid provider account; dual-path current_account |
app/models/simplefin_account.rb | SimpleFIN provider account; ensure_account_provider! |
app/models/account.rb | Core account model; manual scope, create_from_* factory methods |
db/schema.rb | account_providers table + unique indexes |