A modern expense splitting app for iOS and Android. Split bills with friends and groups, track shared expenses, and settle up easily.
- Split Expenses — Equal splits among all or selected group members
- Groups — Create and manage groups for trips, roommates, events, and more
- 1:1 Expenses — Quick expense tracking with individual friends
- Multi-Currency — INR, USD, EUR, GBP, and more
- Settle Up — Record payments and track who owes what
- Balance Spectrum — Visual bar showing each member's group balance at a glance
- Offline Support — Create expenses and settlements offline; changes sync when back online
- Real-time Sync — Changes propagate instantly across devices via Supabase Realtime
- Categories — Organize expenses with emoji icons and categories
- Profile Photos — Upload profile pictures; contact search shows in-app photos automatically
- Shadow Users — Add friends before they've signed up; their data merges when they join
| Layer | Technology |
|---|---|
| Framework | React Native 0.81 + Expo SDK 54 |
| Routing | Expo Router (file-based) |
| Backend | Supabase (PostgreSQL, Auth, Storage, Edge Functions, Realtime) |
| Data Fetching | TanStack Query (React Query v5) |
| Animations | Moti + React Native Reanimated 4 |
| Bottom Sheets | @gorhom/bottom-sheet v5 |
| Analytics | PostHog |
| Offline Queue | AsyncStorage-backed sync queue |
- Node.js 18+
- npm
- Xcode (iOS) or Android Studio (Android)
- Expo Go app for physical device testing
-
Clone the repository:
git clone <repository-url> cd settle
-
Install dependencies:
npm install
-
Set up environment variables:
cp .env.example .env # Fill in your Supabase project URL, anon key, and PostHog key -
Start the development server:
npm start
-
Run on a device:
- Press
i— iOS Simulator - Press
a— Android Emulator - Scan the QR code with Expo Go for a physical device
- Press
| Variable | Description |
|---|---|
EXPO_PUBLIC_SUPABASE_URL |
Supabase project URL |
EXPO_PUBLIC_SUPABASE_ANON_KEY |
Supabase anon/public key |
EXPO_PUBLIC_POSTHOG_KEY |
PostHog analytics key |
settle/
├── app/ # Screens (Expo Router file-based routing)
│ ├── _layout.tsx # Root layout (auth guard, query client)
│ ├── (auth)/ # Unauthenticated screens
│ │ ├── sign-in.tsx
│ │ ├── sign-up.tsx
│ │ ├── verify-otp.tsx
│ │ ├── set-password.tsx
│ │ ├── forgot-password.tsx
│ │ └── reset-password.tsx
│ ├── (tabs)/ # Main tab navigator
│ │ ├── index.tsx # Home — balance summary + recent activity
│ │ ├── friends.tsx # Friends — per-friend net balances
│ │ ├── groups.tsx # Groups — list of all groups
│ │ └── profile.tsx # Profile — settings, photo, sign out
│ ├── add-expense.tsx # Create a new expense
│ ├── create-group.tsx # Create a new group
│ ├── settle-up.tsx # Record a payment
│ ├── expense/[id].tsx # Expense detail / edit
│ ├── friend/[id].tsx # Friend detail — transaction history
│ ├── group/
│ │ ├── [id]/index.tsx # Group detail — members, activity, spectrum bar
│ │ └── [id]/settings.tsx # Group settings — rename, members, delete
│ └── settings/
│ └── about.tsx # App version info
│
├── components/ # Shared UI components
│ ├── ui/
│ │ ├── avatar.tsx # Unified Avatar (user/group, circle/squircle, edit mode)
│ │ ├── balance-spectrum-bar.tsx # Group balance visualisation
│ │ ├── button.tsx
│ │ ├── input.tsx
│ │ ├── skeleton.tsx
│ │ ├── empty-state.tsx
│ │ ├── offline-banner.tsx
│ │ ├── country-picker.tsx
│ │ ├── contribution-bar.tsx
│ │ └── icon-symbol.tsx
│ ├── people-search-sheet.tsx # Unified contact + group bottom sheet picker
│ ├── group-settle-sheet.tsx # Settle Up member selector
│ ├── edit-settlement-sheet.tsx
│ ├── filter-scrubber.tsx
│ └── haptic-tab.tsx
│
├── hooks/ # Custom React hooks
│ ├── use-enriched-contacts.ts # Device contacts + Supabase enrichment (photos, userId)
│ ├── use-contact-group-search.ts # Search layer on top of useEnrichedContacts
│ ├── use-groups.ts / use-group.ts
│ ├── use-friends.ts / use-friend-detail.ts
│ ├── use-expenses.ts / use-expense.ts
│ ├── use-settlements.ts
│ ├── use-direct-group.ts
│ ├── use-recent-activity.ts
│ ├── use-realtime-sync.ts
│ ├── use-network-status.ts
│ ├── use-user.ts
│ └── use-categories.ts
│
├── contexts/
│ ├── auth-context.tsx # Current user session
│ └── sync-context.tsx # Online/offline + sync state
│
├── lib/ # Utilities and services
│ ├── supabase.ts # Supabase client
│ ├── analytics.ts # PostHog wrapper
│ ├── analytics-events.ts # Event name constants
│ ├── sync-manager.ts # Offline sync orchestration
│ ├── sync-queue.ts # AsyncStorage-backed operation queue
│ ├── pending-items.ts # Pending item tracking
│ ├── image-upload.ts # Profile / group photo upload
│ ├── otp-service.ts # OTP request helpers
│ ├── haptics.ts # Haptic feedback helpers
│ ├── query-client.ts # TanStack Query configuration
│ ├── storage.ts # AsyncStorage helpers
│ └── utils.ts
│
├── constants/
│ ├── colors.ts # Design system colour palette
│ └── theme.ts
│
├── types/ # TypeScript type definitions
│ ├── index.ts
│ └── database.ts # Supabase schema types + CURRENCIES map
│
└── supabase/
├── config.toml
├── migrations/ # SQL migrations (source of truth for schema)
└── functions/ # Deno Edge Functions
├── _shared/ # Shared CORS / auth utilities
├── send-otp/
├── verify-otp/
├── create-account/
└── reset-password/
| Screen | Description |
|---|---|
Home (/) |
Net balance summary, recent activity feed |
Friends (/friends) |
All friends with net balance, quick-add expense |
Groups (/groups) |
All groups with balance indicator |
| Group Detail | Members, expense list, balance spectrum bar, settle-up sheet |
| Friend Detail | Transaction history, shared group balances |
| Add Expense | Search contacts/groups via bottom sheet, fill in split |
| Settle Up | Record a payment; available from home and group screens |
| Profile | Edit name/photo, sign out, manage account |
Contact/group search is split into three layers:
useEnrichedContacts— loads device contacts, batch-queries Supabase to attachuserIdandavatarUrlto each contact, and fetches the user's groups. Single source of truth.useContactGroupSearch— thin search/filter wrapper overuseEnrichedContacts. Used byadd-expense.PeopleSearchSheet— bottom sheet UI powered directly byuseEnrichedContacts. Supports multi-select (group member adding) and single-select (add-expense) modes viashowGroupsandselectedIdsprops.
components/ui/avatar.tsx is the single component for all avatar rendering:
userprop → circle, initials fallbackgroupprop → squircle, people-icon fallbackgroupImageUriprop → squircle for local URIs (create-group form)mode="edit"→ camera badge overlay, acceptsonEditPressandisUploading
SyncContexttracks online/offline state viauseNetworkStatus- Mutations go through
SyncManagerwhich queues operations inSyncQueue(AsyncStorage) when offline - On reconnect, queued operations replay in order
- Visual indicators (
OfflineBanner, disabled actions) prevent data loss UX issues
When a user adds an expense with a phone contact who hasn't signed up yet, a shadow user row is created in the users table (is_registered: false). When that contact signs up with the same phone number, their account merges with the shadow record — all existing expenses and group memberships carry over.
Schema is managed through SQL migrations in supabase/migrations/. Migrations are the source of truth — never edit the Supabase dashboard schema directly.
npm run deploy # Link CLI → push migrations → deploy Edge Functions
npm run db:push # Push migrations only (to linked project)
npm run functions:deploy # Deploy Edge Functions onlyLink the CLI to a project before deploying:
supabase link --project-ref <project-ref>- Phone number → OTP verification → account creation
- Optional password for re-authentication
- Handled by Supabase Auth + custom Edge Functions (
send-otp,verify-otp,create-account)
Proprietary software. All rights reserved.