Skip to main content
Version: 1.0.0
Created: 2026-01-28
Updated: 2026-01-29
Status: 📋 Planned
Owner: FA (Finance & Accounting)

Overview

This document defines the integration contract between Encore OS’s FA module and Plaid for automated bank feed integration. Plaid provides broad institution coverage (11,000+ globally), OAuth-based authentication, and a developer-friendly API.

Integration Pattern

Type: External API Integration via Edge Functions
Direction: Outbound (Encore OS → Plaid) + Inbound (Webhooks)
Authentication: API Keys (client_id + secret)

Authentication Requirements

Environment Matrix

Credentials


API Endpoints

Plaid API (Outbound)

Edge Functions (Internal)


Transfer UI (One-Time Payment / Disbursement)

Transfer UI is Plaid’s hosted flow for one-time debit or credit. It is not used for batch payroll (see Programmatic Plaid Transfer below). Flow:
  1. Create intent: Frontend calls plaid-transfer-intent-create with organization_id, mode (PAYMENT | DISBURSEMENT), amount, description, ach_class, user (e.g. legal_name). Backend calls Plaid POST /transfer/intent/create and returns transfer_intent_id.
  2. Link token: Frontend calls plaid-create-link-token with organization_id, transfer_intent_id, and optional link_customization_name. Backend returns a link token with products: ["transfer"] and transfer.intent_id.
  3. Open Link: Frontend opens Plaid Link with that token; user authorizes the transfer in Plaid’s UI.
  4. Post-success: On Link onSuccess, metadata.transfer_status is available; frontend may call plaid-transfer-intent-get with transfer_intent_id to get final status, transfer_id, and failure_reason.
Configuration:
  • Origination account: Required for transfer intent. Resolved in order: (1) fa_module_settings.plaid_origination_account_id for the organization, (2) env PLAID_ORIGINATION_ACCOUNT_ID. At least one must be set.
  • Link customization: In Plaid Dashboard, create a Link customization with Account Select: Enabled for one account. Optionally set fa_module_settings.plaid_link_customization_name per org, or pass link_customization_name when requesting the link token / from frontend (useTransferUI / TransferUIButton).
Frontend: Platform banking exposes useTransferUI and TransferUIButton from @/platform/banking; they orchestrate intent create → link token → Link open → optional intent/get.

Programmatic Plaid Transfer (HR Payroll)

Payroll direct deposit uses Plaid Transfer in a two-step flow. Per Plaid: Creating transfers:
  1. Authorize: Call POST /transfer/authorization/create with access_token, account_id, type, network, amount, ach_class, user, and an idempotency_key. Plaid returns authorization_id and decision (approved/declined).
  2. Create: If approved, call POST /transfer/create with authorization_id, access_token, account_id, and description only.
The hr-plaid-transfer-create edge function uses the shared plaid-client.ts (createTransferAuthorization, createTransfer) and passes an idempotency key per payment item (e.g. hr-pay-{batchId}-{itemId}) to avoid duplicate transfers on retries. ACH description is limited to 10 characters (e.g. PAY + batch number suffix).

Data Flow


Webhook Events

Supported Events

Signature Verification

Header: Plaid-Verification The webhook body is signed using JWT. Verification requires:
  1. Fetch Plaid’s verification key
  2. Verify JWT signature
  3. Validate claims (iat, request_body_sha256)

Database Schema

fa_bank_accounts (Additional Columns)

fa_bank_statement_lines (Additional Column)

fa_module_settings (Additional Columns)


Security Considerations

Token Storage

  • Access tokens stored in plaid_access_token column (encrypted at rest by Supabase)
  • Tokens never exposed to frontend
  • Edge functions access tokens via service role

Multi-Tenancy

  • All Edge Function queries include organization_id filter
  • RLS policies prevent cross-tenant access
  • Audit logging for all Plaid operations

Webhook Security

  • JWT signature verification
  • Request body hash validation
  • Rate limiting via Supabase

Institution-specific behavior (Duplicate Items)

For Chase, PNC, Navy Federal Credit Union, and Charles Schwab, linking the same credentials again (creating a second Plaid Item for the same institution) can invalidate the existing Item at the time the user completes the institution’s OAuth flow. When this occurs:
  • Option 1: Use Plaid’s update mode to repair the existing Item (re-launch Link with the existing access_token). That will invalidate the new Item; then remove the new Item via /item/remove if it was already created.
  • Option 2: Remove the old Item using Plaid’s /item/remove endpoint and use the new Item.
Encore OS prevents duplicate Items per organization by checking plaid_institution_id before exchanging the public token (see duplicate prevention in implementation). This runbook is for awareness when handling support cases or when adding Item-level health handling (e.g. webhooks) that may replace an invalidated Item.

Error Handling

Plaid API Errors

Retry Strategy


Logging

Allowlisted Fields (Safe to Log)

  • bank_account_id
  • plaid_item_id (not access token)
  • plaid_account_id
  • transactions_synced
  • sync_duration_ms

Prohibited (Never Log)

  • plaid_access_token
  • plaid_secret
  • Full account numbers

Testing Strategy

Sandbox Testing

  • Use Plaid sandbox environment
  • Test institutions: ins_109508 (Chase), ins_109511 (Bank of America)
  • Test credentials: user_good / any password

Integration Tests

  • Link token creation
  • Token exchange
  • Transaction sync with deduplication
  • Webhook signature verification

E2E Tests

  • Full connect → sync → disconnect flow

Rollback Strategy

All Plaid columns are nullable. Rollback procedure:
  1. Disable feature flag: plaid_integration_enabled = false
  2. UI hides all Plaid components
  3. Manual CSV/OFX import continues to work
  4. No data migration required

Account selection and GL mapping

After Plaid Link succeeds, Encore OS uses a deferred exchange flow when onPlaidLinkSuccess is set:
  1. Link returns public_token and metadata.accounts (all accounts the user linked at the institution).
  2. The app shows PlaidAccountMappingDialog: user selects which accounts to connect and assigns a GL account to each.
  3. Only selected accounts are sent to plaid-exchange-token with gl_account_id per account.
  4. Backend creates fa_bank_accounts rows and triggers initial sync for each.
Backend contract: plaid-exchange-token accepts accounts[] with optional gl_account_id. If provided, that GL is used (and must be an asset, posting-allowed account in the same organization); otherwise the server falls back to default cash/asset lookup. Plaid docs: metadata.accounts in the Link onSuccess callback contains id, name, mask, type, subtype. Account selection in Link is controlled by the user inside Plaid’s UI; we do not filter which accounts appear in Link for initial link (optional: use account_filters when creating the link token to limit types/subtypes).

Recommendations (from Plaid docs and product)

Product initialization

We request Transactions and Balance when creating Link tokens. The Balance product is required for /accounts/balance/get and for populating fa_bank_accounts.balance; without it, Bank Summary and Cash Allocation widgets will show zero or omit the account. Existing Items created with only transactions do not have Balance—re-linking (or Plaid update mode with balance added) is needed to get balance. When we add Auth or Identity, consider optional_products (Auth) or required_if_supported_products (Identity) per Plaid’s Choosing when to initialize products.

Balance and cash position

When Plaid updates fa_bank_accounts.balance (in plaid-sync after a manual or post-exchange sync, or in plaid-webhook on SYNC_UPDATES_AVAILABLE), the same cash position flow as Teller is used: we invoke the fa-handle-bank-balance-updated edge function with organization_id, bank_account_id, old_balance, new_balance, and optional updated_by. That handler recalculates from all active bank accounts and upserts fa_cash_positions for the current date, so the Cash Allocation & Credit widget stays in sync with Plaid balance changes.

References

  • Spec: specs/fa/specs/FA-PLAID-INTEGRATION-SPEC.md
  • Review: specs/fa/reviews/FA-PLAID-INTEGRATION-REVIEW.md
  • Teller Integration (retired): docs/architecture/integrations/FA_TELLER_INTEGRATION.md
  • Plaid Sandbox verification: docs/testing/PLAID_SANDBOX_VERIFICATION.md (env, payroll $11.11, Transfer UI flow, optional script)
  • Plaid Docs: https://plaid.com/docs/
  • Plaid API Reference: https://plaid.com/docs/api/
  • react-plaid-link: https://github.com/plaid/react-plaid-link