Stop me if this sounds familiar: You open your budgeting app, tap "Add Income," and it asks for one monthly amount. Just one. As if your financial life is a tidy little salary that lands on the 1st like a punctual train, rather than the chaotic improv comedy it actually is—freelance invoices that pay whenever they feel like it, bi-weekly contracting gigs, that one crypto dividend (we don't judge), and the occasional "oh right, I forgot I did that project" lump sum.
Most budget apps are built by people who've apparently never had to explain to a client why invoice #3 is late while standing in a grocery store wondering if they can afford the good cheese.
We fixed that. Welcome to Income Streams—or as I like to call it, "finally, a database schema that understands your side hustle addiction."
The Fantasy vs. The Reality
Here's the dirty secret of personal finance software: it's historically been designed by payroll employees for payroll employees. One check, same day every month, predictable as a metronome. But you're a modern human. You've got:
- A salary that hits on the 15th (except when holidays mess with it)
- Freelance work that pays bi-weekly when the client remembers to click "approve"
- Consulting that pays weekly because apparently, they like you that much
- Random one-time bonuses that you definitely deserve but can't predict
The old solution? Smash it all together into a "monthly average" and hope for the best. Which works great until Week 3 when you've spent your "average" but the next freelance payment is still 11 days away and you're eating instant noodles like it's freshman year again.
We decided to stop averaging and start listening.
The Architecture: Herding Financial Cats
Let's talk about how we actually modeled this chaos. Imagine each income source is a stream (creative, I know), and your month is a bucket. But here's the twist: the bucket changes shape depending on which stream is biggest.
The Multi-Stream Reality
Instead of forcing you to pick one payday, we let you define unlimited income streams, each with their own:
- Frequency: Monthly, weekly, bi-weekly, annually, or "one-time surprise"
- Payday: 1st, 15th, 28th—whenever the money actually hits
- Amount: Because not all gigs pay the same
Yes, that's a real database schema. We normalized your chaotic financial life into SQL-ish tables. You're welcome.
The "Primary Cycle" (Or: Democracy for Your Wallet)
Here's where it gets spicy. When you have multiple streams hitting on different days—say, salary on the 1st and freelance on the 15th—which month boundary do we use for budgeting? The 1st? The 15th? Do we just flip a coin?
We implemented what I call Payday Democracy. The system counts up all your recurring streams and picks the most common payday as the "Primary Cycle." It's like your income streams are voting on when the month actually starts.
So if you get paid on the 15th by your main job and the 15th by your side gig, congratulations—your month now runs from the 15th to the 14th. We don't care what the calendar says; we care when your money shows up.
Math That Doesn't Lie (Much)
Okay, let's address the elephant in the room: converting weekly and bi-weekly income into a monthly budget without lying to yourself.
Most apps use simple multiplication: "Weekly income × 4 = Monthly." Which is adorable, except there are 52 weeks in a year, not 48, so you're literally giving yourself a pay cut in your own budget.
We use the real numbers:
export function convertToMonthlyAmount(amount: number, frequency: IncomeFrequency): number {
switch (frequency) {
case 'weekly':
return amount * 4.33; // 52 weeks / 12 months = 4.333...
case 'bi-weekly':
return amount * 2.167; // 26 bi-weekly periods / 12 months
case 'annually':
return amount / 12;
// ... you get the idea
}
}Is it messier? Yes. Is it accurate? Also yes. We're building budgeting software, not a fantasy novel. Your future self will thank us when December rolls around and you actually have that extra "13th month" of weekly pay accounted for.
The "Wait, It Does What?" Feature
Now for my favorite part: the system is paranoid in a good way. When you record a payment (because yes, you can manually log when money actually hits), we don't just save it and call it a day. We compare it to your expected amount.
If your usual $3,000 freelance payment suddenly becomes $3,500, we don't assume you fat-fingered the keyboard. We assume you got a raise, negotiated better, or finally charged what you're worth (about time, honestly).
// Inside recordPaymentReceived...
const diff = Math.abs(amount - expectedAmount);
const diffPct = expectedAmount > 0 ? (diff / expectedAmount) * 100 : 0;
if ((diffPct > 5 || diff > 50) && amount !== incomeStream.amount) {
// Auto-update the stream amount
await ctx.db.patch(incomeStreamId, { amount, updatedAt: Date.now() });
// Log it as an income change for notifications
await ctx.db.insert('incomeChanges', { ... });
}Yes, we built a "raise detector." Because you shouldn't have to remember to update your budget when your income changes. The budget should notice and ask, "Hey, did you make more money? Can we celebrate? Should we update your safe-to-spend?"
One-Time Income: The Special Guests
What about those random windfalls? The tax refund, the "I sold my old bike on Facebook Marketplace" money, the crypto you forgot about that suddenly became worth something?
We don't bake these into your monthly budget because that would be insane. Instead, one-time income streams sit quietly until you record a payment. Then—and only then—they get added to that specific cycle's budget.
Think of it like this: your recurring income is your base salary, and one-time income is overtime. We don't assume you're working overtime every month (unless you're trying to burn out), but we definitely count it when it happens.
The Beautiful Math of "Safe-to-Spend"
All of this complexity boils down to one number you actually care about: how much can I spend right now without regretting it later?
Here's how we calculate it:
- Total Budget: Sum of all recurring income converted to your primary cycle, plus any one-time payments recorded in this cycle
- Carryover: Whatever you didn't spend last cycle (savings, basically)
- Effective Budget: Total + Carryover (this is your real money)
- Spending: What you've already blown on coffee and subscriptions
- Available: Effective - Spending
- Net Available: Available - Outstanding Loans (because debt is real)
It's not magic. It's just math that actually respects the complexity of your life instead of pretending you get one check on the first of every month like it's 1955.
Why This Actually Matters
Look, we could have built a simple "enter your monthly income" field and called it a day. It would have been easier. The code would have been shorter. I could have gone home early.
But that's not how you live. You live in the gig economy. You have a day job and a side hustle and occasionally you sell NFTs (no judgment). Your money doesn't arrive in a neat monthly package, so your budget shouldn't pretend it does.
By modeling each stream separately, converting frequencies accurately, detecting income changes automatically, and calculating cycles based on your actual paydays, we've built something that finally tells the truth about your money.
And the truth, as it turns out, is way more useful than a fantasy.
P.S. If you're wondering why we store firebaseId instead of just using Convex's auth—let's just say we learned the hard way that auth providers change, but your financial data living forever is non-negotiable. But that's a story for another blog post about database migrations and the tears of engineers.
Built with Convex, TypeScript, and a healthy disrespect for the "one paycheck per month" fantasy. Check out the implementation if you want to see how the sausage is made—just don't judge our utility functions too harshly, math is hard.