Files
SimpleFinanceDash/.planning/codebase/ARCHITECTURE.md

9.7 KiB

Architecture

Analysis Date: 2026-03-16

Pattern Overview

Overall: Layered client-side SPA with React, using hooks for data access and state management through TanStack Query, centralized authentication with Supabase, and component-based UI rendering.

Key Characteristics:

  • Three-tier vertical slice: Pages → Hooks → Library (Supabase + utilities)
  • TanStack Query for caching and synchronization of remote state
  • Route-based code organization with role-based access control (protected/public routes)
  • Supabase PostgreSQL backend with real-time subscriptions capability
  • Internationalization (i18n) at the root level with JSON resource files

Layers

Presentation Layer (Pages & Components):

  • Purpose: Render UI, handle user interactions, coordinate component composition
  • Location: src/pages/ and src/components/
  • Contains: Page components (DashboardPage, TemplatePage, BudgetListPage, etc.), UI primitives from src/components/ui/, and custom components (AppLayout, QuickAddPicker)
  • Depends on: Hooks, UI utilities, i18n, icons (lucide-react), formatting utilities
  • Used by: React Router for page routing

Hooks Layer (Data Access & State):

  • Purpose: Encapsulate all server communication and query/mutation logic, manage request/response caching via TanStack Query
  • Location: src/hooks/
  • Contains: Custom hooks for each domain (useAuth, useCategories, useBudgets, useTemplate, useQuickAdd)
  • Depends on: Supabase client, TanStack Query, type definitions
  • Used by: Page and component layers for CRUD operations and state retrieval

Library Layer (Core Services & Utilities):

  • Purpose: Provide primitive implementations, type definitions, and configuration
  • Location: src/lib/
  • Contains:
    • supabase.ts - Supabase client initialization and environment validation
    • types.ts - TypeScript interfaces for all domain entities (Profile, Category, Budget, BudgetItem, Template, TemplateItem, QuickAddItem)
    • format.ts - Currency formatting using Intl.NumberFormat
    • palette.ts - Category color mapping and labels (internationalized)
    • utils.ts - General utilities
  • Depends on: Supabase SDK, TypeScript types
  • Used by: Hooks and components for types and helpers

Authentication Layer:

  • Purpose: Manage session state and auth operations
  • Location: src/hooks/useAuth.ts
  • Implementation: Reactive Supabase auth listener with automatic session refresh
  • State: Session, user, loading flag
  • Operations: signUp, signIn, signInWithOAuth (Google, GitHub), signOut

Internationalization (i18n):

  • Purpose: Provide multi-language support at runtime
  • Location: src/i18n/index.ts with JSON resource files (en.json, de.json)
  • Framework: react-i18next with i18next
  • Initialization: Automatic at app startup before any render

Data Flow

Read Pattern (Fetch & Display):

  1. Component renders and mounts
  2. Component calls custom hook (e.g., useBudgets(), useCategories())
  3. Hook initializes TanStack Query with async queryFn
  4. Query function calls Supabase table select with filters/joins
  5. Supabase returns typed rows from database
  6. Hook caches result in Query store with staleTime of 5 minutes
  7. Component receives data, loading, and error states
  8. Component renders based on data state
  9. Subsequent mounts of same component use cached data (until stale)

Write Pattern (Mutate & Sync):

  1. Component invokes mutation handler (e.g., click "Save")
  2. Handler calls mutation.mutateAsync(payload)
  3. Mutation function marshals payload and calls Supabase insert/update/delete
  4. Supabase executes DB operation and returns modified row(s)
  5. Mutation onSuccess callback triggers Query invalidation
  6. Query re-fetches from server with fresh data
  7. Component re-renders with new cached data
  8. Toast notification indicates success or error

Real-time Budget Updates:

Example flow for editing a budget item (from useBudgets.ts):

  1. User edits amount in budget detail table → calls updateItem.mutateAsync({ id, budgetId, budgeted_amount: X })
  2. Hook serializes to Supabase .update() with .eq("id", id)
  3. Response contains updated BudgetItem with joined Category data
  4. onSuccess invalidates ["budgets", budgetId, "items"] cache key
  5. DashboardContent's useBudgetDetail query re-fetches entire items array
  6. Component recalculates totals and re-renders pie chart and progress bars

State Management:

  • No Redux or Zustand — TanStack Query handles async state
  • Local component state for UI interactions (dialogs, forms, selections)
  • Session state maintained by Supabase auth listener in useAuth hook
  • Cache invalidation is the primary sync mechanism

Key Abstractions

Query Key Pattern:

Each hook defines typed query keys as const arrays:

  • ["categories"] - all user categories
  • ["budgets"] - all user budgets
  • ["budgets", id] - single budget
  • ["budgets", id, "items"] - items for a specific budget
  • ["template"] - user's monthly template
  • ["template-items"] - items in template
  • ["quick-add"] - user's quick-add library

Purpose: Enables granular invalidation (e.g., update one budget doesn't refetch all budgets)

Mutation Factories:

Hooks expose mutations as properties of returned object:

  • useCategories() returns { categories, loading, create, update, remove }
  • useBudgets() returns { budgets, loading, createBudget, generateFromTemplate, updateItem, createItem, deleteItem, deleteBudget }

Each mutation includes:

  • mutationFn - async operation against Supabase
  • onSuccess - cache invalidation strategy

Hook Composition:

useBudgetDetail(id) is both a standalone export and accessible via useBudgets().getBudget(id):

  • Enables flexible usage patterns
  • Query with .enabled: Boolean(id) prevents queries when id is falsy
  • Used by DashboardPage to fetch current month's budget data

Type Hierarchy:

Core types in src/lib/types.ts:

  • Profile - user profile with locale and currency
  • Category - user-defined expense category
  • Template & TemplateItem - monthly budget template (fixed/variable items)
  • Budget & BudgetItem - actual budget with tracked actual/budgeted amounts
  • QuickAddItem - quick entry library for rapid data entry

Relationships:

  • User → many Profiles (one per row, per user)
  • User → many Categories
  • User → one Template (auto-created)
  • Template → many TemplateItems → Category (join)
  • User → many Budgets (monthly)
  • Budget → many BudgetItems → Category (join)
  • User → many QuickAddItems

Entry Points

Application Root:

  • Location: src/main.tsx
  • Initializes React 19 with context providers:
    • QueryClientProvider (TanStack Query)
    • BrowserRouter (React Router)
    • TooltipProvider (Radix UI)
    • Toaster (Sonner notifications)
  • Creates single QueryClient with 5-minute staleTime default

Route Configuration:

  • Location: src/App.tsx
  • Defines protected and public routes
  • Protected routes wrapped in ProtectedRoute which checks auth state via useAuth()
  • Public routes wrapped in PublicRoute which redirects authenticated users away
  • Routes:
    • /login, /register - public (redirect home if logged in)
    • /, /categories, /template, /budgets, /budgets/:id, /quick-add, /settings - protected

Layout Root:

  • Location: src/components/AppLayout.tsx
  • Renders Sidebar with navigation items (using Radix UI primitives)
  • Renders Outlet for nested route content
  • Provides sign-out button in footer

Page Entry Points:

Each page component is stateless renderer with logic split between:

  1. Page component (routing layer, layout)
  2. Sub-component(s) (content logic)
  3. Hook(s) (data fetching)

Example: DashboardPage.tsx

  • Main component finds current month's budget
  • Delegates to DashboardContent with budgetId prop
  • DashboardContent calls useBudgetDetail(budgetId) for data

Error Handling

Strategy: Try-catch in mutation handlers with toast notifications for user feedback.

Patterns:

  1. Mutation Errors:

    • Try block executes mutation.mutateAsync()
    • Catch block logs to console and shows toast.error(t("common.error"))
    • Button remains clickable if error is transient
    • Example in TemplatePage.tsx lines 182-204
  2. Query Errors:

    • Query errors are captured by TanStack Query internally
    • Hooks return loading state but not explicit error state
    • Pages render null/loading state during failed queries
    • Retry is configured to attempt once by default
  3. Auth Errors:

    • useAuth.ts catches Supabase auth errors and re-throws
    • LoginPage catches and displays error message in red text
    • Session state remains null if auth fails
  4. Missing Environment Variables:

    • src/lib/supabase.ts throws during module load if VITE_SUPABASE_URL or VITE_SUPABASE_ANON_KEY missing
    • Prevents runtime errors from undefined client

Cross-Cutting Concerns

Logging: Minimal logging — relies on:

  • Browser DevTools React Query Devtools (installable)
  • Console errors from try-catch blocks
  • Sonner toast notifications for user-facing issues

Validation:

  • Input validation in mutation handlers (e.g., category ID check, amount > 0)
  • Type validation via TypeScript compile-time
  • Supabase schema constraints (NOT NULL, FOREIGN KEY, CHECK constraints)
  • Database uniqueness constraints (e.g., one template per user)

Authentication:

  • Supabase session token stored in localStorage (browser SDK default)
  • Auth state checked at page render time via useAuth() hook
  • Protected routes redirect unauthenticated users to /login
  • Session persists across page refreshes (Supabase handles recovery)

Authorization:

  • Row-level security (RLS) policies on Supabase tables enforce user_id filters
  • No explicit authorization logic in client (relies on DB policies)
  • All queries filter by current user via await supabase.auth.getUser()

Architecture analysis: 2026-03-16