
Technologies Used
Project Overview
BankRead is a SaaS application built for accountants and bookkeepers who process thousands of bank statement pages every month. Instead of manually entering transactions from PDF statements into spreadsheets, users upload their PDFs and the AI extracts every transaction automatically — categorized, totaled, and ready for export.
The platform processes bank statements from all major Canadian and international banks, handling various PDF formats and layouts through Anthropic's Claude API for intelligent document parsing.
Architecture
Frontend — Next.js 15 + NextAuth.js v5
The frontend is a Next.js 15 application deployed on Vercel with server-side rendering and NextAuth.js v5 for authentication. It communicates with the backend via a shared JWT secret, providing seamless session management across the stack.
Key frontend features include a drag-and-drop multi-file upload interface with real-time processing progress, live page counting, color-coded multi-statement views, and one-click CSV/Excel export.
// NextAuth.js v5 configuration with shared JWT
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Credentials({
async authorize(credentials) {
const res = await fetch(`${BACKEND_URL}/api/v1/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(credentials),
});
if (!res.ok) return null;
return res.json();
},
}),
],
session: { strategy: "jwt" },
});
Backend — FastAPI + SQLModel + PostgreSQL
The backend is a Python FastAPI application using SQLModel (SQLAlchemy + Pydantic) with async PostgreSQL via asyncpg. It handles PDF processing, AI-powered transaction extraction, user management, billing, and audit logging.
Database migrations are managed with Alembic, and the entire backend runs in Docker with Uvicorn workers behind a Caddy reverse proxy with automatic HTTPS.
# FastAPI endpoint for PDF upload and AI extraction
@router.post("/upload")
async def upload_statements(
files: list[UploadFile],
current_user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
org = await get_org(current_user.org_id, session)
if org.month_pages >= org.page_limit:
raise HTTPException(status_code=402, detail="Page limit reached")
results = []
for file in files:
pdf_bytes = await file.read()
pages = extract_pages(pdf_bytes)
for page in pages:
transactions = await extract_with_ai(page, categories)
results.extend(transactions)
await update_usage(org, len(pages), session)
return {"transactions": results}
AI Transaction Extraction
Each uploaded PDF page is sent to Anthropic's Claude API for structured extraction. The AI identifies transaction dates, descriptions, amounts, and running balances, then categorizes each transaction into configurable categories like Groceries, Utilities, Rent, Income, and more.
# AI-powered transaction extraction with custom categories
async def extract_with_ai(page_image: bytes, categories: list[str]):
response = await anthropic.messages.create(
model="claude-sonnet-4-5-20250929",
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64", "data": b64encode(page_image)}},
{"type": "text", "text": f"Extract all transactions. Categorize into: {categories}"},
],
}],
)
return parse_transactions(response)
Stripe Billing Architecture
The billing system supports dual Stripe modes (test and live) controlled by a single environment variable. All Stripe credentials exist in both test and live variants, with config properties that auto-resolve based on the active mode — enabling seamless switching without code changes.
# Dual Stripe mode configuration
class Settings(BaseSettings):
stripe_mode: str = "test" # "test" or "live"
stripe_secret_key_test: str = ""
stripe_secret_key_live: str = ""
@property
def stripe_secret_key(self) -> str:
if self.stripe_mode == "live":
return self.stripe_secret_key_live
return self.stripe_secret_key_test
Subscription lifecycle is fully automated: Stripe Checkout creates subscriptions, webhooks sync plan changes in real-time, the Customer Portal handles upgrades and cancellations, and monthly usage counters reset automatically with each billing period.
Key Features
- Multi-statement upload — Drag-and-drop multiple PDFs, combine months of data into a single exportable view with color-coded sources
- AI categorization — Automatic transaction categorization with custom category support and keyword rules
- CSV & Excel export — One-click export of parsed transactions, ready for accounting software
- Four-tier billing — Free (10 pages), Starter CA$49 (400 pages), Pro CA$199 (2,000 pages), Business CA$499 (10,000 pages)
- Two-factor authentication — Optional TOTP-based 2FA with QR code setup via authenticator apps
- Real-time usage tracking — Per-organization monthly page limits with live counters and automatic billing-period resets
- Privacy by design — Files are processed in memory and never stored, encrypted connections throughout
- Password reset — Email-based password reset flow via Resend API
- SEO-optimized landing page — JSON-LD structured data, pricing section, FAQ, and contact form
Infrastructure & Deployment
- Frontend: Vercel with auto-deploy on push to main
- Backend: Hetzner VPS running Docker Compose (FastAPI + PostgreSQL + Caddy)
- DNS & CDN: Cloudflare routing to Vercel (frontend) and Hetzner (backend API)
- HTTPS: Caddy with automatic Let's Encrypt certificate management
- Database: PostgreSQL 16 with Alembic migrations and async connection pooling
- Monitoring: Structured logging, audit trail, per-user usage analytics
Docker Compose Production Stack
# docker-compose.prod.yml
services:
caddy:
image: caddy:2-alpine
ports: ["80:80", "443:443"]
volumes: ["./Caddyfile:/etc/caddy/Caddyfile"]
postgres:
image: postgres:16-alpine
volumes: ["pgdata:/var/lib/postgresql/data"]
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
backend:
build: ./backend
environment:
STRIPE_MODE: ${STRIPE_MODE:-live}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
command: ["uvicorn", "app.main:app", "--workers", "4"]
depends_on:
postgres: { condition: service_healthy }
Technologies Used
Next.js 15, React 19, TypeScript, Tailwind CSS, NextAuth.js v5, FastAPI, Python, SQLModel, PostgreSQL, Alembic, Anthropic Claude API, Stripe, Docker, Caddy, Vercel, Hetzner, Cloudflare, Resend