Files
SimpleFinanceDash/.planning/research/PITFALLS.md

30 KiB
Raw Blame History

Pitfalls Research

Domain: Adding wizard setup, auto-budget creation, design system rework, and page consolidation to an existing personal budget app (React + Supabase + TanStack Query) Researched: 2026-04-02 Confidence: HIGH (based on direct codebase analysis + known patterns for wizard state, auto-creation concurrency, and design system rework in existing apps)


Critical Pitfalls

Pitfall 1: Duplicate Budget Creation on Concurrent Auto-Create Calls

What goes wrong: The v2.0 goal is "auto-create this month's budget on first visit." The current generateFromTemplate mutation in useBudgets.ts has no deduplication guard — it creates a budget row unconditionally. If the dashboard mounts, checks for a current-month budget, finds none, and fires the auto-create, there is a race: if the component re-renders (e.g., auth state settles, React Strict Mode double-invokes effects) or the user has two tabs open, two budget rows get created for the same month. Both have the same start_date/end_date and user_id, but different UUIDs. There is no unique constraint on (user_id, start_date) in the current 004_budgets.sql migration.

Why it happens: Auto-create logic placed in a React component or effect reacts to data absence, but TanStack Query's enabled flag and mutation guards don't prevent double-invocation in Strict Mode or multi-tab scenarios. The "check then create" pattern is an inherently non-atomic operation in the current client-side architecture.

How to avoid:

  • Add a database unique constraint: UNIQUE (user_id, start_date) on the budgets table via a new migration. This makes duplicate creation fail at the DB level rather than silently succeeding.
  • In the auto-create logic, use an upsert (INSERT ... ON CONFLICT DO NOTHING) rather than a plain INSERT, so concurrent calls are idempotent.
  • Use TanStack Query's mutation state to gate the auto-create: only fire if mutation.isIdle and no existing budget is found, never fire inside a useEffect without a ref guard.
  • After auto-create, immediately invalidate the budgets list query so the new budget is fetched in the same round-trip.

Warning signs:

  • Two budget entries appear for the same month on the BudgetListPage after first visit
  • console.error shows a Supabase 23505 unique_violation if the constraint is added and the race is triggered
  • Dashboard shows "Budget already exists" error toasts intermittently on page load

Phase to address: Auto-budget creation phase — add the DB constraint first, implement upsert-based creation second. Do not build the auto-create UI trigger before the constraint exists.


Pitfall 2: Wizard State Lost on Browser Back/Refresh — No Persistence Strategy

What goes wrong: A multi-step wizard (step 1: pick categories, step 2: set amounts, step 3: confirm) built with local React state (useState) loses all progress if the user navigates away, refreshes, or presses the browser back button. For a setup wizard that may have 35 steps and requires selecting 1020 template items with amounts, this is a critical UX failure on first run. The user loses all work and must restart from step 0.

Why it happens: The natural implementation is const [step, setStep] = useState(0) and const [selections, setSelections] = useState([]) inside the wizard component. React state is ephemeral — it does not survive unmount. This works fine for modals but fails for a first-run setup wizard that the user may pause mid-way.

How to avoid:

  • Persist wizard state to localStorage keyed by user ID (e.g., wizard_state_${userId}) — read on mount, write on every step change, clear on completion/skip.
  • Use a simple serializable state shape: { step: number, selections: { categoryId, amount, tier }[] }.
  • On wizard mount: check localStorage first; if partial state exists, resume from the saved step with a "Continue where you left off" prompt.
  • On wizard completion or explicit skip: clear the localStorage key.
  • Do NOT use URL params to store full wizard state (too much data); use URL only to track the current step number (e.g., ?setup-step=2) for browser back/forward support.

Warning signs:

  • Wizard always starts at step 1 even after the user had reached step 3
  • User reports losing selected categories after an accidental page reload
  • No localStorage keys being set/read during wizard interaction

Phase to address: Wizard setup phase — define the persistence strategy before building any step components. The storage format must be finalized before the first step is wired.


Pitfall 3: Design Token Rework Breaks Existing Pages Silently

What goes wrong: The v2.0 design system rework changes CSS variables in index.css (border-radius tokens from rounded to sharp, color palette from current OKLCH pastels to sharper/clearer ones, spacing adjustments). Because every existing page consumes these global tokens, any change to --radius, --background, --foreground, --border, or the six category color variables cascades to all 9 pages simultaneously. Changing --radius from 0.5rem to 0px sharps every Button, Card, Input, and Badge across the app in one line — but it also breaks charts (Recharts SVG rx attributes don't follow CSS variables) and any component that had visual design crafted around rounded corners.

Why it happens: The current system uses shadcn/ui's CSS variable convention which is explicitly global-scope. Every component that uses rounded-lg (which resolves to var(--radius)) will change simultaneously. Pages that were "done" in v1.0 become visually broken the moment a global token changes.

How to avoid:

  • Before touching any tokens, audit which existing pages use --radius, --border, and --background implicitly (via Tailwind utilities like rounded-md, border, bg-background). This is every page.
  • Change tokens in isolation with a visual regression pass on ALL 9 pages before touching any page's component code. The token change should be one commit; the per-page fixup should be separate commits.
  • For the category color variables (--color-income through --color-investment), verify Recharts SVG fills still resolve after the change — these are passed as fill={var(--color-income)} in JS and require testing in both the browser DOM and printed output.
  • Do not land the token rework mid-phase alongside page component changes — treat it as its own atomic commit.

Warning signs:

  • Auth pages (login/register) have wrong border-radius after a --radius token change
  • Chart SVG fills appear correct in DevTools computed styles but wrong visually on screen
  • Any page that was marked "done" in v1.0 shows new visual regressions after token PR merges

Phase to address: Design system foundation phase — the first phase of v2.0. Token rework must land before any page is rebuilt. Requires an explicit visual regression check of all existing pages immediately after the token commit.


Pitfall 4: Quick-Add Page Removal Breaks Existing User Data and Nav References

What goes wrong: The quick_add_items table and QuickAddPage exist in production. Existing users have data in this table (names, icons, sort_order). If the page is simply removed from routing and the nav (deleting <Route path="quick-add" /> and the nav item in AppLayout.tsx) without a data migration or a clear deprecation path, those users:

  1. Lose access to their quick-add items (no UI)
  2. Have orphaned quick_add_items rows in the database that no longer serve any purpose
  3. May hit broken bookmarks or history entries for /quick-add

The new v2.0 model replaces quick-add with "add one-offs from category library directly in budget detail." This is a different data shape — category library items are categories, not quick_add_items. If a user had quick-add items that conceptually map to categories, that mapping is lost.

Why it happens: Page removal feels like a simple delete-the-route operation. The data persistence and existing user considerations are invisible during development because the developer's test account may have no quick-add data.

How to avoid:

  • Audit: Does any existing user data in quick_add_items map to something meaningful in the new model? If quick-add items are just named shortcuts and the new model uses categories instead, the migration path is: surface existing quick-add items in the new "category library" flow, or clearly communicate to users that this data is being retired.
  • For the route: add a redirect <Route path="quick-add" element={<Navigate to="/budgets" replace />} /> rather than leaving /quick-add as a 404. This handles bookmarks and history.
  • Remove the nav link in AppLayout.tsx at the same time as the redirect is added — never before.
  • Add a DB migration only if you decide to drop or repurpose the quick_add_items table. If keeping the table dormant, document why. If dropping it, confirm no FK references exist (none in the current schema — budget_items does not reference quick_add_items).

Warning signs:

  • Developer tests removal on a fresh account with no quick-add data, misses the issue
  • Browser history entries for /quick-add result in a blank or redirect-looping page
  • useQuickAdd hook is still imported somewhere but the page is gone, causing a dead import

Phase to address: Page consolidation phase (when BudgetDetailPage gets inline add-from-library). Remove the page only after the replacement flow is functional and tested with existing user data.


Pitfall 5: Auto-Budget Creation Ignores User Currency Setting

What goes wrong: The current generateFromTemplate mutation in useBudgets.ts accepts currency?: string with a default of "EUR". If the auto-create logic fires without reading the user's profile currency field first, every auto-created budget will be denominated in EUR regardless of the user's settings. A user who has set their currency to USD or GBP in Settings will get EUR-denominated budgets. The formatCurrency calls will then display amounts in the wrong currency until the user manually edits the budget.

Why it happens: The auto-create is triggered by checking if a budget exists for the current month and calling generateFromTemplate({ month, year }) — the currency parameter is optional and defaults silently. During development on a EUR-default account, this works fine and the bug is invisible.

How to avoid:

  • The auto-create trigger must first load the user's profile (from the profiles table, currency column) and pass that value explicitly: generateFromTemplate({ month, year, currency: profile.currency }).
  • The useProfile or useAuth hook should be resolved before the auto-create mutation fires. Use TanStack Query's enabled flag to chain the dependency: only auto-create when both the budgets list AND the profile are loaded.
  • Add an assertion in the mutation: if currency is undefined, throw rather than silently default.

Warning signs:

  • Auto-created budgets always show EUR symbol even when user has set USD in Settings
  • The profile currency is not read anywhere in the auto-create code path
  • Test passes for EUR users only; USD/GBP users never tested

Phase to address: Auto-budget creation phase — the profile dependency must be resolved as part of the auto-create implementation, not as a follow-up fix.


Pitfall 6: Wizard Creates Categories and Template Items Without Idempotency — Re-run Corrupts Data

What goes wrong: The setup wizard pre-fills common items (rent, groceries, car insurance, etc.) and on completion creates: (a) category rows in categories, (b) template_items in template_items. If the user runs the wizard, quits mid-way, then re-runs it, or if the wizard's "complete" step fires twice due to a network retry, duplicate categories and template items are created. The categories table has no unique constraint on (user_id, name). The user ends up with "Rent" appearing twice as a category and twice as a template item.

Why it happens: Wizard completion is typically a single "save all" mutation. If the user navigates back to step 1 and completes again, or if a network error causes a retry, the entire creation sequence runs again. Category creation has no deduplication.

How to avoid:

  • Add a database unique constraint on categories (user_id, name) (or at minimum check for existence before insert). Use upsert for category creation in the wizard flow.
  • Gate the wizard's final "create" step behind a isFirstRun flag stored in the user's profiles table (e.g., a setup_completed boolean). Once true, the wizard's create mutation is a no-op.
  • Use a two-phase completion approach: "Preview what will be created" → "Confirm" — this prevents accidental double-submission.
  • If using localStorage for wizard state, clear it atomically when the server-side creation succeeds, not before.

Warning signs:

  • Running the wizard twice results in duplicated category rows in the Categories page
  • Template page shows duplicate items with identical names
  • A Supabase unique constraint error on categories fires if the constraint is added after the wizard is built

Phase to address: Wizard setup phase — idempotency requirements must be specified as acceptance criteria before any wizard mutation code is written.


Pitfall 7: CSS Variable Scope Rework Breaks Recharts SVG Fill Resolution

What goes wrong: v1.0 established that category colors are passed to Recharts as CSS variable references resolved in JavaScript (var(--color-income) etc. via categoryColors in palette.ts). If the v2.0 design token rework renames these variables (e.g., from --color-income to --category-income, or changes the OKLCH values such that the Recharts SVG fill attributes show visually different colors than intended), charts break silently — the SVG renders but fills are wrong.

Additionally, Recharts SVG elements operate outside the standard CSS cascade for some browsers. If OKLCH values are changed to be very light (high L) in pursuit of "clearer pastels," SVG fills may become nearly invisible on white chart backgrounds because SVG doesn't inherit background opacity in the same way HTML elements do.

Why it happens: Design token rework focuses on HTML components and Tailwind utility class output. Recharts SVG fills are wired through palette.ts as literal CSS variable strings — not Tailwind classes — so they are easy to miss in a token audit.

How to avoid:

  • After any index.css token change, explicitly open the dashboard and inspect all three charts (donut, bar, horizontal bar) to verify fill colors match intention.
  • Keep palette.ts as the single source of truth for chart colors — do not duplicate color definitions in component files.
  • If renaming CSS variable names (not just changing values), grep the entire codebase for the old variable name before deleting it.
  • Test chart fill visibility against the new --background value — lighter backgrounds require slightly more saturated or darker chart fills to maintain visibility.

Warning signs:

  • Chart fills appear white or very faint after a design token change
  • palette.ts still references --color-income after a rename to --category-income
  • Charts look correct in Storybook/isolation but wrong on the actual dashboard

Phase to address: Design system foundation phase (when tokens change) AND dashboard phase (verification).


Pitfall 8: "Template Empty" Edge Case Breaks Auto-Budget and New-User Wizard

What goes wrong: The auto-budget creation flow depends on the template having items. The current generateFromTemplate code handles an empty template gracefully — it creates an empty budget. But the v2.0 UX intent is that visiting the dashboard for the first time should show a useful budget, not an empty one. If the wizard is bypassed (user skips setup), or if wizard completion fails silently, the auto-created budget has zero items, and the dashboard shows nothing meaningful.

A second edge case: a user who completed v1.0 setup manually (has categories + template items) visits v2.0 for the first time. The wizard should not re-run — but if the "has template items" check is the gate, and they had template items before, the wizard is skipped, and the auto-create fires correctly. If that check is wrong (e.g., checking template.name === "My Monthly Template" instead of template_items.length > 0), existing users get redirected into the wizard.

Why it happens: The first-run detection logic is complex: it must distinguish between (a) brand new user with no data, (b) existing user who set up manually in v1.0, (c) user who started wizard but didn't finish, (d) user who completed wizard. Each case needs different behavior and the logic branches are easy to conflate.

How to avoid:

  • Define the first-run gate explicitly using the profiles.setup_completed boolean (recommended in Pitfall 6) — this is the single authoritative signal, not inferred from data shape.
  • For existing v1.0 users: write a migration or a one-time check that sets setup_completed = true for any user who already has template items. This prevents the wizard from showing for users who have already configured their template.
  • Test all four user cases explicitly before shipping the wizard.

Warning signs:

  • Existing users (who set up in v1.0) see the setup wizard on their next login
  • New users who skip the wizard get a blank dashboard with no explanation
  • The setup_completed flag is never set to true for existing users via migration

Phase to address: Wizard setup phase — the first-run gate and existing-user migration must be defined as part of the wizard's acceptance criteria, not as an afterthought.


Technical Debt Patterns

Shortcut Immediate Benefit Long-term Cost When Acceptable
No UNIQUE (user_id, start_date) constraint on budgets — skip the migration Saves one migration Duplicate budgets on concurrent auto-create; data corruption that is painful to clean Never — add the constraint before any auto-create code ships
Auto-create logic in a useEffect Familiar React pattern Double-invocation in Strict Mode, fires on re-render, race conditions Never for write operations; use TanStack Query useQuery with initialData or a mutation gated on query result
Wizard state in component useState only Simplest to write Lost on refresh; user frustration on first run Only for wizards that complete in under 30 seconds and have no network steps
Skipping the profiles.setup_completed flag and inferring first-run from data shape No migration needed Complex conditional logic; wrong inference for edge-case users; breaks if data shape changes Never — explicit flag is always more reliable than inferred state
Removing /quick-add route without a redirect One less route to maintain Broken bookmarks, broken history, 404 for any hardcoded link Never — always add a redirect when removing a route
CSS token rework and page component rework in the same PR Ships faster Impossible to attribute regressions; review is overwhelming; rollback is all-or-nothing Never for design-system-wide token changes — tokens must land as their own commit
Using the quick_add_items table schema for the new category library Reuses existing table and hooks Conflates two different concepts; data model becomes unclear; future features referencing categories become harder Only if category library is purely a UI concern with no persistence — but it needs persistence, so use categories table

Integration Gotchas

Integration Common Mistake Correct Approach
Supabase + auto-create budget Plain INSERT in a client-side effect, no deduplication Use INSERT ... ON CONFLICT DO NOTHING with a unique constraint on (user_id, start_date)
TanStack Query + auto-create mutation Firing mutation inside useEffect with data dependency Use useQuery to check existence, then fire mutation via user action or a stable effect with ref guard
localStorage + wizard state Storing state without user-scoping — one user's wizard state leaks to another on shared device Key all localStorage entries with ${userId} prefix
Supabase RLS + category library pre-population Inserting pre-filled categories from client-side wizard without user_id user_id must be set to auth.uid() on all inserts; Supabase RLS policy will reject rows with wrong user_id
Recharts + new design tokens Assuming token rename in index.css auto-updates SVG fill values palette.ts references CSS variable names explicitly — any rename requires updating palette.ts too
React Router + wizard steps Using browser back button to go "back" in wizard while step is tracked only in state Use URL search params (?step=2) so browser back/forward navigate wizard steps correctly
TanStack Query + profile currency dependency Auto-create fires before profile query resolves; uses default currency Use enabled: profileLoaded && budgetMissing to chain query dependencies

Performance Traps

Trap Symptoms Prevention When It Breaks
Wizard pre-populates 20+ categories in one bulk insert without optimistic UI Wizard "complete" step hangs for 23 seconds; no feedback to user Show a loading state immediately; use mutate with onMutate for optimistic feedback Every user on wizard completion
Auto-create budget fires on every dashboard mount during loading state Multiple budget rows created; duplicate toasts; TanStack Query cache stale Gate auto-create on budgets.isSuccess (not isLoading), and use a ref to prevent repeat fires Any fast page navigation that remounts the dashboard
Category library rendered as a flat list with 50+ items and no virtualization Picker feels slow and sluggish when inline in BudgetDetailPage Virtualize the list with TanStack Virtual or paginate by category type Not an issue at 10 items; noticeable at 30+; problematic at 50+
Re-fetching template items on every wizard step navigation Network calls on every back/forward in wizard; slow perceived performance Cache template items in TanStack Query with a long staleTime; prefetch on wizard mount Noticeable on slow connections even with 5 template items

Security Mistakes

Mistake Risk Prevention
Wizard inserts categories without user_id from auth.uid() Categories created with wrong or null user_id bypass RLS or are inaccessible Always derive user_id from supabase.auth.getUser() inside the mutation; never pass it from client state
Setup wizard marks setup_completed = true client-side before DB write succeeds User appears "set up" but has no data if the write fails; auto-create never triggers again Mark setup_completed = true only in the mutation's onSuccess callback
Exposing wizard pre-fill library (category names + amounts) as a static client bundle Not a critical risk for personal finance app, but hardcoded amounts (e.g., "Groceries: $500") are visible in source Acceptable for this use case; note it is not PII

UX Pitfalls

Pitfall User Impact Better Approach
Wizard with "skip" that results in empty dashboard User skips setup, sees blank dashboard, doesn't know why nothing is there If user skips, show a non-empty empty state with a clear "Set up your template" CTA
Auto-created budget with no items (empty template) silently creates an empty budget Dashboard appears to "work" but shows zeros; user confused why their categories aren't there Check if template has items before auto-creating; if not, show a "complete your template setup" prompt
Removing quick-add nav item without explanation Users who relied on quick-add can't find the feature; assume it's broken Add a deprecation note or in-app transition message ("Quick-add is now available inline in your budget")
Wizard "back" button erases selections made in the later step User goes back to change one thing and loses all subsequent work Wizard back/forward must preserve all step state, not reset forward steps
Auto-created budget uses wrong month if user's timezone differs from server User visiting on Dec 31 in UTC+11 gets a January budget auto-created Derive month from the client's local timezone, not the server's
Category library in BudgetDetailPage shows all categories including types that contradict the budget context Income categories shown when adding a one-off expense item; confusing Filter the inline picker by relevant category types for the context (expense-only for one-off adds)

"Looks Done But Isn't" Checklist

  • Auto-create: Verify that visiting the dashboard twice in quick succession (e.g., double-clicking the nav link) creates exactly one budget, not two — check the budgets list after each scenario
  • Auto-create currency: Create a test account with currency set to USD in Settings, then visit the dashboard — verify the auto-created budget shows USD, not EUR
  • Wizard re-run: Complete the wizard on a fresh account, then manually navigate back to the wizard URL — verify it does not create duplicate categories
  • Existing user path: Log in with an account that has v1.0 template items configured — verify the wizard does NOT appear, the existing template is preserved, and a budget is auto-created correctly
  • Wizard refresh: Start the wizard, reach step 3, refresh the browser — verify state is restored from localStorage and the wizard resumes at step 3
  • Quick-add redirect: Navigate directly to /quick-add after the page is removed — verify a redirect to /budgets (or equivalent) occurs, not a blank page or 404
  • Empty template budget: Remove all items from the template, then navigate to the dashboard — verify no crash, a meaningful empty state is shown, and no empty budget is silently created
  • Token rework regression: After changing --radius or category color tokens, navigate through all 9 pages (dashboard, categories, template, budget list, budget detail, settings, login, register, and any wizard page) — verify no visual regressions
  • German locale wizard: Complete the entire wizard flow in German locale — verify no raw translation keys appear and all pre-filled item names are localized
  • Design consistency: Navigate between the newly redesigned pages and the pages not yet touched in the current phase — verify no jarring visual inconsistencies at page transitions

Recovery Strategies

Pitfall Recovery Cost Recovery Steps
Duplicate budgets from concurrent auto-create MEDIUM Write a cleanup query to find duplicate (user_id, start_date) pairs and delete the empty duplicates; add the unique constraint; patch the auto-create to use upsert
Wizard created duplicate categories MEDIUM Deduplicate categories by (user_id, name) — merge any budget_items or template_items pointing to the duplicate, then delete the duplicate category rows
setup_completed flag never set for v1.0 users — wizard shows on login LOW Write a Supabase migration that sets setup_completed = true for any user with template_items.count > 0; deploy before releasing v2.0
Token rework broke 3 pages discovered post-merge MEDIUM Revert the token commit if pages are too broken to patch quickly, or fix page-by-page with a visual regression pass; establish page-level snapshot tests
Quick-add route removed without redirect — users hitting 404 LOW Add <Route path="quick-add" element={<Navigate to="/budgets" replace />} /> — one-line fix; deploy immediately
Auto-budget uses wrong currency for existing users LOW Add a one-time correction query: update budgets created since v2.0 launch that have EUR currency to the user's profile currency where they differ; patch the auto-create to read profile currency

Pitfall-to-Phase Mapping

Pitfall Prevention Phase Verification
Duplicate budget creation on concurrent auto-create Auto-budget creation phase — add DB unique constraint before any auto-create code Verify (user_id, start_date) unique constraint exists in migration; test double-navigation scenario
Wizard state lost on refresh Wizard setup phase — define localStorage persistence strategy in design Refresh browser at step 3; verify wizard resumes at step 3
Design token rework breaks existing pages Design system foundation phase — token commit is isolated and followed by full-app visual pass All 9 pages visually checked after every token change
Quick-add removal breaks nav/bookmarks Page consolidation phase — redirect added in same commit as route removal Navigate to /quick-add directly; verify redirect
Auto-budget ignores user currency Auto-budget creation phase — profile dependency in auto-create spec Test with USD-configured account; verify budget currency
Wizard creates duplicate categories on re-run Wizard setup phase — idempotency specified as acceptance criteria Run wizard twice; verify no duplicates in Categories page
CSS variable rename breaks Recharts fills Design system foundation phase — grep for all variable references before rename Open dashboard after any token rename; inspect all chart fills
Empty template edge case shows empty dashboard Auto-budget + wizard phases — explicit first-run gate with setup_completed Test "skip wizard" path; verify empty state shown, not silent empty budget
First-run gate triggers for existing v1.0 users Wizard setup phase — migration to set setup_completed for users with existing template items Log in with a v1.0 account; verify wizard is skipped

Sources

  • Direct codebase analysis: src/hooks/useBudgets.ts (generateFromTemplate), src/hooks/useTemplate.ts (getOrCreateTemplate), supabase/migrations/004_budgets.sql, supabase/migrations/005_quick_add.sql, src/lib/types.ts, src/App.tsx, src/components/AppLayout.tsx
  • TanStack Query mutation patterns: known double-invocation behavior in React 18 Strict Mode (useEffect firing twice)
  • Supabase upsert documentation: INSERT ... ON CONFLICT DO NOTHING / DO UPDATE pattern
  • React Router v6 redirect patterns for deprecated routes
  • localStorage-based wizard state persistence: common pattern in onboarding flow implementations
  • PostgreSQL unique constraint behavior on concurrent inserts

Pitfalls research for: SimpleFinanceDash v2.0 — wizard setup, auto-budget creation, design system rework, page consolidation Researched: 2026-04-02