Banking Provider Integration#
Sure integrates with banking providers — currently EnableBanking (European Open Banking / PSD2) and Akahu (New Zealand) — through a shared, consistent architecture. Each provider follows the same structural pattern: a provider Item (credential holder) → intermediate Account table (provider-specific account mirror) → Sure's internal Account record. Syncing is handled uniformly by the Syncable mixin and SyncJob background queue.
Data Model: Items, Provider Accounts, and the Account Link#
Each provider connection is represented by an Item record owned by a Family:
| Model | Table | Key fields |
|---|---|---|
EnableBankingItem | enable_banking_items | session_id, session_expires_at, aspsp_name, country_code, application_id |
AkahuItem | akahu_items | app_token, user_token, status, pending_account_setup |
Items do not link directly to Sure accounts. Instead, provider-specific intermediate tables hold the raw account mirror:
| Join model | Table | Belongs to |
|---|---|---|
EnableBankingAccount | enable_banking_accounts | enable_banking_item |
AkahuAccount | akahu_accounts | akahu_item |
The binding to Sure's internal accounts table goes through a separate account_providers polymorphic join table , so the same internal account can never be linked to two provider accounts of the same type (enforced by a unique index on [account_id, provider_type]). The full traversal is:
AkahuItem / EnableBankingItem
→ [akahu_accounts / enable_banking_accounts] (has_many)
→ [account_providers] (has_one, polymorphic)
→ Account (internal record)
Both items expose has_many :accounts, through: :akahu_accounts / through: :enable_banking_accounts .
Syncable Mixin#
Both AkahuItem and EnableBankingItem include Syncable (alongside Provided, Unlinking, and Encryptable) .
Syncable (app/models/concerns/syncable.rb) adds a has_many :syncs polymorphic association and the key method sync_later:
- Deduplication: Inside a database transaction with a row lock,
sync_laterchecks for anyvisible(< 5 min old, non-terminal)Sync. If one exists, it expands the date window rather than creating a duplicate . - Enqueueing: Only if no recent sync exists is a new
Syncrecord created andSyncJob.perform_later(sync)called . - Delegation:
perform_syncandperform_post_syncdelegate toself.class::Syncer, so each model supplies its ownSyncerinner class.
Sync State Machine (Sync model)#
The Sync model (table: syncs) is polymorphic (syncable_type / syncable_id) and uses AASM for state management :
States: pending → syncing → completed / failed / stale
- Syncs older than 24 hours that are still incomplete are marked
staleby a cleanup cron . - The
visiblescope (< 5 min old + incomplete) is whatSyncable#sync_laterchecks to prevent duplicates . - Syncs are hierarchical: an Item sync is the parent; each linked
Accountsync is a child. The parent only finalizes once all children complete . Sync.for_familyuses reflection to automatically discover all*_itemsassociations onFamilythat includeSyncable, so no manual registry is needed when adding a new provider .
SyncJob#
SyncJob runs on the high_priority queue and calls sync.perform. It accepts an optional balances_only: flag injected as a singleton method on the Sync instance (rather than persisted) to adjust sync behavior at runtime without a schema change .
Provider Syncers#
Each provider supplies a Syncer inner class following the same phased contract:
AkahuItem::Syncer (app/models/akahu_item/syncer.rb) phases:
- Import latest data from Akahu API (
import_latest_akahu_data) - Identify linked vs. unlinked accounts; set
pending_account_setupflag - Process transactions via
AkahuAccount::Processor - Schedule child
Accountsyncs viaschedule_account_syncs(parent_sync: sync)
EnableBankingItem::Syncer (app/models/enable_banking_item/syncer.rb) follows the same four phases but adds a session validation guard — if the session is expired the item is immediately marked requires_update and the sync aborts cleanly.
Both syncers include SyncStats::Collector to attach structured stats (accounts total/linked/unlinked) to the Sync record.
Adding a New Provider#
The pattern to follow is:
- Create
{Provider}Itemmodel —include Syncable— withhas_many :{provider}_accountsandhas_many :accounts, through: :{provider}_accounts. - Create the intermediate
{Provider}Accountmodel withhas_one :account_providerandbelongs_to :{provider}_item. - Implement
{Provider}Item::Syncerwithperform_sync(sync)andperform_post_sync. - Add
has_many :{provider}_itemstoFamily—Sync.for_familywill pick it up automatically via the*_itemsreflection .