Contents
TABLE OF CONTENTS
Finance OS
BuildingNext.jsTypeScriptPostgreSQL+2 more

Finance OS

A no BS, full-stack personal finance workspace with real-time dashboards, budget tracking, savings goals, and a unified transaction ledger — purpose-built to replace Notion for serious personal finance management.

Timeline

Ongoing

Role

Full Stack

Team

Solo

Status
Building

Technology Stack

Next.js
TypeScript
PostgreSQL
Drizzle ORM
Better Auth

Key Challenges

  • Atomic Multi-Table Mutations
  • Parallel Server-Side Data Fetching
  • Complex Relational Data Modeling
  • Real-Time Budget Computation

Key Learnings

  • Next.js App Router & Server Components
  • Drizzle ORM & Database-Level Constraints
  • Better Auth Integration
  • Advanced Recharts Patterns

## Overview

Finance OS is a personal financial operating system built from scratch — not assembled from templates, not shoehorned into a general-purpose tool. It emerged from a deliberate decision to stop managing personal finances inside Notion and instead build something that fits the problem correctly from the start.

The core insight: personal finance is not a document problem, it is a data problem. Notion's rollups, calculated properties, and filtered views hit hard limits the moment you want real spending trend charts, live budget trackers, or savings goals linked directly to transaction records. Finance OS replaces all of that with a purpose-built data model, a deliberate UI, and behavior that is exactly what's needed — nothing more, nothing less.

## Key Features

  • Net Worth Dashboard: A real-time overview of total net worth across all accounts, cash flow balance, and account distribution — all fetched in parallel on the server using React Suspense with promise passing, so the page skeleton renders immediately and each chart streams in independently.
  • Unified Transaction Ledger: Every money movement — expenses, incomes, transfers — is captured in one place with full metadata: payment method, category, tags, linked bank account, and optional savings goal.
  • Live Budget Tracking: Per-category spending limits computed in real time against actual transaction data, with support for monthly, weekly, and yearly periods. One budget per user per category per period is enforced at the database level.
  • Savings Goals with Atomic Funding: Goals track progress against a target amount and optional target date. Funding a goal atomically deducts from the source account, records a transfer transaction, and updates the goal balance — all inside a single database transaction. Goals auto-complete when the target is met.
  • Recurring Transactions: Schedule templates for repeating expenses and incomes (daily, weekly, monthly, yearly). A server action processes them into real transaction records and advances the next due date.
  • Bank Account Registry: Tracks all financial accounts — checking, savings, credit cards, cash wallets, investments — each with a running balance, currency (defaulting to INR), and optional UI metadata like a hex color and icon identifier.

## Technical Implementation

### Architecture

Finance OS is built on Next.js 16 with the App Router and treats Server Components as the default rendering mode. Data fetching happens on the server, close to the database. Server Actions handle all mutations, eliminating the need for dedicated API routes for standard CRUD operations. The route structure uses route groups to separate the authenticated dashboard shell from the public landing page without polluting the URL.

### Data Layer

The schema is defined in TypeScript using Drizzle ORM, with every table living in its own file under db/schema/. Constraints — type enumerations, amount positivity checks, unique indexes — are declared at the schema level and enforced at the PostgreSQL level (hosted on Supabase), not just in application code. This means the database itself is the final guardian of data integrity, not just the application layer.

Schema tables:
  user, session, account, verification   → Managed by Better Auth
  bank_account                           → Accounts with balance and currency
  category                               → System-default and user-created categories
  transaction                            → Core ledger: expenses, incomes, transfers
  recurring_transaction                  → Scheduled repeating transaction templates
  budget                                 → Spending limits per category per period
  goal                                   → Savings goals with live progress tracking
  tag, transaction_tag                   → Cross-cutting transaction labels

### Authentication

Better Auth handles the entire auth surface — email/password with enforced email verification, and Google OAuth. The auth adapter integrates directly with Drizzle, meaning auth tables live in the same PostgreSQL database with no separate auth service. On user creation, the system hooks in to generate a DiceBear avatar seeded from the user's name, giving every account a distinct identity without requiring a profile photo.

### Parallel Data Fetching

The dashboard page demonstrates a key architectural pattern: all chart queries are initiated at the top of the Server Component and their promises are passed down to child components wrapped in Suspense. Queries run in parallel on the server rather than sequentially, and each chart resolves and hydrates independently — no loading spinners, no sequential waterfalls.

// All promises kicked off at the top of the Server Component
const spendingTrendPromise = getSpendingTrend()
const budgetStatusPromise = getBudgetStatus()
const categoryBreakdownPromise = getCategoryBreakdown()
const goalsProgressPromise = getGoalsProgress()
 
// Passed into Suspense boundaries — each chart resolves independently
<Suspense fallback={<ChartSkeleton />}>
  <SpendingTrend dataPromise={spendingTrendPromise} />
</Suspense>

### Atomic Goal Funding

Funding a savings goal touches three tables simultaneously: the source bank account balance, a new transaction record, and the goal's current amount. This is wrapped in a db.transaction() call inside the Server Action so the operation succeeds or fails as a unit, regardless of what happens mid-request.

await db.transaction(async (tx) => {
  await tx.update(bankAccounts).set({ balance: newBalance }).where(...)
  await tx.insert(transactions).values({ ...transferRecord, goalId })
  const updatedGoal = await tx.update(goals).set({ currentAmount: newAmount }).where(...).returning()
  if (updatedGoal[0].currentAmount >= updatedGoal[0].targetAmount) {
    await tx.update(goals).set({ status: 'completed' }).where(...)
  }
})

### UI and Styling

Styling is handled entirely with Tailwind CSS v4, with a dark-first default theme and neutral base tones. Typography uses Inter for the primary sans-serif and Geist Mono for monospaced contexts. The component library is built on Base UI and Radix UI primitives — accessible, unstyled behaviors wrapped into a cohesive design system that covers buttons, inputs, comboboxes, dialogs, drawers, menus, toasts, calendars, number fields, and more.

Animation runs at two levels: Framer Motion for component-level transitions and layout animations, and GSAP for complex timeline and scroll-driven sequences. @number-flow/react handles animated numeric transitions in the financial summary cards, so values update smoothly as data resolves rather than snapping abruptly.

Data visualization is powered entirely by Recharts: a spending trend bar chart across the last twelve months, budget status indicators, a category spending pie chart, and goals progress visualization.

### Forms and Validation

All form state is managed with React Hook Form, paired with Zod for schema-based validation via @hookform/resolvers. The same Zod schemas used for client-side form validation serve as the single source of truth for form shape on the server — validation logic is not duplicated.

## Project Structure

finance-os/
  app/
    (landing)/           # Public landing page
    (dashboard)/         # Authenticated application shell
      layout.tsx         # Sidebar, header, progressive blur overlay
      dashboard/         # Overview with summary cards and charts
      transactions/      # Transaction ledger and management
      budgets/           # Budget configuration and status
      goals/             # Savings goals with fund allocation
    api/                 # Auth endpoint handlers
    auth/                # Sign in, sign up, verify pages
  components/
    ui/                  # Full design system of primitive and composed components
    common/              # Sidebar, search, shared app-level components
    forms/               # Transaction, goal, and budget form components
    evilcharts/          # Recharts-based chart wrapper components
    landing/             # Landing page components
    providers/           # Theme and context providers
  db/
    schema/              # Drizzle ORM table definitions, one file per table
    index.ts             # Database client initialization
    seed.ts              # Development seed data script
  lib/
    auth.ts              # Better Auth configuration
    queries/             # Read-only database query functions
  actions/               # Server Actions for all mutations

## Design Decisions

Server Components as the default. The entire dashboard fetches data on the server, keeping the client bundle lean and Time to First Byte low. Client components are used only where interactivity demands it — forms, animated values, chart rendering.

Database-level constraints over application-level guards. Enumerations, positivity checks, and uniqueness requirements live as CHECK constraints and UNIQUE indexes in PostgreSQL. Application validation is a convenience layer on top of what the database already guarantees.

INR as the default currency. Finance OS is built for personal use with the Indian financial context in mind. INR is the default currency on bank accounts, though the field accepts any currency string.

No API routes for mutations. Server Actions replace the entire CRUD API surface, meaning there is no parallel request/response layer to maintain for standard operations. The mutation lives as close to the database as possible.

## Challenges

Atomic multi-table mutations were the most architecturally critical challenge — particularly goal funding, where a failure halfway through could leave the bank balance deducted without a transaction record, or the transaction recorded without the goal updated. Wrapping this in a database transaction with automatic rollback was the only correct solution.

Parallel server-side data fetching required understanding how Next.js handles promise passing through Suspense boundaries. The naive approach (awaiting each query sequentially in the Server Component) would waterfall every chart load. Promise passing decouples initiation from resolution and lets the client start rendering immediately.

Real-time budget computation meant there is no precomputed "current spent" value stored in the database — budget status is always derived from aggregating actual transaction records filtered by category and period. This keeps the data model clean but required careful query optimization to avoid N+1 patterns on the dashboard.

Karansingh

Design & Developed by Karan Singh

© 2026. All rights reserved

Command Palette

Search for a command to run...