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/andsrc/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 validationtypes.ts- TypeScript interfaces for all domain entities (Profile, Category, Budget, BudgetItem, Template, TemplateItem, QuickAddItem)format.ts- Currency formatting using Intl.NumberFormatpalette.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.tswith 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):
- Component renders and mounts
- Component calls custom hook (e.g.,
useBudgets(),useCategories()) - Hook initializes TanStack Query with async queryFn
- Query function calls Supabase table select with filters/joins
- Supabase returns typed rows from database
- Hook caches result in Query store with staleTime of 5 minutes
- Component receives
data,loading, and error states - Component renders based on data state
- Subsequent mounts of same component use cached data (until stale)
Write Pattern (Mutate & Sync):
- Component invokes mutation handler (e.g., click "Save")
- Handler calls
mutation.mutateAsync(payload) - Mutation function marshals payload and calls Supabase insert/update/delete
- Supabase executes DB operation and returns modified row(s)
- Mutation onSuccess callback triggers Query invalidation
- Query re-fetches from server with fresh data
- Component re-renders with new cached data
- Toast notification indicates success or error
Real-time Budget Updates:
Example flow for editing a budget item (from useBudgets.ts):
- User edits amount in budget detail table → calls
updateItem.mutateAsync({ id, budgetId, budgeted_amount: X }) - Hook serializes to Supabase
.update()with .eq("id", id) - Response contains updated BudgetItem with joined Category data
- onSuccess invalidates
["budgets", budgetId, "items"]cache key - DashboardContent's useBudgetDetail query re-fetches entire items array
- 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 SupabaseonSuccess- 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 currencyCategory- user-defined expense categoryTemplate&TemplateItem- monthly budget template (fixed/variable items)Budget&BudgetItem- actual budget with tracked actual/budgeted amountsQuickAddItem- 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
ProtectedRoutewhich checks auth state viauseAuth() - Public routes wrapped in
PublicRoutewhich 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:
- Page component (routing layer, layout)
- Sub-component(s) (content logic)
- Hook(s) (data fetching)
Example: DashboardPage.tsx →
- Main component finds current month's budget
- Delegates to
DashboardContentwithbudgetIdprop - DashboardContent calls
useBudgetDetail(budgetId)for data
Error Handling
Strategy: Try-catch in mutation handlers with toast notifications for user feedback.
Patterns:
-
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.tsxlines 182-204
- Try block executes
-
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
-
Auth Errors:
useAuth.tscatches Supabase auth errors and re-throws- LoginPage catches and displays error message in red text
- Session state remains null if auth fails
-
Missing Environment Variables:
src/lib/supabase.tsthrows 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