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:
- ADR-001: Appwrite over Supabase and Firebase
- ADR-002: Dwolla over Stripe for ACH payments
- ADR-003: Separating bank linking from payment processing
Next up: the auth implementation with Appwrite, what it currently does and where it falls short.
