A simple, real-world expense splitting web app for students and friends. Inspired by Splitwise, built with a focus on clarity, speed, and actual usability.
Live demo: https://split-it-wise.vercel.app
Contributions are welcome, see Contributing below. If you're new to open source, this is a friendly place to start.
- Google Sign-In via Firebase Authentication
- Create a group instantly
- Invite friends with a shareable link (works well over WhatsApp)
- Join with one click
- Admin controls: rename the group, remove members, delete the group
- Turn the invite link off once everyone's in, since a forwarded link otherwise works forever
- Amounts in INR (₹)
- Pick who paid
- Three ways to split:
- Equal: pick who's included, the app divides it up
- Exact: type what each person owes
- Percentage: type each person's share as a %
- Delete an expense (creator or admin)
- Per-person balance: "₹X lena hai" (you're owed) or "₹X dena hai" (you owe)
- All money maths runs in integer paise, so balances always sum to exactly zero, with no drift from rounding ₹100 three ways
- Anyone removed from a group while still owing money stays visible in the ledger, tagged "left group", so their debt is never silently lost
- Balances are simplified into actual payments — "Rahul pays Akshat ₹300" — rather than leaving everyone to work out the transfers themselves. A group of n people never needs more than n-1 payments
- Record a repayment in full or in part, and undo one recorded by mistake
- Add your UPI ID and others get a one-tap upi:// link that opens GPay, PhonePe or Paytm with the amount prefilled (phone-first: desktop has nothing to handle the link)
- Repayments are stored separately from expenses, so paying a friend back never inflates the group's spending totals
- Firestore security rules enforce every permission server-side: group membership, admin-only actions, and expense ownership
| Framework | Next.js 16 (App Router) |
| Language | JavaScript (ES modules) |
| UI | React 19, Tailwind CSS v4 |
| Auth | Firebase Authentication (Google) |
| Database | Cloud Firestore |
| Tests | Vitest |
| Hosting | Vercel |
Everything runs client-side and talks to Firestore directly. There's no backend server
or API layer, so security lives in firestore.rules rather than in the app code.
git clone https://github.com/aashu2006/split-it-wise.git
cd split-it-wise
npm install
npm run devWith no .env.local, the app starts in demo mode. Click Continue with Google and
you're signed in as a sample account with two groups, six expenses across all three
split types, a recorded settlement and a former member who still owes money — enough to
reach every screen and almost every UI branch. Data lives in memory, so a reload puts it
back to the seed and nothing leaves your machine.
You're admin of one group and not the other, so the admin-only controls are reachable
both ways. The fixtures live in src/lib/demo.js.
Demo mode is only ever active when the Firebase config is absent, so it can't shadow real data. For anything touching auth, Firestore or the security rules, do the full setup below.
You'll need your own Firebase project to develop against. It's free and takes about five minutes.
- Node.js 20.19+ (check with
node -v) - A Google account
git clone https://github.com/aashu2006/split-it-wise.git
cd split-it-wise
npm install- Go to the Firebase console and click
Add project. Name it anything you like,
split-it-wise-devworks. - Google Analytics is optional; you can turn it off.
- In the sidebar, go to Build → Authentication → Get started
- Select Google under Sign-in method, toggle it Enable
- Pick a support email, then Save
localhost is authorised by default, so nothing else to configure for local dev.
- Go to Build → Firestore Database → Create database
- Choose a location near you
- Start in production mode. This repo ships its own rules and you'll deploy them in step 7.
- In Project settings (gear icon), scroll to Your apps
- Click the web icon (
</>), give it a nickname, and register the app - You'll see a
firebaseConfigblock. Keep it open.
Create a file called .env.local in the project root and copy the values across:
NEXT_PUBLIC_FIREBASE_API_KEY=AIza...
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-project.firebasestorage.app
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=123456789
NEXT_PUBLIC_FIREBASE_APP_ID=1:123456789:web:abc123.env.local is gitignored, so never commit it.
There's one optional extra:
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=6L...Set it to turn on App Check, which stops anyone who lifts the config above from pointing the SDK at your project and running up its quota. Leave it out and the app works exactly as before, so you don't need one to develop locally. In production it's worth having: register the site under App Check in the Firebase console with reCAPTCHA v3, then switch on enforcement for Cloud Firestore — the key alone doesn't turn anything away.
These keys are not secrets. Anything prefixed
NEXT_PUBLIC_is compiled into the browser bundle and is visible to anyone using the site. That's normal for Firebase web apps, since a web API key just identifies the project. The security comes entirely from the Firestore rules, which is why step 7 matters.
Without this, the app can't read or write anything.
npx firebase-tools login
npx firebase-tools deploy --only firestore:rules,firestore:indexes --project YOUR_PROJECT_IDThis pushes firestore.rules (permissions) and firestore.indexes.json (the composite
index the expense list query needs).
npm run devOpen http://localhost:3000 and sign in with Google.
To try the invite flow, open the invite link in a private window and sign in with a second Google account.
src/
├── app/ Pages (App Router)
│ ├── page.jsx Home, your groups
│ ├── group/[groupId]/ Group dashboard
│ └── join/[groupId]/ Invite link handler
├── components/ UI components
├── context/ AuthContext: Google sign-in, current user
├── lib/ All Firestore access + business logic
│ ├── firebase.js SDK setup
│ ├── groups.js Create, join, rename, remove members, delete
│ ├── expenses.js Add, fetch, delete expenses
│ ├── user.js Profile lookups
│ └── calculations.js Splitting + balance maths
└── types/ Shared data shapes, as JSDoc typedefs
firestore.rules Server-side permissions, the real security
firestore.indexes.json Composite indexes
Rules of thumb:
- Firestore calls belong in
src/lib/, not in components - Components stay presentational and take props
- Any change to permissions needs a matching change in
firestore.rules
npm testCovers the splitting and balance maths in src/lib/calculations.test.js, the part where
a bug costs people real money. If you touch calculations.js, add a test.
There's no compiler to catch mistakes here, so the linter, the tests and a build are the whole safety net. Worth running before you open a PR:
npm run lint # ESLint: unused vars, hook deps, Next.js rules
npm run build # production buildThe linter currently reports a handful of warnings, all pre-existing and all tracked in the roadmap above. Keep it at zero errors.
Open for anyone to pick up. Comment on an issue (or open one) before starting so we don't duplicate work.
Most wanted
- Edit an expense, since currently you can only add and delete. A typo'd amount means deleting the expense and re-entering it.
- Real-time updates. The app refetches after every change instead of using
Firestore
onSnapshotlisteners, so a second device won't see a new expense until it reloads. This is also what thereact-hooks/set-state-in-effectlint warnings are pointing at — the fetch-then-setState effects go away once the data arrives as a subscription.
Nice to have
- Expense categories and filtering
- Export a group's history to CSV
- Multiple currencies (INR is hardcoded)
- Dark mode
Housekeeping
- Component tests, only the maths is covered today
- Swap the
<img>avatar inMembersListfornext/image(needs aremotePatternsentry forgoogleusercontent.com) - Member emails are fetched to every group member's browser (needed by the current user lookup). Splitting public profile fields from private ones would fix it.
- One-time cleanup of expenses orphaned by groups deleted before cascade delete existed
- Fork the repo and create a branch off
main:git checkout -b feat/settle-up
- Make your change. Match the surrounding style. The codebase is plain JavaScript
with no state library, and comments explain why rather than what. Data shapes
live as JSDoc typedefs in
src/types/index.js— reference them from JSDoc rather than leaving a function's arguments undocumented. - Check it works:
npm test npm run lint npm run build - Open a pull request describing what changed and how you tested it. Screenshots help for UI changes.
Good to know
- If your change touches permissions, update
firestore.rulestoo. The checks insrc/lib/run in the browser and can be bypassed, so they're UX, not security. - Money is handled in integer paise (see
toPaise/toRupeesincalculations.js). Never sum floats, because0.1 + 0.2 !== 0.3and small errors compound into wrong balances. - Don't commit
.env.localor any service account key
Bug reports and feature ideas are just as welcome as code, so open an issue.
MIT, so it's free to use, modify and distribute, including commercially. Just keep the copyright notice. By contributing, you agree your work is licensed the same way.
Author: Akshat Patil & Community ❤️