Inspiration
Every budgeting app I tried had the same blind spot: they tell you what you spent, never what you'll have. I'd look at $2,400 in checking, feel fine, and forget that rent, a car payment and two subscriptions were all landing before my next paycheck. The number on the screen was true and useless at the same time.
The second gap was personal. I follow Islamic finance rules — no riba (interest), zakat owed at 2.5% of net wealth held above the nisab threshold for a lunar year. Every mainstream app either ignores that entirely or, worse, gets it confidently wrong: I've seen "zakat calculators" bill it as a monthly charge on salary, which is not what zakat is.
So Mizan ("balance" / "scale" in Arabic) is the app I actually wanted: envelope budgeting on live bank data, a forward-looking balance projection instead of a backward-looking report, and an AI coach that knows both my real numbers and the rules I budget by.
What it does
Envelope budgeting on live bank data. Link a real bank through Plaid; transactions sync incrementally and auto-file into envelopes. Envelope "spent" is derived at runtime from this month's transactions rather than stored as a counter, so the numbers can never silently drift out of sync with the bank.
A running-balance cash-flow projection. The Cash Flow screen answers "what will my balance be on the 20th?" It anchors to your live Plaid cash minus card balances owed, then walks forward day by day: future paychecks land on their scheduled dates, due-day bills drop their remaining budget on their due date, un-dated everyday spending drips evenly across remaining days, and goal contributions leave on each payday. If any end-of-day balance dips below zero, the app raises an overspend risk flag before it happens. Nothing is stored — it's one pure function over your plan inputs.
An agentic AI coach built on the Claude API. Two modes:
- Monthly plan — one-shot advice grounded in a structured financial snapshot, returned as strict JSON (
status/headline/ optionalwarning/steps[]) so the UI can render it as real components with status colors instead of a wall of chat text. - Ask Mizan chat — a multi-turn agent with tool use. Claude can call
list_envelopes,query_transactions(search by merchant, category, envelope, date range, amount), andpropose_recategorize. Ask "how much did I spend at Amazon last month?" and it queries your real rows. Ask "move all my Chipotle purchases into Dining Out" and it finds the matching transactions and returns a proposal — the app renders a confirmation card, and nothing is written until you tap Confirm.
Faith-aware, by architecture not by prompt-hack. A rules layer feeds the model an Islamic-finance policy: zakat is 2.5% of net wealth (cash + goal savings − short-term debt) once above nisab for a full lunar year, never a monthly charge on income; riba is discouraged; haram spending categories get flagged. Adding another tradition is one new file implementing a FaithRules interface.
A savings personality. Aggressive / balanced / passive maps to a target savings rate (30% / 15% / 7%) and the tone the coach speaks in, so the same engine can be blunt with one user and gentle with another.
Six swipeable tabs: Home, Envelopes, Activity, Savings, Coach, Settings.
How I built it
Client — React Native 0.81.5 / Expo SDK 54 / TypeScript. File-based routing via expo-router, with the React Native New Architecture (Fabric + TurboModules) enabled. The tab bar is @react-navigation/material-top-tabs mounted through expo-router's withLayoutContext, so tabs are a native finger-tracked pager (react-native-pager-view) that snaps like the iOS home screen — file-based routing and native paging at once. Charts (bars / donut / Sankey) are hand-rolled in react-native-svg; there's no chart library in the dependency tree.
Backend — Supabase Postgres + Auth, with Row Level Security on every single table. 12 SQL migrations. Every table carries a user_id and an owner-only RLS policy for select/insert/update/delete, so authorization is enforced by the database rather than trusted to app code. That decision pays off directly in the AI layer (below).
Bank data — Plaid, running in Production. Five Deno Edge Functions (~950 lines) sit between the app and Plaid so credentials never touch the client: plaid-create-link-token, plaid-exchange-token, plaid-sync, plaid-remove, ai-advise.
plaid-sync is the load-bearing one. It uses /transactions/sync with a per-item cursor stored on aggregator_items for true incremental sync, refreshes account balances via /accounts/get on every run, and categorizes in priority order: user-defined merchant rules first, then Plaid's personal_finance_category mapped onto the user's envelope names by keyword — deliberately conservative, so income, transfers and loan payments never auto-file, and ambiguous categories land in an "Other" bucket instead of guessing wrong. Each sync also backfills previously-uncategorized rows, so "Sync now" actively drains Other.
AI — the Claude API (claude-haiku-4-5) behind an Edge Function proxy. The API key lives only as a Supabase secret. The interesting part is the agentic loop in ai-advise: up to five tool-use rounds, where each tool executor runs against the caller's own RLS-scoped Supabase client. The model literally cannot read another user's data — not because the prompt says so, but because the database connection it runs through is scoped to one user. Tools are strictly read-only; propose_recategorize computes a preview and returns it, and the mutation happens client-side only after explicit user confirmation. Spend is capped by an atomic Postgres RPC (bump_ai_usage) at 20 calls/day/user, which fails open on error so a metering hiccup never bricks the coach.
Testing — Jest + ts-jest, 54 tests across 7 suites, aimed at the pure logic where money bugs actually live: the cash-flow projection, pay-frequency math, budget-period boundaries, savings-rate and zakat calculation, category labelling, and the tolerant JSON plan parser.
Distribution. The app runs in Expo Go for development and ships to my phone as an unsigned IPA sideloaded through AltStore — a scripted expo prebuild → xcodebuild → repackage pipeline that puts a real, icon-branded app on a real iPhone without a $99/yr Apple Developer account.
~15,000 lines of TypeScript and SQL, built solo.
Challenges I ran into
Expo Go forbids native modules — and Plaid's SDK is native. react-native-plaid-link-sdk requires a custom dev client, which would have cost me Expo Go's instant-reload workflow. I routed around it with Plaid Hosted Link opened via expo-web-browser, plus a redirect URI that auto-closes the browser on completion and a server-side poll on plaid-exchange-token to catch OAuth flows that finish out-of-band. Real bank linking, zero native code.
One bank failing killed every bank. Multi-item sync was wrapped in a single try/catch, so the first bank to throw aborted the whole run — and a duplicate transaction_id that Plaid legitimately re-sends within a sync window was enough to trip it. I rebuilt the loop with per-item error isolation (catching all errors, not just typed PlaidErrors), in-batch deduplication, and a row-by-row insert fallback so one bad row can't drop a good batch. Failing items now get marked requires_reauth or error and self-heal on the next successful sync instead of needing a manual re-link.
Balances were frozen at link time. accounts.balance was written once when a bank was linked and never refreshed, so Home slowly drifted away from reality. Diagnosing it meant proving the sync was RLS-scoped to the signed-in user (no household merge), which also explained why a second person's banks looked "stuck" — they simply hadn't opened the app. Fixed by refreshing balances from /accounts/get on every sync at no extra Plaid cost.
Getting an LLM to output UI instead of prose. Free-text advice looks like a chatbot, not a product. I moved the monthly plan to a strict JSON contract and wrote a tolerant parser that extracts the object even when the model wraps it in prose or code fences, validates every field, and returns null — falling back to plain-text rendering — rather than throwing on malformed output. The parser is unit-tested against exactly those malformed cases.
Letting an AI touch financial records safely. The obvious design is to give the model write tools. I didn't. Every tool is read-only; the "write" tool returns a diff-like proposal (count, sample rows, summary) that the app renders as a confirmation card. Combined with RLS-scoped tool execution, the worst a prompt injection in a merchant name can achieve is showing you a card you decline. Early on the coach would also leak raw tool-call syntax into the chat bubble when a round ended mid-tool-use — fixed by filtering to text blocks only.
A schema migration I couldn't do in one step. Collapsing the vestigial allocation_rules table would have crashed the older build still running on a second phone, since it SELECTs that table on every envelope load. So it's a deliberate two-phase migration: 0012 adds the replacement columns and backfills them losslessly (including four percentage-based goal contributions), and the DROP waits until every device is on the new build.
Accomplishments that I'm proud of
- The cash-flow engine. 353 lines of pure, dependency-free TypeScript that turns scattered plan inputs — multiple income sources on different pay frequencies, due-day bills, un-dated everyday spending, goal contributions — into a single running balance anchored to real bank cash. Fully unit-tested, and it stores nothing.
- A genuinely agentic AI feature, not a chatbot bolted on. Tool use, a multi-round loop, structured output, per-user rate limiting, and a propose-then-confirm safety model — all in production, costing about half a cent per call.
- Security posture I'd defend in a code review. RLS on every table, provider credentials only in Edge Function secrets, tool executors scoped to the caller's own DB session, no AI writes without human confirmation.
- It runs on real money. Plaid Production, real linked banks, real transactions, on a real sideloaded iPhone build. It's the app I actually use, not a demo with seeded data.
- Restraint. I deleted a radar chart, five dead onboarding screens, a duplicate pace chart, and an entire
allocation_rulesfunding step once the data showed nobody needed them. Cutting features I'd already built was consistently the highest-value work.
What I learned
- RLS isn't just a security control, it's an architecture. Once the database enforces ownership, the AI tool layer becomes almost trivially safe to write — I stopped hand-checking
user_idin application code entirely. - Derive, don't store. Envelope "spent" and the entire cash-flow projection are computed at read time. Every stored aggregate I'd written earlier eventually disagreed with the transactions it summarized.
- Constrain the model, then trust it. JSON schemas, read-only tools, a confirmation gate, and a spend cap did more for output quality and safety than any amount of prompt tuning.
- Fail open on metering, fail closed on money. A rate-limit RPC that errors should let the user through; a proposed edit to their financial records should never apply itself.
- Integrations fail partially, not cleanly. Plaid delivers pending transactions unreliably and banks error independently. Assuming any external call fails on its own — and isolating it — was the single biggest reliability win in the project.
- The hardest part of a finance app isn't the math. It's deciding what number to show. Debt is never displayed as money. Savings envelopes never go red. Duplicated signals get deleted. Those calls shaped the product more than any algorithm.
What's next for Mizan
- Payday automation — envelope funding and goal contributions triggered by detected paycheck deposits, with local notifications, now that the cash-flow engine knows exactly when income lands.
- Bring-your-own-key AI at scale. Today I pay for inference. The planned path is OpenRouter OAuth (PKCE) so each user connects their own account and their own credits pay —
ai-adviseis already written provider-agnostically to slot this in. - Proactive coaching. The projection already detects a below-zero day before it happens; the next step is surfacing it as a push notification with a concrete fix instead of waiting for the user to open the Cash Flow screen.
- Model tiering — route quick lookups to Haiku and monthly-plan reasoning to a larger Claude model, since the proxy already abstracts the model choice.
- Multi-faith rules. The
FaithRulesinterface was designed for this on day one; Islamic finance is just the first implementation. - TestFlight, so it stops being an app I sideload for two people and starts being one other people can use. ```
Built With
- api
- claude
- deno
- edge-functions
- expo-router
- expo.io
- ios
- javascript
- jest
- plaid
- plpgsql
- postgresql
- react
- react-native
- shell
- sql
- supabase
- typescript
Log in or sign up for Devpost to join the conversation.