Skip to main content

Building SnapStub: A Full-Stack Expense Tracker

#go #flutter #react #fullstack #ocr

The Problem

This project started from my own daily struggle: dealing with receipts and invoices. I’d stuff them in my wallet, forget to log expenses, and by the end of the month have no idea where my money went. Most tracking apps require manual entry, which is friction nobody wants.

I wanted something smarter: snap a photo of your receipt, and let the system figure out the rest. That’s how SnapStub was born. It’s a personal project to solve my own problem while showcasing my full-stack skills.

The Architecture

SnapStub is a full-stack project with three main components:

  1. Go Backend API handling authentication, OCR processing, and data storage
  2. React Web App for desktop users who prefer a dashboard
  3. Flutter Mobile App for on-the-go expense tracking

Why This Stack?

Go for the backend was an easy choice. It compiles to a single binary, has excellent concurrency support, and the Gin framework makes building REST APIs straightforward. The type system catches bugs at compile time rather than runtime.

React for web because the ecosystem is mature. TanStack Query handles all the data fetching complexity, and Tailwind CSS with shadcn/ui components let me build a polished UI without fighting CSS.

Flutter for mobile because I wanted true cross-platform. One codebase, two platforms. MobX handles state management reactively, and the offline-first architecture means the app works even without network connectivity.

The Backend

The Go API is the heart of the system. Key features:

  • JWT Authentication via Supabase for secure, stateless auth
  • OCR Processing using Tabscanner API to extract receipt data
  • Cloud Storage with Cloudflare R2 for receipt images
  • PostgreSQL with GORM for data persistence
  • Row Level Security ensuring users only access their own data

The architecture follows dependency injection patterns for testability. Each handler depends on services, which depend on repositories. This makes mocking trivial during testing.

// Clean separation of concerns
type ExpenseHandler struct {
    service *ExpenseService
}

type ExpenseService struct {
    repo    *ExpenseRepository
    storage *R2Service
    ocr     *TabscannerService
}

OCR Integration

The system supports multiple OCR providers through a pluggable architecture:

  • Tabscanner API for cloud-based receipt parsing with high accuracy
  • Vision Models via OpenAI-compatible APIs (OpenRouter, local inference). Tested with Qwen3 VL 30B and Gemma 3 27B locally with great results
  • Tesseract OCR as an optional local fallback (enabled via Go build tags)

The receipt processing flow:

  1. User uploads an image
  2. Image gets stored in Cloudflare R2
  3. Selected OCR provider extracts structured data (total, line items, merchant)
  4. Extracted data gets saved with user’s expense record
  5. User can review and edit before confirming

The cloud integrations use async polling with exponential backoff since OCR processing takes a few seconds. The provider can be switched via environment configuration, making it easy to compare accuracy and cost across services.

The Web Frontend

Built with React 19, Vite, and TanStack Query. The dashboard provides:

  • Expense overview with category breakdowns
  • Budget tracking with progress indicators
  • Receipt image viewer with full-screen lightbox
  • Manual expense entry for those without receipts
  • User profile with financial goals

TanStack Query eliminates most of the data fetching boilerplate. Mutations automatically invalidate queries, keeping the UI in sync with the server.

const { data: expenses } = useQuery({
  queryKey: ['expenses'],
  queryFn: fetchExpenses,
})

const deleteMutation = useMutation({
  mutationFn: deleteExpense,
  onSuccess: () => queryClient.invalidateQueries(['expenses']),
})

The Mobile App

Flutter with MobX for reactive state management. The app has two expense models:

  • Local Expense: Manual entries stored in SQLite for offline access
  • API ExpenseReceipt: OCR-scanned receipts synced with the backend

This dual model lets users track expenses offline, then sync when connected. The camera integration makes receipt scanning feel native.

Key packages:

  • Dio for HTTP with interceptors
  • sqflite for local persistence
  • fl_chart for expense visualizations
  • image_picker for camera/gallery access

Lessons Learned

Start with the API contract. I defined the OpenAPI spec before writing code. This let me work on frontend and backend in parallel without integration surprises.

Offline-first is worth the complexity. Users expect mobile apps to work without network. SQLite sync adds code, but the UX improvement is significant.

Type safety across the stack. TypeScript on web, Dart on mobile, Go on backend. Every layer catches type errors at compile time. Runtime surprises are rare.

Docker simplifies deployment. Multi-stage builds keep images small. The Go binary is under 15MB. Compose orchestrates everything locally.

What’s Next

The app works, but there’s always more:

  • AI-powered expense categorization
  • Budget alerts and notifications
  • Recurring expense detection
  • Export to CSV/PDF for tax season

Building SnapStub reinforced that full-stack development is about making good tradeoffs. No single technology is perfect, but the right combination creates something greater than its parts.

Interested in trying it out? Feel free to reach out and I’ll set you up with a demo account.