Best Boilerplates for Fintech Apps in 2026
TL;DR
There's no "fintech boilerplate" that handles compliance for you — regulations are your responsibility. The right approach: start with a solid SaaS boilerplate (Supastarter or Makerkit), add Plaid for bank connectivity, Stripe for payments, and Persona for KYC. Use APIs that hold the compliance burden — don't try to be a bank.
Fintech: Compliance Changes Everything
Fintech development differs from standard SaaS in one critical dimension: regulatory compliance. The features you're used to building (auth, subscriptions, email) are still here — but they're layered under a compliance framework that shapes every architectural decision.
Depending on what your product does, you may need:
| Regulation | Applies When | Who Handles It |
|---|---|---|
| PCI DSS | Storing/processing card data | Stripe (don't touch cards yourself) |
| SOC 2 Type II | Handling financial data for businesses | Your team + audit firm |
| Banking license | Moving money yourself | Use Stripe, Plaid, or Increase instead |
| KYC / AML | Onboarding financial service users | Persona, Stripe Identity, Onfido |
| GLBA | Handling consumer financial info (US) | Your infrastructure policies |
| PSD2 | Open banking in the EU | Plaid, TrueLayer, or similar |
| MSB license | Transmitting money in the US | Modern Treasury, Unit, or Synapse |
The default answer for fintech startups: Don't hold money. Don't hold card data. Don't transmit funds yourself. Use APIs that have the licenses and compliance burden — Stripe, Plaid, Unit, Increase. This lets you build a fintech product without needing a banking license or a compliance department.
Recommended Stacks by Product Type
| Product | Recommended Stack | Compliance API |
|---|---|---|
| Personal finance / budgeting | Supastarter + Plaid | Plaid handles bank connectivity |
| Payment marketplace | T3 Stack + Stripe Connect | Stripe handles KYC for recipients |
| Investment tracking | Next.js + Plaid Investments | Plaid |
| Expense management | Makerkit + Stripe Issuing | Stripe handles card issuance |
| B2B banking | Unit.co or Increase.com | Partner bank holds license |
| Lending | Blend, Lendio, or regulated partner | Complex — consult a lawyer |
| Crypto | Separate category, consult lawyer | FinCEN registration likely required |
Plaid Integration: Bank Connectivity
Plaid is the standard bank connectivity API — 12,000+ US financial institutions. It handles OAuth flows with banks so you never see usernames or passwords.
Setup
npm install plaid
# or
yarn add plaid
Server-Side Flow
import { Configuration, PlaidApi, PlaidEnvironments, Products, CountryCode } from 'plaid';
const configuration = new Configuration({
basePath: PlaidEnvironments[process.env.PLAID_ENV as 'sandbox' | 'production'],
baseOptions: {
headers: {
'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID,
'PLAID-SECRET': process.env.PLAID_SECRET,
},
},
});
const plaidClient = new PlaidApi(configuration);
// Step 1: Create link token (called when user clicks "Connect Bank")
export async function createLinkToken(userId: string) {
const response = await plaidClient.linkTokenCreate({
user: { client_user_id: userId },
client_name: 'Your App Name',
products: [Products.Transactions, Products.Auth],
country_codes: [CountryCode.Us],
language: 'en',
});
return response.data.link_token;
}
// Step 2: Exchange public token (after user authenticates with Plaid Link)
export async function exchangePublicToken(publicToken: string, userId: string) {
const response = await plaidClient.itemPublicTokenExchange({
public_token: publicToken,
});
// ALWAYS encrypt access tokens at rest
await db.bankConnection.create({
data: {
userId,
accessToken: encrypt(response.data.access_token), // Never store plaintext
itemId: response.data.item_id,
institutionId: '', // Fetch with /institutions/get
},
});
}
// Step 3: Fetch transactions
export async function getTransactions(userId: string, startDate: string, endDate: string) {
const connection = await db.bankConnection.findFirst({ where: { userId } });
if (!connection) throw new Error('No bank connected');
const response = await plaidClient.transactionsGet({
access_token: decrypt(connection.accessToken),
start_date: startDate,
end_date: endDate,
options: { count: 500, offset: 0 },
});
return response.data.transactions; // date, amount, merchant, category, account
}
Plaid Webhook Handling
Plaid sends webhooks for transaction updates, new accounts, and errors. Set up a webhook endpoint:
// app/api/webhooks/plaid/route.ts
export async function POST(req: Request) {
const body = await req.json();
switch (body.webhook_type) {
case 'TRANSACTIONS':
if (body.webhook_code === 'SYNC_UPDATES_AVAILABLE') {
// Queue a job to sync new transactions
await inngest.send({
name: 'plaid/sync-transactions',
data: { itemId: body.item_id },
});
}
break;
case 'ITEM':
if (body.webhook_code === 'ERROR') {
// Bank connection broken — notify user to re-connect
await notifyUserToReconnect(body.item_id);
}
break;
}
return Response.json({ received: true });
}
Plaid pricing:
- Sandbox: free (unlimited)
- Development: free (100 Items)
- Production: $0.30-0.50/connected account/month + per-product fees (Transactions: $0.10/request)
Budget ~$1-3/connected user/month for a typical personal finance app.
The Double-Entry Ledger Pattern
Any fintech app that tracks, moves, or records money should use a proper ledger. Single-value balance columns are a trap — they don't give you audit trails, reconciliation, or the ability to replay history.
// Prisma schema for double-entry bookkeeping
model LedgerEntry {
id String @id @default(cuid())
accountId String
amount Decimal @db.Decimal(19, 4) // Never use Float for money
currency String @default("USD")
description String
referenceType String // 'stripe_payment' | 'bank_transfer' | 'fee' | 'adjustment'
referenceId String // External ID for idempotency
metadata Json?
createdAt DateTime @default(now())
account Account @relation(fields: [accountId], references: [id])
@@unique([referenceType, referenceId]) // Prevent duplicate processing
@@index([accountId, createdAt])
}
// Compute balance: never store as column, always compute
async function getBalance(accountId: string, currency = 'USD') {
const result = await db.ledgerEntry.aggregate({
where: { accountId, currency },
_sum: { amount: true },
});
return result._sum.amount ?? new Decimal(0);
}
// Record a transaction atomically
async function recordPayment(
fromAccountId: string,
toAccountId: string,
amount: Decimal,
stripePaymentIntentId: string
) {
await db.$transaction([
// Debit from account
db.ledgerEntry.create({
data: {
accountId: fromAccountId,
amount: amount.negated(),
description: 'Payment sent',
referenceType: 'stripe_payment',
referenceId: `debit_${stripePaymentIntentId}`,
},
}),
// Credit to account
db.ledgerEntry.create({
data: {
accountId: toAccountId,
amount,
description: 'Payment received',
referenceType: 'stripe_payment',
referenceId: `credit_${stripePaymentIntentId}`,
},
}),
]);
}
Why Decimal, not Float? IEEE 754 floating-point arithmetic has rounding errors. 0.1 + 0.2 = 0.30000000000000004 in JavaScript. Use PostgreSQL's DECIMAL/NUMERIC type (Prisma's Decimal) for all financial amounts.
KYC / AML: Don't Build It Yourself
Know Your Customer (KYC) and Anti-Money Laundering (AML) checks are legally required for many fintech products. The bar for compliant implementation is high.
APIs that handle KYC:
- Persona ($1.50-3/verification) — Best developer experience, configurable workflows
- Stripe Identity ($1.50/verification) — If you're already on Stripe
- Onfido ($1.50-5/verification) — International coverage, extensive ID types
- Jumio — Enterprise, higher cost, comprehensive compliance
// Persona KYC integration
import Persona from 'persona';
const personaClient = new Persona.Client({
apiKey: process.env.PERSONA_API_KEY!,
});
// Create an inquiry (verification session)
export async function startKYC(userId: string, email: string) {
const inquiry = await personaClient.inquiries.create({
inquiryTemplateId: process.env.PERSONA_TEMPLATE_ID!,
referenceId: userId,
fields: {
'email-address': email,
},
});
// Redirect user to Persona hosted flow
return inquiry.attributes['inquiry-token'];
}
// Webhook: receive verification result
export async function POST(req: Request) {
const event = await req.json();
if (event.data.type === 'inquiry.completed') {
const inquiry = event.data.attributes;
const userId = inquiry['reference-id'];
const status = inquiry.status; // 'approved' | 'declined' | 'needs_review'
await db.user.update({
where: { id: userId },
data: {
kycStatus: status,
kycCompletedAt: new Date(),
kycInquiryId: inquiry.id,
},
});
}
}
Stripe Connect for Payment Marketplaces
If your product routes payments between multiple parties (marketplace, platform), Stripe Connect handles the KYC for each seller/recipient:
// Create connected account (Stripe handles KYC)
const account = await stripe.accounts.create({
type: 'express', // 'standard' | 'express' | 'custom'
email: seller.email,
capabilities: {
card_payments: { requested: true },
transfers: { requested: true },
},
});
// Generate onboarding link
const accountLink = await stripe.accountLinks.create({
account: account.id,
refresh_url: `${BASE_URL}/onboarding/refresh`,
return_url: `${BASE_URL}/onboarding/complete`,
type: 'account_onboarding',
});
// Create a payment with automatic transfer to connected account
const paymentIntent = await stripe.paymentIntents.create({
amount: 10000, // $100.00
currency: 'usd',
transfer_data: {
destination: connectedAccountId, // Seller receives payout
amount: 9000, // $90 (platform keeps 10%)
},
application_fee_amount: 1000, // Your platform fee: $10
});
Stripe handles the bank account verification, tax form collection (W-9/W-8), and payout scheduling for your connected accounts. This is the fastest path to a compliant payment marketplace.
Security Requirements for Fintech
These are non-negotiable:
Data encryption:
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
const ALGORITHM = 'aes-256-gcm';
const KEY = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex'); // 32 bytes
export function encrypt(text: string): string {
const iv = randomBytes(16);
const cipher = createCipheriv(ALGORITHM, KEY, iv);
const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted.toString('hex')}`;
}
export function decrypt(encryptedText: string): string {
const [ivHex, tagHex, encHex] = encryptedText.split(':');
const iv = Buffer.from(ivHex, 'hex');
const tag = Buffer.from(tagHex, 'hex');
const encrypted = Buffer.from(encHex, 'hex');
const decipher = createDecipheriv(ALGORITHM, KEY, iv);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
}
Audit log every financial operation. Every balance change, payment, transfer, or configuration update must be logged with: who did it, when, from what IP, and what changed.
Separate your financial database from your application database. This allows different backup schedules, access controls, and audit scopes.
Rate limiting on financial endpoints. Payment initiation, bank linking, and balance queries should all be rate-limited — both to protect your costs (Plaid charges per call) and to prevent abuse.
Compliance Checklist Before Launch
Before accepting real money, verify:
- PCI DSS: No card data touches your servers (use Stripe Elements or Checkout)
- KYC: Verified identity for any user moving >$3,000 or per your legal requirements
- AML: Sanctions screening (Plaid, Stripe, or dedicated API)
- Data encryption: All financial data encrypted at rest and in transit
- Audit log: Complete trail of all financial operations
- Fraud monitoring: Velocity checks, suspicious pattern detection
- Terms of service: Clear language about fund usage, fees, and liability
- Privacy policy: How you handle financial data (GLBA if US)
- SOC 2 roadmap: Required for B2B sales to financial institutions
How to Evaluate a Fintech Boilerplate Foundation
No off-the-shelf boilerplate is designed specifically for fintech — the compliance requirements are too product-specific. What you're evaluating is how well a general SaaS boilerplate can be extended with fintech-specific APIs and security requirements. Key evaluation criteria:
Row Level Security. Any fintech product that stores financial data per user requires database-level access control, not just application-level filtering. Supabase's Row Level Security policies enforce that user A can never read user B's transaction data, even if there's a bug in the application query. Evaluate Supastarter (Supabase-based) or T3 Stack with Neon's RLS features for this property.
Audit log completeness. Financial applications require immutable audit logs. Look for a boilerplate where audit events are: timestamped with server time (not client time), include the authenticated user ID and session, capture the IP address, and are written to a separate table that application code can't modify (append-only via database policy). If a boilerplate's audit implementation allows DELETE from the audit log table, it's not compliance-ready.
Environment variable handling. Fintech products require separate key management for multiple sensitive services: Plaid (sandbox and production keys), Stripe (test and live keys), encryption keys for access tokens, and database credentials. A boilerplate's environment variable structure should cleanly separate development and production values without risk of mixing them. Check for explicit validation of required environment variables at startup — failing loudly on missing keys is better than running in an unexpected state.
Database decimal precision. As covered in the ledger section: financial amounts must use DECIMAL/NUMERIC database types with explicit precision, not FLOAT or DOUBLE. Evaluate whether the boilerplate's ORM configuration makes it easy to use Decimal types throughout the codebase. Prisma's @db.Decimal(19, 4) attribute and Drizzle's numeric column type both support this. A boilerplate that uses JavaScript number types for amounts will cause subtle rounding bugs in production.
What Fintech Products Built on SaaS Boilerplates Have in Common
Fintech products that successfully launched on general SaaS foundations share a few structural decisions:
They chose their compliance API early and built around it. The products that moved fastest chose Stripe for payment handling and avoided touching card data. The products that needed bank connectivity used Plaid from day one rather than building their own bank API integrations. The compliance infrastructure came from APIs that hold the compliance burden.
They treated encryption as infrastructure, not an afterthought. Plaid access tokens, bank account numbers, and any PII with financial context are encrypted at rest using application-level encryption (AES-256-GCM) before database storage — not relying on database-at-rest encryption alone. This encryption implementation is written once, tested thoroughly, and used consistently rather than added ad-hoc when someone remembers to encrypt a new field.
They implemented the double-entry ledger from day one, even for simple products. Products that start with a balance column and migrate to a ledger later spend 2-3 weeks on the migration without adding any user-visible functionality. Starting with the ledger pattern — even when it feels like overengineering for an MVP — prevents an expensive refactor at the worst possible time (when you have paying customers and financial data that must be migrated without errors).
Database Choice for Fintech: PostgreSQL vs. Alternatives
PostgreSQL is the default database for fintech applications on SaaS boilerplates, and for good reason. It supports DECIMAL/NUMERIC types with exact precision, ACID transactions for double-entry ledger entries, row-level security for multi-user data isolation, and a mature ecosystem of extensions including pgvector (for AI features) and pg_cron (for scheduled billing jobs). Most boilerplates — Supastarter, Makerkit, T3 Stack — target PostgreSQL.
The specific hosting choice matters more for fintech than for standard SaaS:
Neon (serverless PostgreSQL) works well for early-stage fintech products. Branching lets you test database migrations against a copy of production data before running them on live financial records. The serverless pricing model scales to zero during development. Downside: branching and serverless cold starts can cause latency on financial endpoints that need consistent response times.
Supabase is the better choice for fintech products with complex multi-tenant data isolation requirements. Supabase's Row Level Security UI makes it easier to write, test, and audit RLS policies. The built-in audit logging hooks are useful for compliance documentation. Supastarter uses Supabase by default and is the practical choice if RLS-based isolation is a compliance requirement.
PlanetScale (MySQL-compatible) lacks native DECIMAL precision guarantees equivalent to PostgreSQL's NUMERIC type and doesn't support partial indexes or RLS natively. Avoid for fintech products where financial precision and data isolation are requirements.
Fraud Prevention Without Building Your Own System
Fraud is a production problem, not a launch problem — but the architecture for handling it needs to be in the codebase from the beginning. Two approaches:
Use Stripe Radar. If payments flow through Stripe, Stripe Radar provides machine-learning-based fraud detection with zero configuration. Custom rules let you add product-specific signals: block all payments from VPNs, require 3D Secure for high-value transactions, flag accounts that create multiple payment methods in 24 hours. For most fintech products built on Stripe, Radar covers the fraud surface area without additional infrastructure.
Use a dedicated fraud API for non-payment signals. For fintech products with login fraud, account takeover, or identity fraud vectors (in addition to payment fraud), Sift or Sardine provide risk scores for user actions beyond payments. Integrate at account creation, login from a new device, and any action that initiates a financial operation. These APIs return a risk score that you use to trigger additional verification (step-up auth) rather than blocking the action outright.
The key architectural principle: log enough data to reconstruct what happened in any fraud investigation. IP addresses, device fingerprints, user agent strings, and timestamps for every financial action — stored in the audit log, separate from the application database, append-only.
For the base SaaS infrastructure layer that fintech products are built on, see the best SaaS boilerplates guide. For multi-tenant SaaS patterns specifically (B2B fintech often has org-level data isolation requirements), the best multi-tenant SaaS boilerplates guide covers the options with the strongest data isolation guarantees. For the open-source foundations that eliminate license costs while providing production-grade infrastructure, see the free open-source SaaS boilerplates. For the marketplace payment patterns that overlap with fintech when you're building a two-sided financial platform, the best boilerplates for marketplace apps guide covers Stripe Connect in depth.
For more on the base SaaS infrastructure, see the best SaaS boilerplates for 2026 and the guide to Stripe integration in boilerplates.
Financial applications have higher compliance and security requirements than typical SaaS products. Any boilerplate used as a foundation for fintech should be evaluated for PCI DSS compliance readiness, audit log support, and the security practices of its authentication implementation.
Disclaimer: This is developer guidance, not legal advice. Consult a fintech attorney before launching any product that handles financial transactions.
Every fintech SaaS product should treat security review as a continuous process, not a one-time checklist. Any boilerplate chosen as a foundation must be evaluated for its security update cadence, not just its initial feature set.