Building Fortify: Why I Picked This Stack

Choosing a tech stack for your average web app is a solved problem. You have your favorites, the trade-offs are well-known, and the path is clear. But fintech is a different beast.


Cover Image for Building Fortify: Why I Picked This Stack

Fortify's stack, Appwrite, Plaid, Dwolla, came from a tutorial. I want to be upfront about that. What I don't like is using a tool without knowing why it's the right one, so after the fact I went looking for the reasoning behind each choice. Some held up immediately. Some required more digging. All of them taught me something about how fintech infrastructure actually fits together.

The Framework: Next.js

This one needs the least justification. Next.js with the App Router is the dominant full-stack React choice right now, and it fits a fintech project specifically because of the boundary it draws between server and client code.

In a banking application that distinction is a security decision, not just a performance one. Sensitive operations, fetching balances, validating sessions, calling Plaid and Dwolla, happen on the server. API keys stay out of the browser. Response data gets shaped before it reaches the client. Next.js Server Components make that the default path rather than something you have to architect around.

The Backend: Appwrite

Appwrite was in the tutorial. After looking at the alternatives, the reasoning held up.

The realistic options were Supabase, Firebase, and a custom stack built with Clerk, Prisma, and a hosted database. Firebase was easy to rule out: its NoSQL model gets unwieldy for relational financial data, and locking user account data to Google infrastructure wasn't a dependency I wanted to take on.

Supabase was the harder call. It has a larger community than Appwrite, stronger Next.js ecosystem support, and a PostgreSQL foundation that would give significantly more power for complex financial queries. If I were starting today, Supabase would be a serious contender.

Appwrite won for one specific reason: it's self-hostable. In a financial context, keeping user identity and account data on infrastructure you control matters, not as a hard requirement at the sandbox stage, but as the right philosophy for a banking application. The caveat is that Appwrite's document model may become a limiting factor as transaction data grows more complex. That's a known trade-off worth watching.

The Fintech Service Layer: Narrower Than Expected

Before getting into specific service choices, it's worth understanding how narrow the developer-accessible options actually are for bank-to-bank transfers. The services most people associate with this kind of money movement, Zelle, Venmo, Cash App, aren't developer platforms.

Zelle is a real-time payment network operated by a consortium of major US banks. Accessing it requires a direct institutional partnership with Early Warning Services, the entity running the network. That's the same category as a banking license. Venmo and Cash App are closed consumer products with no public API. The realistic developer-facing choices are Dwolla, Stripe ACH, and a small number of newer infrastructure providers like Moov.io or Modern Treasury.

Bank Linking: Plaid

Plaid was in the tutorial. This one held up without much investigation, Plaid is the industry standard for bank account linking, with the largest institution coverage and a sandbox that accurately mirrors production behavior. The alternatives exist but nothing offered a compelling reason to switch.

What matters more than the choice itself is understanding what Plaid actually does, and what it doesn't. Plaid handles the OAuth handshake between a user and their financial institution. It retrieves account data and surfaces balances and transaction history. It doesn't move money. That separation drives every decision downstream.

Payments: Dwolla

Dwolla was in the tutorial. Everyone defaults to Stripe. I almost questioned this one too, until I looked at what Dwolla actually is versus what Stripe actually is.

Stripe is card-first. ACH is a secondary capability built onto card rails. Dwolla is built from the ground up for ACH bank-to-bank transfers. Its data model, customers, funding sources, transfers, fits a banking application directly rather than being adapted from a card-payment framework.

For Fortify, every transfer is the product, not a secondary feature. That's what makes the tutorial's choice hold up. The trade-off is real: Dwolla's developer community is significantly smaller than Stripe's, the setup is more involved, and debugging means leaning on official documentation. But card rails with ACH bolted on isn't the right foundation for an application where bank-to-bank movement is the entire point.

How Plaid and Dwolla Work Together

The most important thing to understand about the stack isn't either service individually, it's how they're designed to work together and why they stay strictly separated.

Plaid verifies and links the bank account. Dwolla processes the transfer using those verified credentials. The handoff between them is the key integration point in the application:

User initiates bank link
  → Plaid OAuth flow, user authenticates directly with their bank
  → Plaid returns verified account credentials
  → Credentials passed to Dwolla as a verified funding source
  → Dwolla processes ACH transfers using that funding source

Plaid never touches money movement. Dwolla never handles bank authentication. Failures stay isolated. This is the pattern real fintech products use, and it's what makes building on this stack genuinely instructive rather than just functional.

Testing: A Consequence of the Service Layer

Three external services in the stack means any meaningful test coverage requires mocking those boundaries without hitting live APIs. That's what led to Mock Service Worker over jest.mock(). MSW intercepts at the network level rather than the module level, the actual fetch call is made and intercepted rather than the underlying module being replaced. For a fintech app, where the realism of API mocking matters, that difference counts. Post 8 in this series covers the full testing strategy.

The Full Stack

Layer Technology Why
Framework Next.js (App Router) Server-first by default
Language TypeScript End-to-end type safety
Auth & Database Appwrite Self-hostable BaaS, clean SDK
Bank Linking Plaid Industry standard, largest institution coverage
Payments Dwolla Purpose-built for ACH
Validation Zod Runtime schema validation at every data boundary
Testing Jest + RTL + MSW Realistic API mocking without live credentials
Monitoring Sentry Error tracking across client, server, and edge

The deeper reasoning behind the three biggest choices lives in the Architecture Decision Records in the repository:

Next up: the auth implementation with Appwrite, what it currently does and where it falls short.

Next up

Next up: the auth implementation with Appwrite, what it currently does and where it falls short.


This post is evidence for the Fortify Bank case study →

Written with AI assistance.