Rubi

Sole Founder & Engineer
In development
API complete · Mobile in progress

Personal finance app built for intentional tracking. No bank sync, no automatic categorization. You record, the app gives you structure.

Rubi is a personal finance app I've been building solo (backend and mobile client) for people who want to actually track their money rather than have a dashboard passively import it. No bank sync. No automatic categorization. You record what you spend, save toward goals you name, and the app gives you structure and real-time reporting around what you enter.

The premise is that intentional recording is itself the habit. Rubi is the infrastructure for that habit. The backend is Spring Boot with PostgreSQL, deployed on Railway. The mobile client is Expo/React Native. I've been building both simultaneously.

Rubi home screen or transaction list

Auth That Works on Both Platforms

The backend serves two different client types (a web app and a React Native app) and the ideal token delivery mechanism is different for each.

For web, JWTs in HTTP-only cookies are the right call. The cookie is invisible to JavaScript, which closes the XSS attack surface entirely. For React Native, HTTP-only cookies don't behave like they do in a browser. The standard approach is receiving the token in the response body and storing it in the device's secure storage.

Rather than two separate auth endpoints with diverging behavior, the backend issues both simultaneously: cookie and response body. Each client uses whichever it can. A single auth implementation covers both.

The second problem was Google OAuth. If someone has an existing account with their Gmail address as a local email/password login, and later tries “Sign in with Google” with the same address, a naive implementation either creates a duplicate or silently merges them, both wrong. The system explicitly rejects Google OAuth attempts where the email already exists as a local account, surfacing a clear error that directs them to use their password.

Rubi login or onboarding screen

Exchange Rate Caching

Rubi supports multi-currency accounts. That means exchange rates need to be accurate and available without hammering an external API on every conversion.

The system uses a three-tier fallback. First, it checks a database cache: rates are fetched nightly and stored with a seven-day TTL. If the cache is stale or missing for a currency pair, it calls the external API directly and writes the result back. If the external API is unavailable, it returns the most recent rate it has, regardless of age. A stale rate is better than a hard error on a transaction.

This approach reduced external API calls by over 90% in testing compared to on-demand fetching.

Rubi multi-currency account or transaction screen

Transfers and Balance Integrity

A transfer between accounts creates two linked records: a debit on the source side and a credit on the destination, sharing an identifier that connects them. For same-currency transfers the amounts are equal. For cross-currency, the destination amount is calculated from either the user-provided rate or the system's cached market rate.

Both sides write in a single transaction: either the transfer completes fully or neither record is created. Before any debit, the system validates that the balance is sufficient and surfaces a typed insufficient-balance error if not, caught at the application layer before it reaches the database, so the error reaching the mobile client is specific and actionable rather than generic.

Dashboard Aggregation Cache

The home dashboard aggregates net worth, spending by category, and account balances across all currencies, an expensive operation to run on every load. I built an in-memory cache with a short TTL that invalidates automatically on any mutation. The result is fast dashboard loads with data that's never more than a couple minutes stale, without the complexity of a distributed cache for a single-user personal app.

Rubi dashboard or reports screen

0
REST endpoints across accounts, transactions, budgets, goals
0
reduction in external FX API calls via three-tier cache
0
Flyway migrations, 16 JPA entities
0
client types, one auth implementation

Tech Stack

Java 21Spring Boot 4Spring SecurityJWTOAuth2PostgreSQLJPAFlywayBucket4jReact NativeExpoTanStack QueryZustandexpo-secure-store
Lagos, Nigeria