Skip to content

okpalindrome/xpens

Repository files navigation

Xpens

A self-hosted, invite-only personal expense tracker. Connects to your Gmail account and automatically imports UPI transaction emails, so you can label, filter, and visualise your spending — all on infrastructure you control.

Self-hosted first. Your financial data never leaves your own MongoDB instance. There is no cloud service, no third-party analytics, and no shared database. You run it, you own it.


Why self-host?

  • Privacy — transaction data stays in your own MongoDB Atlas cluster (or any MongoDB instance).
  • Control — invite exactly who you want; revoke access at any time from the database.
  • No vendor lock-in — standard Next.js app; deploy anywhere (Vercel, Railway, Fly.io, VPS).
  • Extensible — add support for more banks by writing a single parser file; no other changes needed.

Features

Dashboard

  • Stat cards — spent this month, transaction count, largest transaction, top payee
  • Linked bank accounts shown alongside the dashboard heading
  • Area chart with selectable time ranges: Monthly · Quarterly · 6 Months · Year
  • Category breakdown bar chart (proportional, per selected range)

Transactions

  • Auto-imported from Gmail via the Sync button (last 30 days)
  • Filter by date range, label, or free-text search (payee / UTR / account)
  • Assign a single label per transaction inline
  • Delete transactions with confirmation dialog
  • Download as XLSX — pick a date range, get a two-sheet report (Overview + Transactions)

Labels

  • Create, rename, and delete custom labels with emoji support
  • Renaming cascades to all transactions that use the label

Security

  • Invite-only — only pre-approved email addresses can sign in
  • PKCE + state + nonce enforced on the Google OAuth flow
  • Full CSP, HSTS, X-Frame-Options, Referrer-Policy, and Permissions-Policy headers
  • All GraphQL inputs validated server-side; introspection disabled in production
  • Rate limiting on the GraphQL endpoint; MongoDB query timeouts

Stack

Layer Technology
Framework Next.js 15 (App Router)
UI shadcn/ui · Tailwind CSS v3
API GraphQL — Apollo Server 4 + Apollo Client 3
Auth NextAuth v5 (Auth.js) · Google OAuth only
Database MongoDB Atlas
Email Gmail API · modular parser registry
Charts Recharts v3
Export SheetJS (xlsx)

Getting Started

1. Clone and install

git clone https://github.com/okpalindrome/xpens.git
cd xpens
npm install

2. Set environment variables

Create .env.local in the project root:

MONGODB_URI=mongodb+srv://...
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
AUTH_SECRET=          # openssl rand -base64 32
AUTH_URL=http://localhost:3000
ADMIN_EMAIL=you@gmail.com
Variable How to get it
MONGODB_URI MongoDB Atlas → Connect → Drivers
GOOGLE_CLIENT_ID Google Cloud Console → APIs & Services → Credentials
GOOGLE_CLIENT_SECRET Same credential above
AUTH_SECRET Run openssl rand -base64 32
AUTH_URL http://localhost:3000 for local dev
ADMIN_EMAIL Your Gmail address — auto-invited on first startup

3. Configure Google OAuth

In Google Cloud Console:

  1. Create an OAuth 2.0 Client ID (Web application)
  2. Add authorized redirect URI: http://localhost:3000/api/auth/callback/google
  3. Enable the Gmail API for the same project
  4. Add your Gmail address as a test user (OAuth consent screen → Test users)

4. Run

npm run dev

On first startup, ADMIN_EMAIL is automatically added to the invites collection if it is empty. Open http://localhost:3000 and sign in with Google.


Invite Management

Access is by invitation only — there is no self-serve sign-up.

Invite a user (MongoDB Atlas Data Explorer or mongosh):

db.invites.insertOne({
  email: "newuser@gmail.com",
  invitedAt: new Date(),
  invitedBy: "admin"
})

Revoke access:

db.invites.deleteOne({ email: "user@gmail.com" })

Existing sessions expire naturally based on the JWT lifetime.


Email Parsing

How it works

  1. Sync fetches Gmail messages in the selected date range
  2. Each message's From and Subject headers are matched against the parser registry
  3. The matching parser extracts: amount, date, receiver VPA, account suffix, UTR
  4. Transactions are upserted with the Gmail message ID as the unique key — re-syncing never creates duplicates

Current parsers

Bank Sender Subject pattern
HDFC Bank alerts@hdfcbank.bank.in You have done a UPI txn

Adding a new bank

  1. Create lib/email/parsers/<bank>.ts implementing the EmailParser interface:
import type { EmailHeaders, EmailParser, ParsedTransaction } from "@/lib/email/types";

export const MyBankParser: EmailParser = {
  sender: "alerts@mybank.com",
  subjectPattern: /debit alert/i,
  subjectQuery: "Debit Alert",   // used in Gmail search query
  bankName: "My Bank",
  parse(html, headers): ParsedTransaction | null {
    const text = html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
    const amount = text.match(/INR ([\d.]+)/i);
    if (!amount) return null;
    return {
      messageId: headers.messageId,
      amount: parseFloat(amount[1]),
      date: headers.date,
      source: "unknown",
      receiver: "unknown",
      utr: "unknown",
    };
  },
};
  1. Register it in lib/email/registry.ts:
import { MyBankParser } from "@/lib/email/parsers/mybank";
const parsers: EmailParser[] = [HDFCParser, MyBankParser];

No other files need to change — the Gmail search query is built dynamically from the registry.


Architecture

app/
  (auth)/
    login/              # Sign-in page
    no-access/          # Shown for uninvited accounts
  (dashboard)/
    page.tsx            # Dashboard — stats, chart, category totals
    transactions/       # Transaction table with filters + XLSX export
    labels/             # Label CRUD with emoji picker
  api/
    auth/[...nextauth]/ # NextAuth v5 handlers
    graphql/            # Apollo Server 4 endpoint

lib/
  auth.ts               # Full NextAuth config (MongoDB adapter, PKCE, nonce)
  auth.config.ts        # Edge-compatible config (middleware — no MongoDB)
  validation.ts         # Server-side input validation for all GraphQL inputs
  excel.ts              # XLSX export (SheetJS)
  db/
    mongodb.ts          # MongoClient singleton
    models/             # user, transaction, label model functions
  email/
    types.ts            # EmailParser interface
    registry.ts         # Parser registry
    parsers/hdfc.ts     # HDFC UPI email parser
    sync.ts             # Gmail API fetch → parse → upsert
  graphql/
    schema.ts           # GraphQL SDL
    context.ts          # Auth context (userId from session)
    resolvers/          # transaction, label, sync resolvers
    operations.ts       # Client-side GQL operation documents
  apollo-client.ts      # Apollo Client singleton

middleware.ts           # Auth guard (edge-compatible config)

MongoDB Collections

Collection Description
invites Allow-list. Fields: email, invitedAt, invitedBy
user_tokens Gmail OAuth tokens. Fields: userId, accessToken, refreshToken, updatedAt
transactions UPI transactions. _id = Gmail message ID (dedup key)
labels User-defined labels. Fields: userId, name, createdAt
users Managed by NextAuth
accounts Managed by NextAuth

Development

npm run dev     # Start dev server (http://localhost:3000)
npm test        # Run test suite
npm run build   # Production build + type check
npm run lint    # ESLint

Deployment

Deploys as a standard Next.js application (Vercel, Railway, Fly.io, etc.).

Example: Vercel:

  1. Push to GitHub
  2. Import the project in Vercel
  3. Set all environment variables from the table above
  4. Add the production callback URI in Google Cloud Console: https://yourdomain.com/api/auth/callback/google
  5. Set AUTH_URL=https://yourdomain.com in Vercel environment variables

About

A self-hosted, invite-only personal expense tracker based on Gmail's Auth & UPI transaction alerts.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors