SimpleFIN Integration#
SimpleFIN is Sure's primary US/global bank connectivity provider, covering depository, credit card, loan, investment, and crypto account types. It uses a bridge model: users create a SimpleFIN Bridge that aggregates one or more financial institutions, then generate a setup token to connect it to Sure. Unlike Plaid, no server-side cleanup is needed on disconnect — the access URL simply becomes inactive .
Architecture: Provider/Client Pattern#
Provider::Simplefin (app/models/provider/simplefin.rb) is an HTTParty-based client with a 120-second timeout and SslConfigurable support. Its public interface has three methods:
claim_access_url(setup_token)— Base64-decodes the setup token and POSTs to the claim URL to exchange it for an access URL containing embedded HTTP Basic Auth credentials.get_accounts(access_url, start_date:, end_date:, pending:)— Fetches accounts (with optional transactions/holdings) using Unix-timestamp date params. Returns parsed JSON or raises a typedSimplefinError.get_info(base_url)— Fetches server info lines from the bridge.
Transient network errors (SocketError, Net::*Timeout, ECONNRESET, etc.) are retried up to 3 times with exponential backoff and 25% jitter . The claim endpoint uses max_retries: 1, backoff: false for low-latency token exchange .
Runtime configuration is centralized in config/initializers/simplefin.rb, which reads:
SIMPLEFIN_INCLUDE_PENDING(default:true) — controls whether pending transactions are fetchedSIMPLEFIN_DEBUG_RAW(default:false) — enables raw payload loggingmoney_market_tickersandmoney_market_patterns— lists for detecting cash-equivalent holdings in investment accounts
Data Model#
The Item → Account → account_providers chain mirrors the pattern used by other Sure providers (Akahu, EnableBanking). See Banking Provider Integration for the shared architecture overview.
Family
→ SimplefinItem (one bridge connection)
→ SimplefinAccount[] (one per upstream bank account)
→ AccountProvider (polymorphic join, provider_type="SimplefinAccount")
→ Account (Sure's canonical internal account)
simplefin_items — owns the bridge credential :
| Column | Type | Notes |
|---|---|---|
access_url | text | Encrypted; contains embedded HTTP Basic Auth |
name | string | User-assigned display name |
institution_id/name/url/domain | string | Populated from first account's org data |
status | string | good or requires_update |
raw_payload | jsonb | Encrypted; last full /accounts response |
raw_institution_payload | jsonb | Encrypted; institution metadata |
simplefin_accounts — mirrors each upstream account :
| Column | Type | Notes |
|---|---|---|
account_id | string | Upstream SimpleFIN account ID |
name | string | Account display name from provider |
currency | string | ISO code or URL (normalized) |
current_balance / available_balance | decimal(19,4) | Provider-reported balances |
account_type / account_subtype | string | Provider type classification |
raw_payload | jsonb | Encrypted; per-account API snapshot |
raw_transactions_payload | jsonb | Encrypted; accumulated transaction history |
raw_holdings_payload | jsonb | Encrypted; investment holdings |
org_data | jsonb | Institution metadata (org field from API) |
Encryption is applied to all sensitive fields via encrypts when encryption_ready? . SimplefinItem also encrypts access_url deterministically (enabling exact-match queries) .
The account_providers polymorphic join table links SimplefinAccount to Account. SimplefinAccount#current_account prefers the AccountProvider-linked account and falls back to the legacy direct FK (accounts.simplefin_account_id) during the dual-write migration window .
Sync Pipeline#
The sync is driven by SimplefinItem::Syncer (app/models/simplefin_item/syncer.rb), which SimplefinItem exposes via the shared Syncable concern.
Phase 1 – Discovery / Balances-only#
When no linked accounts exist yet (first connection), the syncer runs a balances-only import via Importer#import_balances_only . This fetches /accounts without date filters, upserts minimal SimplefinAccount attributes, and updates balances for any already-linked accounts — but does not create new Account records or fetch transactions. last_synced_at is left nil so the next full sync triggers the chunked-history path .
Phase 2 – Chunked History Import (first full sync)#
When last_synced_at is nil or all linked accounts have no transactions yet, Importer#import_with_chunked_history runs . It:
- Calls
perform_account_discovery— an unbounded/accountsfetch to discover all accounts regardless of date . - Walks backwards in 60-day chunks from today up to 1 year (max 6 chunks) .
- Stops early after 2 consecutive empty chunks (adaptive stopping) .
- Merges transactions across chunks by ID (or composite key fallback) with a comparator that prefers non-pending records with real
postedtimestamps .
Phase 3 – Regular Sync#
Subsequent syncs use import_regular_sync, which fetches a 30-day lookback window from last_synced_at with a buffer .
Phase 4 – Account Processing#
After import, SimplefinItem#process_accounts dispatches SimplefinAccount::Processor for each linked account . The processor runs four sub-steps :
process_account!— normalizes the balance (including liability sign logic viaOverpaymentAnalyzer), updatesaccounts.balanceandaccounts.cash_balance, and callsaccount.set_current_balancewhich feedsCurrentBalanceManagerto write thecurrent_anchorvaluation .process_transactions— delegates toSimplefinAccount::Transactions::Processor.process_investments— for Investment accounts: runsTransactionsProcessorandHoldingsProcessor; enqueuesSimplefinHoldingsApplyJobwhen holdings change .process_liabilities— for CreditCard/Loan: delegates toCreditProcessororLoanProcessor.
Balance history is then computed in a child Account::Syncer sync via the reverse-balance strategy (start from today's anchor, work backwards). See Balance History System.
Deduplication and Stale Linkage Repair#
Transaction deduplication uses the transaction id (or fitid) as the primary key, falling back to a [posted, amount, description] composite key when both are absent . During import, pending records are superseded by matching posted records using the priority comparator in Importer#import_account . After each account is processed, Entry.reconcile_pending_duplicates auto-resolves exact pending↔posted matches, and Entry.auto_exclude_stale_pending excludes pending entries older than 8 days .
Stale linkage repair handles the scenario where a user deletes and re-adds an institution in SimpleFIN, which generates new account_id values upstream. SimplefinItem#repair_stale_linkages detects unlinked SimplefinAccount records that have transactions but no Account link, matches them to linked records by name (case-insensitive), merges their transaction history, and transfers the AccountProvider (or legacy FK) from the old record to the new one .
Orphaned SimplefinAccount records (upstream account_id no longer returned by the API and not linked to any Account) are pruned during perform_account_discovery .
Key Files#
| File | Role |
|---|---|
app/models/provider/simplefin.rb | HTTParty client, retry logic, error types |
app/models/simplefin_item.rb | Item model, stale linkage repair, merge helpers |
app/models/simplefin_item/syncer.rb | Sync orchestration, Turbo broadcast |
app/models/simplefin_item/importer.rb | Chunked history, regular sync, discovery, dedup |
app/models/simplefin_account.rb | Account mirror model, snapshot upserts |
app/models/simplefin_account/processor.rb | Balance/transaction/investment/liability processing |
db/migrate/20250807143728_create_simplefin_items.rb | simplefin_items schema |
db/migrate/20250807143819_create_simplefin_accounts.rb | simplefin_accounts schema |
config/initializers/simplefin.rb | Runtime config (pending, debug, money market tickers) |