Solo full-stack project · 2019–2020 · Angular · Firebase · ML

Makaw — personal finance forecasting

An Angular + Firebase web app to track personal finances and forecast future savings, with a Random Forest model on Google Cloud predicting how each life factor moves the needle.

Makaw — personal finance forecasting

Makaw is a personal-finance project I built across 2019–2020. It started from a question most people ask themselves at some point: how much will I be able to save in a year? And if I want to save a specific amount, how much can I afford to spend?

The existing apps each answered half of it. Bank apps and Fintonic are great at showing your current situation; Monefy is great at fast manual logging. None of them forecast the future, told you a concrete saving goal, or let users talk finance with each other. Those three gaps are exactly what Makaw set out to fill — and as an engineer, the interesting part was the architecture underneath.

What you can do in the app #s1

You start by logging your income and expenses — including the periodic ones, like a salary or a monthly subscription. Where most apps stop at charting the past, Makaw projects those periodic movements forward and draws the curve of where your balance is actually heading.

Around that forecast sit the rest of the features: a machine-learning prediction of how much someone with your profile tends to save, a saving-goal calculator that turns a target-by-a-date into a concrete spend-per-month and spend-per-day, a financial score that ranks you against people like you, and a small social layer — a Twitter-style feed for personal-finance tips and real-time chat between users.

It's live at makaw-3a1bf.web.app and the full source is on GitHub — both linked at the bottom of this page. What follows is the engineering underneath, from the infrastructure up to the frontend.

Firebase as the entire backend infrastructure #s2

Rather than build and maintain a traditional backend, Makaw runs entirely on Firebase as a Backend-as-a-Service. Authentication handles sign-in (with OAuth providers available out of the box); Cloud Firestore — a NoSQL, real-time document database — is the heart of the system; Cloud Functions host the serverless endpoints; Cloud Storage keeps user-generated content; and Firebase Hosting serves the app with one-command deploys and SSL.

The database is modelled as collections of documents rather than tables and rows. Users, transactions, chats and posts each live in their own collection; a document is just a set of key-value pairs (a JSON object), and the schema is free to vary per document — exactly the flexibility NoSQL buys you when an entity's shape changes over time.

The same offline ML pipeline hangs off this backend: a Cloud Function bridges the Angular app to a Random Forest model served on Google Cloud AI Platform, which I cover further down.

Makaw infrastructure An Angular single-page app in the browser talks to Firebase (Authentication, Cloud Firestore, Cloud Storage and a predictSavings Cloud Function). The Cloud Function calls a Random Forest model named user_savings on Google Cloud AI Platform. Below, an offline pipeline generates a synthetic dataset in Java, trains the model in a Cloud Datalab Jupyter notebook with scikit-learn, serialises it to forestmodel.joblib and deploys it to AI Platform. Browser Angular SPA served by Firebase Hosting HTTPS realtime Firebase Backend-as-a-Service Authentication sign-in · OAuth Cloud Storage user content Cloud Firestore NoSQL · realtime documents users · transactions · chats · posts Cloud Functions predictSavings europe-west3 · CORS Google Cloud AI Platform ML Engine user_savings Random Forest regressor googleapis service account · IAM Model training offline pipeline data-generator Java synthetic dataset dataset.csv 7 features → savings Cloud Datalab Jupyter notebook scikit-learn forestmodel.joblib trained model deploy pull user data
Makaw's infrastructure: the Angular SPA in the browser talks over HTTPS to Firebase (Authentication, Cloud Firestore, Cloud Storage and the predictSavings Cloud Function in europe-west3); that function calls the user_savings Random Forest model on Google Cloud AI Platform. An offline pipeline below generates a synthetic dataset in Java, trains the model in a Cloud Datalab notebook with scikit-learn, and deploys forestmodel.joblib to AI Platform.

Real-time chat and user media on Firestore #s3

Chat is the feature where Firestore's real-time nature pays off most. Each conversation is a single document in a chats collection, and the document id is derived deterministically: take the two participants' uids, sort them alphabetically, and concatenate them. Because the same pair always produces the same id, there's no lookup table and no risk of two parallel conversations — the id is the address.

Firestore then hands that document back as an "infinite" observable: an array of message objects that re-emits in full every time anyone writes to it, so both participants' screens update live without polling. The flip side of an endless stream is that unsubscribing becomes your responsibility once a conversation is closed.

User media — profile images and video — lives in Cloud Storage, with secure uploads and downloads brokered by Firebase. The heavy bytes stay in Storage while only the lightweight reference (the download URL) is written into the Firestore document, the standard split that keeps documents small and reads cheap.

Real-time chat and media on Firestore Two users derive a deterministic conversation id by sorting their two uids alphabetically and concatenating them, so each pair maps to exactly one Firestore document under the chats collection with no lookup. The chats/{conversationId} document holds the messages; a message that carries a photo stores only a download URL that points to the bytes living in Cloud Storage. Firestore exposes the document as an "infinite" observable — an array of messages re-emitted on every write — which is pushed live to both subscribers. User A uid a7f1… User B uid c3d9… conversation id [uidA, uidB].sort().join('') deterministic — one document per pair chats/{conversationId} { from: a7f1, text: 'hola', ts } { from: c3d9, text: '¿ahorras?', ts } { from: a7f1, photoUrl, ts } Cloud Storage images · video secure up / download via Firebase url ref valueChanges() “infinite” observable Observable<Message[]> · re-emits on write pushed live to both subscribers
Two users derive a deterministic conversation id by sorting their uids and concatenating them, addressing exactly one chats/{conversationId} document. The document holds the messages; a message with a photo stores only a download URL pointing at the bytes in Cloud Storage. Firestore exposes the document as an "infinite" observable that re-emits the full message array on every write, pushed live to both subscribers.

Forecasting savings with machine learning #s4

The headline feature was the forecast. Historical-data forecasting was the easy half — project periodic transactions forward and render the future balance curve. The interesting half was US0009: predicting savings from a user's profile with a model.

I trained a Random Forest regressor (scikit-learn) in a Google Cloud Datalab notebook, with the whole pipeline kept in the repo's machine-learning directory. Single decision trees overfit; a forest of trees — here 1,200 trees, depth 5, each seeing a different slice of the data and averaging their outputs — generalises far better and, as a bonus, reports feature importances. Since the platform barely had real users, a small Java data-generator synthesised the dataset, one class per feature: studies, city, age, children, civil status, gender, and a deliberately random bank field as a control, to prove the model finds signal in the noise.

The result: mean absolute error of ~135 € with the model versus ~518 € using just the average — roughly 5× more accurate. The feature weights came out as city of residence 30%, level of studies 22%, age 19%, gender 15%, children 12%, and civil status and bank ~0% (the random control correctly weighed nothing). The trained forest was serialised to a forestmodel.joblib and handed off to Google Cloud for serving.

Makaw "Artificial Intelligence Predictions" modal: dropdowns for studies, city, age, number of children, civil status, gender and bank, with a Request button.
Makaw "Artificial Intelligence Predictions" modal: dropdowns for studies, city, age, number of children, civil status, gender and bank, with a Request button.

From notebook to production: the serverless bridge #s5

Getting the model out of the notebook was the real engineering challenge, and the repository splits that work into separate pieces. The trained Random Forest was uploaded to Google Cloud and published on AI Platform (ML Engine) as a versioned model named user_savings.

A thin Firebase Cloud Function — predictSavings, deployed to europe-west3 and living in the firebase-functions directory — is the bridge between the Angular app and that model. It handles CORS (including the OPTIONS preflight), authenticates to Google Cloud with a service account scoped to cloud-platform, forwards the request's feature instances to ml.projects.predict through the googleapis client, and streams the prediction back. Most of the pain here was IAM authorization, not the model itself.

A separate cloud-functions directory holds an experimental Admin-SDK script that I kept as a scaffold for a future server-side feature — its commit is honestly flagged "ignore this, it's just an example for the next feature." The financial score, by contrast, stayed client-side in Angular, with only a stubbed calculateScore left commented out for the day it might move to the backend.

A layered architecture around the facade pattern #s6

Before writing features I invested an early "take root" stage in the design that everything else would hang from. The app is split into three layers: a core layer (state + API access), an abstraction layer (the facades), and a presentation layer (the components).

Each domain — transactions, users, chat — exposes a facade: an injectable service that encapsulates what the components are allowed to see and do. Components never touch the API or the state directly; they only talk to the facade. That single indirection is what kept the codebase maintainable as it grew.

Makaw layered architecture around the facade pattern Three stacked layers. The presentation layer holds the smart Container component plus the dumb Table and Chart components. The abstraction layer holds the injectable TransactionsFacade, the single indirection every component must go through. The core layer holds the transactions state (a BehaviourSubject exposed as an Observable) and the transactions API (Firestore access). Components talk only to the facade; they never reach the core directly, shown by a crossed-out red path. Presentation layer components Container smart component subscribes · dispatches Table dumb component renders rows Chart components line · bar · pie each derives its own observable components talk only to the facade Abstraction layer the single indirection TransactionsFacade injectable · guards what components may see + do next() · observable fetch · persist Core layer state + API access Transactions state BehaviourSubject<Transaction[]> exposed only as Observable Transactions API Firestore access load · create · update no direct access
Three stacked layers. The presentation layer holds the smart Container plus the dumb Table and Chart (line/bar/pie) components. The abstraction layer holds the injectable TransactionsFacade — the single indirection every component must pass through. The core layer holds the transactions state (a BehaviourSubject exposed only as an Observable) and the transactions API (Firestore access). Components talk only to the facade; a crossed-out red path marks that they never reach the core directly.

Reactive state with RxJS and unidirectional flow #s7

State lives behind the facade as a BehaviourSubject — a transactions array that starts empty and is exposed to components only as an Observable. A smart component (the container) asks the facade to load data, subscribes to transactions$, and gets an immediate response thanks to the initial value; when the API later resolves, the facade calls next(transactions) and the subscription updates.

From there it's strictly unidirectional: the smart component passes the observable down to dumb components (the line, bar and pie charts) as @Input. Each child subscribes and derives its own internal observable, so its transformations never mutate the parent's stream. Changes flow down the tree, never up — edits travel back through @Output events that ask the facade to update state. This same shape was then reused verbatim for users and chat.

Makaw "Create transaction" modal over the dashboard table, toggling between income and expense with description, category, amount and date.
Makaw "Create transaction" modal over the dashboard table, toggling between income and expense with description, category, amount and date.

Score, saving goals, feed and chat #s8

On top of the forecast sat the social and goal-setting layer. The financial score compares your current savings against the average of users who share your profile (currentUserSavings / peopleAverageSavings): below one shows red with the percentage you fall short, above one shows green, and beyond 2× it switches from a percentage to an "X times the average" framing.

Saving goals let you pick a future date; the app computes the maximum you could save by then from your periodic transactions, with a slider — wired through RxJS by subscribing to both form inputs — that recalculates spend-per-month and spend-per-day live as you drag.

The feed is a Twitter-style board for personal-finance tips: a posts collection where each post carries nested comments and likes sub-collections, kept in sync by the same real-time Firestore observables — and happily embracing the data redundancy NoSQL trades for simpler, faster reads. Chat, covered above, is the real-time messaging layer between users.

Makaw dashboard cards: current savings 3,450 € with "you save 66% more than a person with the same characteristics", plus gradient action cards for adding transactions, showing future transactions and AI predictions.
Makaw dashboard cards: current savings 3,450 € with "you save 66% more than a person with the same characteristics", plus gradient action cards for adding transactions, showing future transactions and AI predictions.

What I took away #s9

Makaw was where I first felt the payoff of investing in architecture before features. The facade-plus-RxJS foundation made state management genuinely maintainable, and the component/service split meant the transaction-calculation library and reusable UI (table, navbar, cards) came along for free on every new screen.

It also taught me to estimate without prior experience, to document the work as I went (a disciplined Trello-and-git trail), and that the hardest part of shipping ML is rarely the model — it's the permissions and plumbing around deploying it. Years later, working with Angular professionally, I can see plenty I'd refactor now, which is its own kind of satisfying.

Stack

Angular TypeScript RxJS Firebase Cloud Firestore Python · scikit-learn

Links