Track your spending in your own Google Sheet — the sheet is the database.
- Login with Google — the initial login requests only basic identity information.
- Choose sheet access — authorize Google Sheets to create or connect sheets as yourself, or share one specific sheet with the optional service account.
- Add expenses — entries are appended straight to your sheet. Dashboard, filters and budgets read from it live.
The Google Sheets scope is requested incrementally only when a user chooses Google-account access.
The service-account method does not require the user to grant that scope and can access only sheets
explicitly shared with its email. The app does not request access to other Drive file types. There is
also no database and no server-side storage: user settings live in a hidden Config tab, OAuth
tokens live in an encrypted session cookie, and the connected sheet id lives in an httpOnly cookie.
- Next.js (App Router) + TypeScript
- Auth.js with basic Google login and incremental
spreadsheetsauthorization - Storage adapter over the Google Sheets API, with optional selected-sheet service-account access
- Tailwind CSS + shadcn/ui, Recharts, SWR
- PWA (installable on mobile)
Expenses tab, columns A–O:
| Id | CreatedAt | Date | Month | Amount | Currency | Category | Type | PaymentMode | Note | Tags | Source | SplitTotal | SplitWith | SplitPaidBy |
|---|
When an expense is split with friends, Amount is your share (what analytics count), SplitTotal is the full bill, SplitWith lists friends' shares as Rahul: 300, Anna: 200, and SplitPaidBy records who paid. The app uses these fields to calculate who owes whom.
Config tab (hidden): categories, paymentModes, defaultCurrency, monthlyBudget as key/value rows.
- Node.js 20 or newer
- npm
- A Google Cloud project
- A Google account added as an OAuth test user while the app is in Testing mode
git clone https://github.com/cube-root/expenser.git
cd expenser
npm ciFollow Google Cloud setup to enable the Sheets API, configure the OAuth consent screen, and create a Web application OAuth client. For local development, configure:
Origin: http://localhost:3000
Redirect URI: http://localhost:3000/api/auth/callback/google
cp .env.example .env.local
npx auth secretFill in the generated secret and OAuth credentials:
AUTH_SECRET=<generated secret>
AUTH_GOOGLE_ID=<oauth client id>.apps.googleusercontent.com
AUTH_GOOGLE_SECRET=<oauth client secret>Environment files are ignored by Git. Never commit OAuth secrets or service-account keys.
Optionally configure GOOGLE_SERVICE_ACCOUNT_EMAIL and GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY to
offer a selected-sheet-only setup method. The option is hidden unless both are set. See the
service account setup guide.
GOOGLE_SERVICE_ACCOUNT_EMAIL=myexpense@project-id.iam.gserviceaccount.com
GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"npm run devOpen http://localhost:3000, sign in with a configured test account, choose an access method, and connect or create a sheet. Restart the development server after changing environment variables.
Run these before opening a pull request:
npm run lint
npx tsc --noEmit
npm run buildContributions are welcome. For a focused review and a clean history:
-
Fork the repository and create a branch from the current default branch:
git checkout -b fix/short-description
-
Install with
npm ciand configure.env.localfrom.env.example. -
Keep changes scoped to one feature or fix. Avoid unrelated formatting and lockfile changes.
-
Preserve compatibility with existing Google Sheets. If the sheet schema changes:
- append new columns instead of reordering existing columns;
- provide defaults for rows created by older versions;
- ensure Resync structure can add the new headers without changing expense rows.
-
Never add credentials, downloaded service-account JSON files, personal sheet IDs, or production data to commits, screenshots, fixtures, logs, or pull requests.
-
Run lint, TypeScript, and the production build.
-
Open a pull request explaining:
- what changed and why;
- how it was tested;
- any environment, OAuth, cookie, or sheet-schema changes;
- screenshots for visible UI changes.
When reporting a bug, include reproduction steps, expected and actual behavior, browser/runtime details, and sanitized error output. Remove access tokens, cookies, email addresses, sheet IDs, and private URLs before posting.
Deploys as a standard Next.js app on Vercel — set the same environment variables and add the production redirect URI to the OAuth client.
Production releases use the Release and deploy GitHub Actions workflow. Local production releases are intentionally blocked.
Configure these GitHub Actions secrets, preferably on a protected production environment:
VERCEL_TOKENVERCEL_ORG_IDVERCEL_PROJECT_ID
Follow the complete Vercel GitHub Actions setup guide to create a scoped token, find the project IDs, protect the production environment, and run a release.
In Settings → Actions → General → Workflow permissions, allow Read and write permissions so
the workflow's GITHUB_TOKEN can push the version commit and tag. If main has a ruleset that
blocks automation pushes, allow this workflow to bypass that rule or use an approved release token.
To release, open GitHub → Actions → Release and deploy → Run workflow, choose patch, minor,
or major, and run it from main. The workflow validates the project, uses release-it to bump
package.json and package-lock.json, creates and pushes the release commit and vX.Y.Z tag, and
then deploys that exact release to Vercel production. npm publishing remains disabled. Deployment
does not run if the Git push fails.
The included Dockerfile.bubblewrap uses Bubblewrap's official container image, so a local JDK and
Android SDK are not required. Docker must be installed and the production PWA must be reachable.
Build the local tooling image and create a directory for the generated Android project:
./scripts/build-android-pwa.shThe script builds the Docker image, initializes the Android project from the deployed manifest when needed, and builds the APK and App Bundle. To run those stages manually instead, use:
docker build -f Dockerfile.bubblewrap -t myexpense-bubblewrap .
mkdir -p .bubblewrap/androidInitialize the project:
docker run --rm -it \
-v "$PWD/.bubblewrap/android:/app" \
myexpense-bubblewrap init \
--manifest="https://expense.abhijith.me/manifest.webmanifest"Answer Bubblewrap's application ID and signing-key prompts, then build the APK and Android App Bundle from the same generated project:
docker run --rm -it \
-v "$PWD/.bubblewrap/android:/app" \
myexpense-bubblewrap buildThe generated packages remain under .bubblewrap/android. Keep the signing keystore and its
passwords private and backed up; the same key is required for future Play Store updates. Before
publishing, also deploy the Digital Asset Links file produced for the signing key at
https://expense.abhijith.me/.well-known/assetlinks.json.