A free, local-first shared-expense app for trips, dinners, homes, and other group activities—no account required. Tally tracks who paid, supports equal or exact splits, calculates clear suggested payments, and can synchronize a trusted group through a capability-protected live activity backed by Supabase.
On first use, Tally asks for a display name and stores that identity only in the current browser. The name replaces the ambiguous generic “You” in participant lists and is included as the sender identity when an activity link is shared.
Tally supports English and Simplified Chinese. It starts in Chinese when the browser language is Chinese or the device time zone is in mainland China; otherwise it starts in English. A manual choice in Settings is saved in the browser and always takes priority. Expense timestamps use the browser's IANA time zone, which is shown in Settings.
Production uses privacy-preserving first-party analytics through Supabase for both browser-local and live activity workflows. Only allowlisted event names, a coarse local/live/snapshot surface, the resolved en/zh-CN UI locale, an allowlisted currency code for currency-selection events, and a one-way session hash are stored—never precise location, URLs, capability tokens, activity IDs, names, descriptions, amounts, or balances. Tally does not load a third-party analytics beacon. See the analytics design.
Tally supports two deliberately different sharing modes:
- Share QR creates a read-only snapshot compressed into the URL fragment. Recipients can inspect it or save an isolated local copy without changing the sender's activity.
- Share live creates a short capability URL for one canonical activity in Supabase. Trusted recipients with the complete link can load and edit the same revision-checked data from different browsers.
- Share link opens the device share sheet, with a separate copy-link fallback. If Safari opens the link outside the installed PWA, Join activity safely transfers the copied link into the existing Tally app session.
Opening a snapshot never overwrites browser data, and Tally never loads third-party analytics. First-party measurement records only the coarse snapshot surface, never the fragment containing names and expense details. Live links keep their secret edit token in the fragment; Supabase stores only its SHA-256 hash. Every browser that successfully opens a live link keeps the latest full snapshot as a recovery copy, while Supabase remains the source of truth for as long as that Live session is available. See the live sharing architecture and production deployment guide.
For launch copy, a privacy-safe demo storyboard, and channel guidance, see the promotion playbook.
- Create activity groups and add friends without requiring profiles or accounts.
- Choose one currency per activity and change it later; every expense, balance, settlement, live session, and export stays consistent.
- Record who paid and split expenses equally among everyone or only selected people, or enter exact amounts.
- Keep historical splits stable when friends join later, then explicitly edit an expense when it should include them.
- See clear person-to-person settlement directions instead of an ambiguous group balance.
- Record full or partial settlement payments, keep repayment history, and undo mistakes without inflating spending totals.
- Edit and delete expenses, or delete an entire activity and its local data.
- Export a shareable PNG summary for friends.
- Persist data in the browser and synchronize changes across open tabs.
- Install Tally as a PWA and reopen the local app shell without a network connection.
- Collaborate across browsers through short, revision-checked live activity links that automatically load newer changes while visible.
- Continue a Safari-opened shared link in an installed Tally PWA without abandoning the existing app session.
- Measure anonymous local and live feature usage without sending activity data or secret URLs to analytics.
- Use the responsive interface on desktop or mobile.
- Switch between English and Simplified Chinese, with China-aware defaults and local-time expense timestamps.
Local activities remain in browser localStorage and can be viewed after the installed app shell has been cached for offline use. Live activities are stored in Supabase and are editable by anyone with the full capability link while connected. Each participating browser also keeps the latest full Live snapshot locally: if the connection is unavailable, that recovery copy is read-only so offline changes cannot silently conflict. A person can explicitly duplicate it into an independent editable activity, or continue it locally after the backend confirms that the Live session has ended. There are no user accounts or participant-level permissions. Read PRIVACY.md before deploying or sharing real activity data.
Currency selection controls display only. Tally does not convert amounts or support mixed currencies inside one activity.
- React 19 and TypeScript
- Vite
- TanStack Query for live-activity server state, polling, and mutations
- Zod for versioned activity-snapshot validation
- Vite PWA and Workbox for installation and offline app-shell caching
- Lucide icons
- Vitest and Testing Library
- GitHub Actions for CI and deployment
- GitHub Pages for static hosting
- Supabase Postgres for optional live activities and first-party product analytics
Requirements:
- Node.js 24 recommended; Node.js 22.13 or newer is supported
- npm
git clone https://github.com/PengfanZ/splitbill.git
cd splitbill
npm ci
npm run devOpen http://localhost:3000.
Local activities and QR snapshots work without a backend. To develop live sharing as well, install a Docker-compatible runtime and the Supabase CLI dependencies included in this repository, then run:
cp .env.example .env.local
npm run backend:start
npm run backend:reset
npm run test:backend
npm run devAfter the local stack starts, replace your-publishable-key in .env.local with the publishable key printed by npm run backend:start. Production configuration and secrets are documented in docs/DEPLOYMENT.md.
| Command | Purpose |
|---|---|
npm run dev |
Start the local development server |
npm run backend:start |
Start the local Supabase stack |
npm run backend:reset |
Recreate the local database and apply every migration |
npm run test:backend |
Run pgTAP database and security contracts |
npm run lint |
Lint all TypeScript and React files with zero warnings allowed |
npm test |
Run the Vitest unit, component, and behavior suite |
npm run test:all |
Run coverage plus the Playwright integration suite |
npm run test:behavior |
Run focused happy-path and edge-case scenarios |
npm run test:coverage |
Run all tests and enforce 100% coverage |
npm run test:e2e |
Build the GitHub Pages bundle and run the Chromium integration suite |
npm run typecheck |
Type-check every TypeScript and TSX file without emitting output |
npm run build:pages |
Build the static artifact with the GitHub Pages base path |
npm run build |
Build a root-hosted production artifact |
npm run preview |
Preview the production build locally |
src/
├── App.tsx # Application orchestration
├── components/ # Reusable shell components
├── data/ # Browser persistence
├── domain/ # Models and pure financial logic
├── features/
│ ├── activity/ # Dashboard and expense workflows
│ ├── identity/ # Browser-local participant identity
│ ├── liveSharing/ # Capability links and backend synchronization
│ └── sharing/ # QR snapshots and PNG exports
├── hooks/ # React lifecycle integrations
├── i18n/ # Locale selection, translations, and time formatting
├── pwa/ # Testable service-worker cache helpers
├── queryClient.ts # Shared server-state defaults
└── sw.ts # Offline shell and cache lifecycle
supabase/
├── migrations/ # Versioned schema and RPC releases
└── tests/ # pgTAP database and security contracts
See ARCHITECTURE.md for dependency boundaries and persistence rules.
Every push and pull request must pass:
- realistic happy-path tests;
- edge cases for cents, floating-point inputs, browser APIs, and persistence;
- direct unit tests for pure domain modules;
- compiler validation for every TypeScript and TSX file;
- ESLint with TypeScript and React Hooks rules and zero warnings;
- component and helper tests;
- Playwright integration tests against the production GitHub Pages build;
- pgTAP contracts for live-activity and analytics access control, validation, privacy, retention, and rate limits;
- 100% statement, branch, function, and line coverage;
- a production static build.
The complete contract is documented in TESTING.md.
Every pull request is type-checked, linted, tested, and built by GitHub Actions. A successful main release applies pending Supabase migrations before publishing the configured frontend artifact to GitHub Pages. Follow docs/DEPLOYMENT.md for the one-time environment setup and release procedure.
Contributions and focused bug reports are welcome. Read CONTRIBUTING.md before opening a pull request, and use SECURITY.md for vulnerability reports.
Tally is available under the MIT License. Copyright (c) 2026 Pengfan Zhang.
