Skip to content

PengfanZ/splitbill

Repository files navigation

Tally

CI Live demo Coverage

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.

Sharing and live collaboration

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.

Try the live demo

For launch copy, a privacy-safe demo storyboard, and channel guidance, see the promotion playbook.

Friends sharing expenses with Tally

Highlights

  • 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.

Important data note

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.

Tech stack

  • 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

Getting started

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 dev

Open 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 dev

After 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.

Available scripts

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

Project structure

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.

Quality and testing

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.

Deployment

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.

Contributing

Contributions and focused bug reports are welcome. Read CONTRIBUTING.md before opening a pull request, and use SECURITY.md for vulnerability reports.

License

Tally is available under the MIT License. Copyright (c) 2026 Pengfan Zhang.

About

Local-first shared expense splitting with live collaboration, flexible splits, settlements, QR sharing, and privacy-friendly analytics. Built with React, TypeScript, and Supabase.

Topics

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages