Open Finapp
Reference

What Changed Since Firebase

What changed between v6 (Firebase) and the current Supabase + PowerSync app - backend, performance, security, tests, and architecture.

Two eras. Finapp's backend went from Firebase (v6 - Firebase Realtime Database + Firebase Auth) to the current stack: Supabase Postgres + PowerSync (offline-first) + Supabase Auth.

This document is a side-by-side comparison of what changed. The app-level improvements (performance, refactoring, UX, TypeScript, i18n) and the backend, auth, sync, and dependency sections all describe the current codebase. See Architecture for a full description of the current Supabase + PowerSync architecture.

Side-by-side comparison of what the app looks like on the firebase branch (v6, Firebase) vs the current Supabase + PowerSync app.

Backend

firebase (Firebase)current
DatabaseFirebase Realtime DatabaseSupabase Postgres (replicated by PowerSync)
AuthFirebase Auth via nuxt-vuefireSupabase Auth (email/password)
Data accessReal-time listeners (getDataAndWatch)Local SQLite via db.watch(...), writes via upsertRow / deleteRow
ValidationFirebase security rules (JSON)Postgres RLS (auth.uid()) + client-side Zod schemas
Server codeNone (client-only SDK)Supabase migrations + self-hosted PowerSync sync rules (per-user WHERE "userId" = auth.user_id())
Exchange ratesClient-side fetchrates table synced through PowerSync like any other table
User settingslocalStorage onlyuser_settings table (baseCurrency, locale), auto-created on signup by a trigger
Schema indexesN/AClient SQLite indexes in AppSchema.ts (userDate: ['userId','date'], plus userId per table)
MigrationN/AFirebase JSON export -> transform -> PostgREST import via scripts/import-firebase.mjs (see Migration History)
Account deletionN/ARLS-scoped delete of the user's rows + local wipe (removeAllUserData)

What the current backend gives

  • Offline-first - all reads/writes hit local SQLite; PowerSync syncs in the background, so the app works fully offline
  • Per-user isolation - Postgres RLS (auth.uid()) + PowerSync sync rules guarantee each user only ever syncs their own rows
  • Typed client schema - AppSchema.ts defines the SQLite tables; row<->item transforms are centralized in transforms.ts
  • Automatic upload queue - local mutations are queued and uploaded by the connector (uploadData), with auto-reconcile on rejected ops
  • Incremental sync - PowerSync streams only changed rows after the initial sync

Store Pattern

firebasecurrent
Data sourceFirebase listener -> setTrns()db.watch('SELECT * FROM ...') -> reconcile -> setX()
PersistdeepUnref() + immediate localforage.setItem()PowerSync SQLite is the source of truth; a blob read-through cache (useStoreCache.ts) primes the first frame
Offline opsManual helpers in trns/helpers.ts, ad-hoc localStorage keysNone needed - PowerSync's built-in upload queue handles offline writes
ID systemFirebase push IDsClient-generated UUIDs, stable across sync (no remap)
Optimistic UIPartial (fire-and-forget)Full: upsertRow/deleteRow write local SQLite -> watch re-emits -> connector uploads in the background
Sync verificationNonePowerSync checksums per bucket; rejected uploads trigger auto-reconcile (uploadReconcile.ts)

The vue-deepunref dependency is removed. Store data uses shallowRef, so deep cloning on every persist was unnecessary.

Performance

Algorithm improvements

WhatfirebasecurrentImpact
Wallet totalsgetWalletTotal(id) called per wallet in reduce - each call filters all trns: O(W×N)getWalletsTotals() single pass, distributes amounts via Map: O(N), then O(1) lookup per wallet5 wallets × 10K trns: ~50K → ~10K ops
Interval bucketingInline in Item.vue: intervalsInRange.reduce() filtering all trns per interval: O(N×I)Extracted to bucketTrnsByIntervals(): single pass + binary search: O(N log I)Year/365 days/5K trns: 1.8M → ~43K comparisons
Transaction filtering6 sequential .filter() chained via reduce + walletsIds.some(w => includes(w)) O(W²) + always sorts O(N log N)Single .filter() with early returns + Set.has() O(1) + sorting optional5 intermediate arrays + O(W²) → single pass + O(1); sort O(N log N) skipped when not needed
Wallet lookup in getTotalwalletsIds.includes(): O(W) per transfernew Set(walletsIds).has(): O(1) per transferLinear → constant per lookup
Demo data generation{ ...acc } spread in .reduce() + getTransactibleIds() called twice per iterationDirect acc[i] = ... + lookups cached before loopO(N²) → O(N)
Category statisticscategoriesStat eagerly computes both grouped and ungrouped on every change; filter().at(0) for biggestLazy computed() per variant, only accessed one executes; .find() stops at first matchUp to 2× fewer computeCategoriesWithData calls
Vertical categories filterverticalCategories.filter(c => c.value !== 0) called twice in template (title + v-for): re-evaluated on every renderCached in visibleVerticalCategories computed: filtered once, reused2 filter passes → 1 cached
Number formattingcurrency.js library + currencies.find() O(N) called twice per formatNative Intl.NumberFormat cached in Map + currencyMap.get() O(1)No library overhead; formatter created once per precision
Last created trnObject.keys().sort().find() - sorts all trns O(N log N) then searchesSingle O(N) pass tracking running max date10K trns: ~130K ops → ~10K ops
Category parent checkcategoriesForBeParent calls Object.values(trns).some() O(N) per root category: O(M×N)Pre-built usedCategoryIds Set via for...in O(N) + Set.has() O(1) per check; no intermediate arrays200 categories × 10K trns: ~2M → ~10K ops
Recent categoriesObject.keys(trns).filter().sort() O(N log N) + acc.some(c => c.id) O(K²) dedup + includes() O(W) per categorySingle O(N) pass via Map tracking latest date + entries().toSorted() O(K log K) + Set.has() O(1)10K trns: ~130K + K² → ~10K + K log K ops
Transactible categories.reduce() with acc.includes() O(M) + childIds.filter(!acc.includes) O(C×M): O(M²)Map<parentId, childIds[]> built in O(M) + Set dedup O(1) per check200 categories: ~40K → ~200 ops
Transfer detectiontransferCategoriesIds.includes(categoryId) O(W) array scan per transactiontrn.type === TrnType.Transfer O(1) enum comparisonLinear → constant per check
Type filter counts3 separate .filter() passes over trnsIds (expense, income, transfer): O(3N)Single loop with counters object: O(N)3 full passes → 1 pass
Trn item cache in listcomputeTrnItem(id) called twice per item in template (item + date): O(2P) callsPre-computed trnItemsMap via Map: O(P) compute + O(1) lookups30 items: ~60 → ~30 computations per render
Group totals cachegetTotalOfTrnsIds() called twice per group in template (income + expense): O(2G) callsPre-computed groupTotals Map + paginatedTotal computed: O(G) compute + O(1) lookups15 groups: ~30 → ~15 total computations per render
Category grouping sortcategories.sort() called inside reduce on every child push: O(K² log K) per parentSort once after grouping loop, only if length > 1: O(K log K) per parent5 children: ~50 → ~12 comparisons
Category grouping totalsgetTotalOfTrnsIds(cat.trnsIds).sum called twice per child (for push value + parent sum)Computed once into catTotal, reused: single call per child2× fewer getTotal calls per child category
Data syncFirebase full replacement on every update: O(N)Delta sync: only changed records fetched - O(D) where D « NAvoids reprocessing unchanged transactions
Interval generationlist.unshift(current) in while loop: O(n) per call, O(n²) totallist.push(current) + list.reverse(): O(1) per call, O(n) totalYear/365 days: ~66K shifts → ~365 pushes + 1 reverse
Date duration keysDynamic { [\${period}s`]: value } - TS seesRecord<string, number>, no type check against Duration`toDuration(period, value) returns typed Duration via exhaustive switchCompile-time safety; new period variants cause errors, not silent fallback
Date formattingEach format function called twice (type: 'start' + type: 'end'), concatenated; 3 IIFE switch blocks for period comparisonEach format function returns full string; isSamePeriod(a, b, period) replaces all 3 switches~40% fewer lines in useGetDateRange; no double calls
Average computationsum !== 0 checked 3 times + Object.keys(items).length > 0 guardEarly sum === 0 exit; day average always present (range ≥ 2 guaranteed)Single check instead of 4
Favorite categories sortInline sort duplicating parent-name + name logic (8 lines)Reuses compareCategoriesByParentAndName utility (1 line)DRY; single sort implementation to maintain
Recent categories loop.reduce() iterates all sorted entries even after reaching maxCategories limitfor loop with break stops at limit200 categories, limit 16: ~200 → ~16 iterations
Recent categories iterationfor...of Object.keys(trnsItems) creates intermediate arrayfor...in trnsItems iterates directly on objectNo intermediate array allocation for 10K+ trns
Category IDs reuseObject.keys(items.value) called separately in categoriesRootIds, favoriteCategoriesIdsBoth reuse categoriesIds computedKeys computed once, shared across dependents
Transactible IDs reusecategoriesIdsForTrnValues re-filters all categories with !hasChildren checkDelegates to already-computed transactibleIds + .filter(id !== 'transfer')Avoids redundant O(M) parent scan
Children IDs dedupgetChildrenIdsOrParent duplicates .filter() logic from getChildrenIdsDelegates to getChildrenIds(), checks .lengthSingle filter implementation

Bundle and rendering

Whatfirebasecurrent
ECharts9 components in main bundle (incl. PieChart, DataZoom, MarkPoint)7 components in lazy-loaded async chunk (unused removed)
Theme plugin81 lines with SSR inline scripts and regex replacements32 lines, SPA-only appConfig updates from localStorage
PersistdeepUnref() on every localforage.setItem() - full recursive cloneDirect write, debounced 300ms, no cloning
Responsive layoutuseWindowSize() JS listener for sidebarPure CSS sm: / md: Tailwind breakpoints
Container queriesMedia queries onlyCSS container queries (@xl/page:, @md/page:) for layout-aware sizing
Dependenciescurrency.js for number formattingNative Intl.NumberFormat (no dependency)
Fonts4 families (Roboto, Roboto Condensed, Nunito, Unica One), multiple weights, cyrillic only - Google Fonts <link> in headSame 4 families via @nuxt/fonts, cyrillic + latin
Dark modeimport colors from 'tailwindcss/colors' + as any castusePreferredDark() + hardcoded neutral, handles system mode

Security

Whatfirebasecurrent
Calculator parsernew Function(expression) - code injection risk, requires unsafe-eval CSPRecursive descent parser - only +,-,*,/ on numeric literals
Input validationFirebase rules (client can't verify)Client-side Zod schemas + Postgres column types / RLS
Auth tokensFirebase-managedSupabase-managed JWT + refresh token (validated by PowerSync via JWKS)
Session lifetimeFirebase defaultSupabase Auth session with automatic token refresh
Cascade deletesN/A (Firebase)RLS-scoped delete of the user's rows (no FK constraints; PowerSync clears local data)
Data integrityNone enforced in codeTransfer ↔ categoryId='transfer', type change normalization, name uniqueness
Redirect safetyNo validationgetSafeRedirectPath() - only relative paths starting with / (not //)

Auth

firebasecurrent
ProviderFirebase AuthSupabase Auth (email/password)
Pluginnuxt-vuefire (auto-handles auth)plugins/powersync.client.ts - connects PowerSync when a session resolves
Token flowFirebase SDK internalSupabaseConnector.fetchCredentials() hands the Supabase JWT to PowerSync; validated via JWKS
OfflineFirebase SDK handles reconnectPersisted Supabase session in localStorage; the route guard reads it synchronously (useAuthSession)
Auth state hintNonePersisted session read synchronously at cold start (getPersistedSession()), no network
CallbackFirebase redirectEmail/password sign-in, no OAuth popups
CORSN/ASupabase + PowerSync service URLs from runtime config
LoginFirebase auto-redirectStores the intended redirect, signs in via useSupabaseAuth()

Sync and offline

firebasecurrent
MechanismAd-hoc helpers in trns/helpers.tsPowerSync's built-in upload queue - no hand-written offline queue
WritesScattered localStorage keysupsertRow / deleteRow write local SQLite (INSERT/UPDATE, never ON CONFLICT)
UploadsaveTrnToAddLaterLocal(), removeTrnToDeleteLaterLocal()SupabaseConnector.uploadData() drains the queue to Supabase
ReadsManual on next loaddb.watch(...) - one subscription covers initial load + realtime, local + synced
Conflict handlingNoneAuto-reconcile: rejected uploads compute divergedOps (uploadReconcile.ts) and trigger forceResync
ValidationNoneClient-side Zod schemas on form input; Postgres RLS on the server
First frameNoneBlob read-through cache (useStoreCache.ts) primes stores before PowerSync init

PWA

firebasecurrent
StrategyinjectManifest (custom service worker via SW env var)generateSW (auto-generated by Workbox)
Service workerHand-written sw.tsNone - Workbox generates it
PrecachingManual configurationAutomatic for all build assets
IconsFetched from Iconify API at runtimeBundled at build time via clientBundle

TypeScript

firebasecurrent
Strict modeEnabled, but unresolved errorsZero TypeScript errors
Type castsas any used across codebaseZero as any - replaced with type guards, @vue-ignore, narrow casts
Transfer detectiontransferCategoriesIds computed arrayTrnType.Transfer enum + discriminated union types
Runtime validationNoneZod schemas for user settings, rates, stat config, date params
Date period functionsgetStartOf(date, string), getEndOf(date, string) - default fallback to dayTyped as Period, exhaustive switch with case 'day' - new variants cause compile errors
Duration constructionDynamic { [\${period}s`]: value }- TS seesRecord<string, number>`toDuration(period, value): Duration - typed return via exhaustive switch
Stat type mappingRecord<string, TrnType[]> with ?? fallbackRecord<SeriesSlugSelected | StatTabSlug, TrnType[]> - all keys covered, no fallback needed
Average key resolutionkey as keyof TotalReturns unsafe cast + as numberTyped key: keyof TotalReturns via explicit 'netIncome' -> 'sum' mapping, no casts
Currency rate fallbackrates[code] || 1, wallet?.currency || 'USD' - falsy 0/'' triggers fallbackrates[code] ?? 1, wallet?.currency ?? 'USD' - only null/undefined triggers fallback
Query param parsingif (data.rangeOffset) - falsy 0 skippedif (data.x !== undefined) - zero values applied correctly
Total propsbaseCurrencyCode?: string loose typebaseCurrencyCode?: CurrencyCode - consistent with getAmountInRate
Category stat paramstrnsItems: Record<TrnId, { categoryId?: string }> inline typetrnsItems: Record<TrnId, Pick<TrnItem, 'categoryId'>> - linked to source type
Sort null guardsortCategoriesByAmount checks if (!a || !b) at runtimeGuard removed - TS already guarantees CategoryWithData parameters; explicit : number return type

Tests

firebasecurrent
ConfigNo vitest config2 vitest projects: unit (node), store (happy-dom)
Sync testsNoneRow<->item transforms, upload reconcile / divergence planning (uploadReconcile.test.ts)
Calculator testsBasic46 cases (precedence, decimals, division by zero, injection safety)
Extracted logic testsNoneWallet filter/grouping/counts (archived exclusion, available visibility), category grouping, auth gate, stat barUtils, useStatItem
Test infrastructureNonesetup-store.ts (shared mocks for PowerSync/Supabase)

Refactoring

Pure function extraction

Business logic moved from Vue components into testable pure functions:

  • Statistics: bucketTrnsByIntervals, computeAverageTotal, sortCategoriesByAmount, getSelectedType, computeBarStyle, formatCompactAmount, computeSeriesAverage, getTrnTypeByAmount
  • Wallets: filterWalletsByCurrency, filterWalletsByViewType, groupWalletsByProperty, computeWalletCounts, sumWalletAmounts, getCreditAvailable
  • Categories: collectCategoriesByTrns, flattenCategoriesWithValues, groupCategoriesWithValues
  • Sync: reconcile (store reconciliation against fresh watch emissions), planDivergence (upload reconcile), transforms (row<->item)
  • Demo: simplified to read-only - removed per-entity CRUD methods (addDemoCategory, deleteDemoWallet, etc.), only generateDemoData() + isDemo

Component changes

Added: ActionButton, ChipButton, TabsScroll, TextMuted, SettingsCard, TitleSection, ItemBody, BottomSheetModal, WalletsPageListItem, WalletsPageGroupHeader, Onboarding, CurrenciesPageList, CurrenciesItem, ConfigSwitch, GroupingToggle, SelectorItem

Removed: Item14, Tabs2, TabsItem1/3/4, TextSm1/2, Title3/6/7/8, TitleOption, ToggleAction, SwitcherTabs, Welcome, Round (stat chart), SectionWithCollapse, CurrenciesToggleDep, getStyles.ts

Component patterns

  • Class merging: getStyles() utility removed - replaced with cn() (clsx + tailwind-merge)
  • Form inputs: value/updateValue props -> v-model / defineModel

Composable decomposition

  • Wallets: useWalletContextMenu, useWalletDelete, useWalletsFilter, useWalletsGrouping, useWalletsCounts
  • Categories: useCategoryContextMenu
  • Statistics: Symbol-based injection keys (filterKey, statDateKey, statConfigKey) for type-safe provide/inject
  • Stat pages: useStatPage composable - extracts duplicated stat infrastructure (filter, statConfig, statDate creation + provide, activeTab, storageKey, trnsIds, maxRange) from three page components (Dashboard, Category, Wallet) into a single reusable composable. Each page passes only its entity-specific filter logic via getTrnsFilter callback. Script section reduced by 24–53% across the three pages
  • useCategoryLongPress composable: extracted 59 lines of duplicated long-press logic from Round2lines.vue, Line.vue, and Vertical.vue into a shared composable. Handles long-press to create transactions from categories with proper date selection from chart intervals
  • useStatDate encapsulation: navigation computed properties (shouldShowNav, isAtEnd, isAtStart, isDayToday, shouldShowNavHome) and direct params.value mutations moved from Navigation.vue and chart/Wrap.vue into useStatDate methods (selectInterval(), resetInterval(), setIntervalsBy(), navigate()). Navigation.vue reduced from ~54 lines of script to 5 lines
  • Duplicate stat/date/Nav.vue removed: identical copy of date/Nav.vue was deleted - only the canonical DateNav component remains
  • UiNumberStepper component: extracted duplicated ±/input counter pattern from ConfigModal.vue into a reusable UI primitive with v-model, min, max props
  • Stat component self-containment: StatAverage consolidated three conditional Amount blocks into one via computed props; StatSumWrap and StatChartWrap removed redundant prop-drilling in favor of inject; StatItem merged two modal blocks into one via state-machine pattern
  • Config caching: Section2.vue and Line.vue cache statConfig.config.value.catsList.* reads into computed properties (isItemsBg, isLines, isRoundIcon, isListGrouped, etc.) for template readability
  • Range.vue simplified: replaced manual intervalsInRange indexing with statDate.selectedInterval; extracted isIntervalSelected computed
  • useStatItem cleanup: extracted baseTrnsIdsForSelection computed to eliminate duplicated interval-selection logic; fixed unsafe type.value as keyof TotalReturns cast with type.value ?? 'sum' fallback; fixed chart average denominator using wrong data source (intervalsData -> intervalsDataWithFilteredCategories)
  • useCategoriesExpanded composable: extracted 52-line expand/collapse logic from DetailedSection.vue (286->234 lines)
  • Stat component rename: Section/Section2 -> RoundSection/DetailedSection for clarity
  • barUtils extraction: computeBarStyle() and formatCompactAmount() extracted from inline template logic into testable utilities
  • Wallet statistics consistency: archived wallets fully excluded from all counts (total, type sums, withdrawal, available) via early continue; available.isShow requires both withdrawal and credit wallets; auto-reset filter to Total when currency switch produces empty results
  • Store sync consolidation: showErrorToast/showSuccessToast/showWarningToast merged into generic showToast(type, key, params?)
  • Wallet constants: WALLET_STORAGE_KEYS extracted from inline strings to constants.ts

Semantic renaming

firebasecurrent
getTrnsIdsfilterTrnsIds
getCategoriesWithDatacomputeCategoriesWithData
getTotalOfTrnsIdscomputeTotalForTrnsIds
getChildsIdsgetChildrenIds
trnFormCreate / Edit / DuplicateopenFormForCreate / Edit / Duplicate
getIsShowSumshouldShowSum
isItTransactibleisTransactible
editedAtupdatedAt
TransferTypeTransferSide
Selector2 (categories)SelectorGrid
Selector (categories)SelectorTree
Checkbox (ui)SwitchItem
Tabs1 (ui)TabsBar
Toggle3 (ui)ToggleControlled
Section (stat)RoundSection
Section2 (stat)DetailedSection
isNotTransferCategoryshouldShowAmounts
biggestCatNumbermaxCategoryValues
addMarkAreawithMarkArea
toggleOpenedtoggleAllCategoriesExpanded

Data model

firebasecurrent
Category childrenDenormalized childIds arrayComputed dynamically from parentId
Transfer detectiontransferCategoriesIds (computed from category data)TrnType.Transfer enum value
AdjustmentNot distinguishedcategoryId === 'adjustment', excluded from income/expense stats

UX Improvements

  • Haptic feedback on transaction submit (mobile vibration)
  • Accessibility: <div> -> <button> in interactive components, i18n aria-label on icon-only buttons
  • Calculator: fixed infinite loop on zero result, proper 0. decimal display, rounded division results
  • Form: amount clears immediately after submit, popover closes before edit navigation
  • Onboarding: Welcome component replaced with Onboarding - improved layout, cookie-persisted state (finapp.isOnboarded)
  • Currencies page: two sections (Used / All) with global search, O(1) lookup via currencyMap
  • Wallet/category detail: edit/delete actions moved to three-dot menu
  • Wallet statistics: archived wallets fully excluded from all filter tabs; Available tab only shown when both withdrawal and credit wallets exist; filter auto-resets to Total on currency switch if current filter is empty
  • Wallet edit guard: redirect to /wallets when wallet doesn't exist
  • Toast dismiss: click anywhere on toast notification to close it
  • Layout: keepalive prop from layout slot for page transition persistence
  • Settings page: reorganized with SettingsCard, language dropdown, currency picker modal, danger zone with "Delete all data"
  • Login page: version display at bottom

i18n

firebasecurrent
Error messagesGeneric or nonePer-entity error keys (categories.errors.*, wallets.errors.*, trns.errors.*)
Form labelsVerbose (Category name, Category color)Concise (Name, Color, Icon)
Onboardingwelcome.* keysonboarding.* with step-by-step guidance
Themeneutral, radiusBackground color, Rounding, Appearance
TyposerrorChildserrorChildren

Code Quality

Removed from firebase

  • vue-deepunref dependency
  • Old page-specific wallet composables
  • Dead code: unused props, emits, functions, stale TODOs
  • Deprecated types: WalletsDirty, TrnItemDirty, TransferDeprecated, TrnTypeSlug
  • CSS utilities: flex-center-col, absolute-center, layoutBase
  • nuxt-vuefire and firebase dependencies
  • Firebase config files (firebase.json, .firebaserc, pnpm-workspace.yaml)

Fixed

  • Memory leaks: addEventListener without cleanup, ResizeObserver without disconnect
  • Null spreading in 3 stores (missing ?? {} guards)
  • Emit during render in SelectionCategoriesFast
  • Magic numbers replaced with TrnType enum (8 places)
  • ref<any> replaced with proper types (4 files)
  • Error layout localized (hardcoded English -> i18n keys)
  • Tailwind v4 CSS syntax: [var(--name)] -> (--name) functional notation
  • Radius uses ?? (not ||) so 0 is valid: radius ?? 0.375
  • CSS utility typo: bottomSheetDrugClassesCustom -> bottomSheetDragClassesCustom
  • ESLint: layout vue/no-multiple-template-root exception
  • createLogger(prefix) - dev-only log, always-on warn and error, with [prefix] formatting across all modules
  • Hardcoded 'ru' locale in chart formatters -> dynamic locale from settings
  • Statistics.vue empty group sections hidden with v-show
  • Selector.vue onSelectRange(value: any) -> typed { end: unknown, start: unknown }
  • setWalletViewType type fixed from WalletType | 'total' to WalletViewTypes | 'total'
  • Version bumped from v6.6.4 (the firebase branch baseline)
  • useAmount.getAmountInBaseRate: dead noFormat param removed, unnecessary + unary on number removed
  • useFilter: redundant [...array.filter()] and [...string.split()] spreads removed - both methods already return new arrays
  • getUCalendarTimedDate: new Date(string) parsed as UTC -> date.toDate(getLocalTimeZone()) for correct local timezone
  • useGetDateRange: type: 'start' | 'end' pattern removed - format functions return full strings directly, 3 IIFE switches replaced with isSamePeriod helper
  • getIntervalsInRange: rangeOffset made optional in IntervalsInRangeProps - not used by getIntervalsInRange, only by calculateIntervalInRange
  • compareCategoriesByParentAndName: || -> ?? for empty string fallback (consistent nullish semantics)
  • useCategoriesStore: removed 5 unnecessary ?? {} defensive checks - items is always initialized via shallowRef
  • useCategoryLongPress: removed redundant ! after optional chaining (value?.start already narrowed)
  • useCategoriesExpanded: forEach -> for...of; reduce with as cast -> Object.fromEntries
  • sortCategoriesByAmount: renamed isP/isN -> bothPositive/bothNegative for readability; removed redundant categories ??= [] (already initialized in grouping)

Dependencies

(What changed from the firebase branch to the current app. For the full list see the project package.json.)

Removed (firebase era)Added (current)
firebase@powersync/web
nuxt-vuefire@supabase/supabase-js
vue-deepunref@vueuse/nuxt
currency.jszod
es-toolkit