Integrate Plaid for connecting bank accounts and syncing transactions in TypeScript applications using Bun.
Quick Start
bun add plaid
import { Configuration, PlaidApi, PlaidEnvironments } from 'plaid';
const plaidClient = new PlaidApi(new Configuration({
basePath: PlaidEnvironments[process.env.PLAID_ENV || 'sandbox'],
baseOptions: {
headers: {
'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID,
'PLAID-SECRET': process.env.PLAID_SECRET,
}
}
}));
Environment Setup
| sandbox
| Development/testing
| No
| No (test accounts)
| development
| Limited Production
| Yes for redirect, No for popup
| Yes (with limits)
| production
| Full production
| Yes
| Yes
Critical: Use popup mode (no redirect_uri) for local development to avoid HTTPS requirements:
// Popup mode - works with HTTP localhost
const linkConfig = {
user: { client_user_id: `user-${Date.now()}` },
client_name: 'My App',
products: ['transactions'],
country_codes: ['US'],
language: 'en',
// NO redirect_uri = popup mode
};
Authentication Flow
The Plaid Link flow has 3 steps:
-
Create Link Token (backend) → Returns temporary token for Link UI
-
User Authenticates (frontend) → Opens Plaid Link, user logs into bank
-
Exchange Tokens (backend) → Trade public_token for permanent access_token
See references/code-examples.md for complete implementation.
Key Concepts
-
Item: A bank connection (one per institution per user)
-
Access Token: Permanent credential for API calls (store securely)
-
Public Token: Temporary token from Link (exchange immediately)
-
Link Token: Short-lived token to initialize Link UI
Products
Common products to request:
| transactions
| Transaction history and real-time updates
| auth
| Account and routing numbers
| identity
| Account holder information
| investments
| Investment account data
| liabilities
| Loan and credit card data
Database Schema
For multi-account support, use SQLite with Bun's built-in driver:
import { Database } from "bun:sqlite";
db.run(`
CREATE TABLE IF NOT EXISTS items (
id TEXT PRIMARY KEY,
access_token TEXT NOT NULL,
institution_id TEXT,
institution_name TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
db.run(`
CREATE TABLE IF NOT EXISTS accounts (
id TEXT PRIMARY KEY,
item_id TEXT NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL,
subtype TEXT,
current_balance REAL,
FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE
)
`);
db.run(`
CREATE TABLE IF NOT EXISTS transactions (
id TEXT PRIMARY KEY,
account_id TEXT NOT NULL,
amount REAL NOT NULL,
date TEXT NOT NULL,
name TEXT NOT NULL,
merchant_name TEXT,
category TEXT,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
)
`);
Transaction Pagination
Plaid returns max 500 transactions per request. Always paginate:
let offset = 0;
const count = 500;
let hasMore = true;
while (hasMore) {
const response = await plaidClient.transactionsGet({
access_token,
start_date: '2023-01-01',
end_date: '2024-12-31',
options: { count, offset },
});
// Process response.data.transactions
offset += response.data.transactions.length;
hasMore = offset < response.data.total_transactions;
}
Common Errors
| INVALID_ACCESS_TOKEN
| Token expired or invalid
| Re-link the account
| ITEM_LOGIN_REQUIRED
| Bank requires re-authentication
| Use update mode Link
| INVALID_FIELD + "redirect_uri must use HTTPS"
| Using redirect in dev/prod
| Use popup mode or HTTPS
| PRODUCTS_NOT_SUPPORTED
| Institution doesn't support product
| Check institution capabilities
Documentation Links
Reference Files
-
references/code-examples.md- Complete implementation patterns -
references/api-reference.md- API endpoints and responses