Ledger is a personal finance app built to production-grade standards. Not to compete with Mint, but to demonstrate how I think about systems.
The feature set is a vehicle. The architectural decisions are the point.
Mint is dead. The space is crowded. That's not the point. Personal finance naturally justifies real architecture. Bank connectivity requires compliance thinking, budgeting requires domain modelling, and multi-account aggregation requires event-driven design. The domain earns every pattern in the codebase. A todo app wouldn't.
If real users show up, the foundation is ready. Plaid is real. The infrastructure is production-grade. Compliance and cost would be the conversation at that point, not the architecture.
The pivots, the migrations, and the honest accounting of what was tried before the current approach.
The project ran through a NestJS phase. The wins were real but narrow. The overhead was not.
MigrationWhy the project moved from tRPC to server actions, and what that decision actually cost.
ArchitectureNot every event belongs to an aggregate. Recognising the difference kept the domain honest.
MigrationThe project started in Nuxt 3. Here is what moved, what did not, and why the switch happened.
State ManagementThe server owns the data. The client caches it. Mutations invalidate. No store, no reducers, no sync.
InfrastructureHaving used New Relic on frontend apps, the choice to go with Grafana was deliberate, not unfamiliar.
Every pattern here has a rationale. These are the answers to the “why” questions.
The layered model and one-way dependency rules from FSD, without the full specification overhead.
Transport layerComposable middleware, schema validation, and a single catch boundary via next-safe-action.
System designService boundaries drawn too early are a bet on requirements you don't have yet.
Domain layerA finance domain has real invariants worth modelling explicitly. A todo app wouldn't justify this.
Application layerCommands and queries are separate concerns. The bus makes dispatch type-safe without boilerplate.
System designA separate read model is justified. A separate database is not, at least not yet.
InfrastructureDomain events decouple what happened from what happens next. The interface hides the delivery mechanism.
InfrastructureEvery event hits Postgres before any handler runs. QStash handles async delivery.
InfrastructureRegistration order is implicit coupling. It works because processing is sequential. An explicit event chain would make the dependency visible.
SecurityShort-lived JWTs carry only userId. A type claim separates access tokens from MFA challenge tokens without multiple signing keys.
SecurityTwo-step login with challenge tokens, aggregate-owned events, and service-layer signing. No auth logic in handlers.
InfrastructureTier-gated, cache-aside, zero vendor. A database table and a Redis cache replace a SaaS subscription.
InfrastructureOpen standards for instrumentation. The backend is a config swap, not a rewrite.
InfrastructureA shallow probe keeps the orchestrator happy. A scheduled deep check catches silent dependency failures before users do.