docs: map existing codebase
This commit is contained in:
240
.planning/codebase/ARCHITECTURE.md
Normal file
240
.planning/codebase/ARCHITECTURE.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# 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*
|
||||
Reference in New Issue
Block a user