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 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.
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.
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.
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.
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.
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.
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.