A mobile-first Progressive Web App to capture, categorize, and back up every bill & receipt directly to your Google Drive.
Built with TanStack Start (SSR), React 19, Redux Toolkit, Tailwind CSS v4, and the Google Drive API v3.
- Google Drive storage — bill metadata stored as
bills.json; attachments uploaded into organized folders - Category folders — files sorted into
BillVault/<category>/on Drive automatically - Patient sub-folders — Medical bills further sorted by family member (
Hari,Monika,Divith,Father,Mother) - 16 bill categories — Medical, Groceries, Dining, Travel, Electronics, and more
- Rich bill form — title, amount, currency (INR/USD/EUR/GBP), date, vendor, payment method, tags, notes, and up to 5 attachments
- Attachment viewer — image previews + in-app PDF viewer via Google Drive embed
- Search & filter — full-text search across title/vendor/notes; filter by category, payment method, date range, amount range, and tag
- Grid / list view — toggleable from the bills page
- Dark mode — persisted per-device via
localStorage - PWA — installable on Android/iOS/desktop with offline shell via Workbox
- About modal — floating ℹ button with developer contact & links
| Layer | Library / Tool |
|---|---|
| Framework | TanStack Start v1 (SSR + Server Functions) |
| Routing | TanStack Router v1 (file-based) |
| UI | React 19, Radix UI primitives, Tailwind CSS v4 |
| State | Redux Toolkit + redux-persist (UI preferences only) |
| Icons | react-icons v5 (Font Awesome subset) |
| Charts | Recharts |
| Toasts | Sonner |
| Storage | Google Drive API v3 (bills + attachments) |
| Auth | OAuth 2.0 refresh-token or Google Service Account JWT |
| Testing | Vitest + @testing-library/react + jsdom |
| PWA | vite-plugin-pwa + Workbox |
| Package manager | pnpm |
bill-vault/
├── public/
│ ├── favicon.svg # SVG favicon (receipt icon)
│ ├── icon-192.png # PWA icon
│ ├── icon-512.png # PWA icon (large)
│ └── manifest.json # Web App Manifest
├── src/
│ ├── components/
│ │ ├── Bills/ # BillCard, BillDetail, BillFilters, BillForm, BillList
│ │ ├── Common/ # AboutModal, CategoryIcon, EmptyState, FileUploader
│ │ ├── Dashboard/ # Dashboard, SpendingChart
│ │ ├── Layout/ # AppShell (sidebar + mobile bottom nav)
│ │ └── PWA/ # InstallPrompt
│ ├── hooks/ # use-blob-url, use-mobile
│ ├── routes/ # File-based routes (TanStack Router)
│ │ ├── __root.tsx # Root layout + BillsLoader + metadata
│ │ ├── index.tsx # Dashboard
│ │ ├── bills.index.tsx # Bills list
│ │ ├── bills.add.tsx # Add bill
│ │ ├── bills.$id.tsx # Layout for bill detail/edit
│ │ ├── bills.$id.index.tsx # Bill detail
│ │ ├── bills.$id.edit.tsx # Edit bill
│ │ └── settings.tsx # Settings
│ ├── services/
│ │ ├── drive.functions.ts # TanStack Server Functions (client-callable)
│ │ ├── googleDrive.server.ts # Google Drive API helpers (server-only)
│ │ └── storage.ts # IndexedDB blob store (legacy)
│ ├── store/ # Redux store, billSlice, uiSlice, StoreProvider
│ ├── types/ # Bill, Attachment, CategoryType, PaymentMethod
│ └── utils/ # constants, formatters
├── test/ # Vitest unit tests (87 tests)
├── .env.example # Environment variable template
└── vitest.config.ts
BillVault stores everything in your Google Drive under a BillVault/ root folder. You need valid credentials before the app can save or load bills.
- Open Google Cloud Console → Credentials
- Select your OAuth 2.0 Client ID → add
https://developers.google.com/oauthplaygroundto Authorized redirect URIs → Save - Go to OAuth Playground
- Click the gear icon → enable "Use your own OAuth credentials" → enter your Client ID & Secret
- In Step 1 select scope:
https://www.googleapis.com/auth/drive.file - Click Authorize APIs → sign in → Exchange authorization code for tokens
- Copy the
refresh_tokenvalue - Add to
.env:
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-your-secret
GOOGLE_REFRESH_TOKEN=1//your-refresh-tokenNote: Your Google Cloud app must have your email listed as a Test User (OAuth consent screen → Test users) while it remains in Testing mode.
- IAM & Admin → Service Accounts → Create Service Account
- Skip optional role/permission steps → Done
- Open the service account → Keys tab → Add Key → JSON → download
- Add to
.env(paste the entire JSON as a single line or use the file path):
GOOGLE_SERVICE_ACCOUNT_JSON={"type":"service_account","project_id":"...","private_key_id":"...","private_key":"-----BEGIN RSA PRIVATE KEY-----\n...","client_email":"...@....iam.gserviceaccount.com","token_uri":"https://oauth2.googleapis.com/token"}Service accounts never expire and don't require browser-based consent flows.
- Node.js ≥ 20
- pnpm ≥ 9
npm install -g pnpmgit clone https://github.com/HariKrishna-9885699666/bill-vault.git
cd bill-vault
pnpm installCopy the example and fill in your Google credentials:
cp .env.example .envMinimum required variables:
# Option A — OAuth refresh token
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REFRESH_TOKEN=
# Option B — Service Account (takes precedence if set)
GOOGLE_SERVICE_ACCOUNT_JSON=pnpm devOpens at http://localhost:8080 (or next available port).
pnpm build
pnpm preview # preview production build locallypnpm test # run all tests once
pnpm test:watch # watch mode
pnpm test:ui # Vitest UI in the browser87 unit tests covering:
src/store— billSlice (add/update/delete/setBills), uiSlice (theme/filters/viewMode)src/utils— formatters (currency, date, bytes), constants (categories, payment methods)src/components— CategoryIcon, EmptyState (render + conditional action)src/hooks— useMobile (breakpoint detection + resize events)src/types— Bill and Attachment shape validation
My Drive/
└── BillVault/
├── bills.json ← all bill metadata (auto-managed)
├── medical/
│ ├── Hari/ ← patient sub-folder
│ ├── Monika/
│ ├── Divith/
│ ├── Father/
│ └── Mother/
├── groceries/
├── dining/
├── travel/
└── ... ← one folder per category
Folders are created automatically on first upload. Bills are never stored locally — all data lives in Drive.
| Variable | Required | Description |
|---|---|---|
GOOGLE_SERVICE_ACCOUNT_JSON |
One of A/B | Full service account key JSON |
GOOGLE_CLIENT_ID |
One of A/B | OAuth 2.0 client ID |
GOOGLE_CLIENT_SECRET |
One of A/B | OAuth 2.0 client secret |
GOOGLE_REFRESH_TOKEN |
One of A/B | Long-lived refresh token |
Hari Krishna Anem
| anemharikrishna@gmail.com | |
| Phone | +91 98856 99666 |
| GitHub | github.com/HariKrishna-9885699666 |
| linkedin.com/in/anemharikrishna | |
| Blog | hashnode.com/@HariKrishna-9885699666 |
| Portfolio | harikrishna.is-a-good.dev |
This project is private. All rights reserved © 2026 Hari Krishna Anem.