[{"data":1,"prerenderedAt":1268},["ShallowReactive",2],{"navigation_docs_en":3,"-en-reference-architecture":191,"-en-reference-architecture-surround":1263},[4,61,127,171],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":60},"Guide","i-lucide-book-open","\u002Fen\u002Fguide","en\u002F1.guide",[10,15,20,25,30,35,40,45,50,55],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fen\u002Fguide\u002Fintroduction","en\u002F1.guide\u002F01.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Install the App","\u002Fen\u002Fguide\u002Finstallation","en\u002F1.guide\u002F02.installation","i-lucide-smartphone",{"title":21,"path":22,"stem":23,"icon":24},"Authentication","\u002Fen\u002Fguide\u002Fauth","en\u002F1.guide\u002F03.auth","i-lucide-lock",{"title":26,"path":27,"stem":28,"icon":29},"Wallets","\u002Fen\u002Fguide\u002Fwallets","en\u002F1.guide\u002F04.wallets","i-lucide-wallet",{"title":31,"path":32,"stem":33,"icon":34},"Categories","\u002Fen\u002Fguide\u002Fcategories","en\u002F1.guide\u002F05.categories","i-lucide-tags",{"title":36,"path":37,"stem":38,"icon":39},"Transactions","\u002Fen\u002Fguide\u002Ftransactions","en\u002F1.guide\u002F06.transactions","i-lucide-receipt",{"title":41,"path":42,"stem":43,"icon":44},"Transfers","\u002Fen\u002Fguide\u002Ftransfers","en\u002F1.guide\u002F07.transfers","i-lucide-arrow-left-right",{"title":46,"path":47,"stem":48,"icon":49},"Statistics","\u002Fen\u002Fguide\u002Fstatistics","en\u002F1.guide\u002F08.statistics","i-lucide-bar-chart-3",{"title":51,"path":52,"stem":53,"icon":54},"Theme","\u002Fen\u002Fguide\u002Ftheme","en\u002F1.guide\u002F09.theme","i-lucide-palette",{"title":56,"path":57,"stem":58,"icon":59},"Settings","\u002Fen\u002Fguide\u002Fsettings","en\u002F1.guide\u002F10.settings","i-lucide-settings",false,{"title":62,"icon":63,"path":64,"stem":65,"children":66,"page":60},"Development","i-lucide-code","\u002Fen\u002Fdevelopment","en\u002F2.development",[67,72,77,82,87,92,97,102,122],{"title":68,"path":69,"stem":70,"icon":71},"Installation","\u002Fen\u002Fdevelopment\u002Finstallation","en\u002F2.development\u002F01.installation","i-lucide-download",{"title":73,"path":74,"stem":75,"icon":76},"Codebase Graph","\u002Fen\u002Fdevelopment\u002Funderstand-anything","en\u002F2.development\u002F02.understand-anything","i-lucide-network",{"title":78,"path":79,"stem":80,"icon":81},"Offline & PWA","\u002Fen\u002Fdevelopment\u002Foffline","en\u002F2.development\u002F03.offline","i-lucide-wifi-off",{"title":83,"path":84,"stem":85,"icon":86},"Data Migration History","\u002Fen\u002Fdevelopment\u002Fmigration","en\u002F2.development\u002F04.migration","i-lucide-database",{"title":88,"path":89,"stem":90,"icon":91},"Deployment","\u002Fen\u002Fdevelopment\u002Fdeployment","en\u002F2.development\u002F05.deployment","i-lucide-rocket",{"title":93,"path":94,"stem":95,"icon":96},"Testing","\u002Fen\u002Fdevelopment\u002Ftesting","en\u002F2.development\u002F06.testing","i-lucide-flask-conical",{"title":98,"path":99,"stem":100,"icon":101},"Date Utilities","\u002Fen\u002Fdevelopment\u002Fdate-utilities","en\u002F2.development\u002F07.date-utilities","i-lucide-calendar",{"title":103,"path":104,"stem":105,"children":106,"page":60},"Ai Workflow","\u002Fen\u002Fdevelopment\u002Fai-workflow","en\u002F2.development\u002F08.ai-workflow",[107,112,117],{"title":108,"path":109,"stem":110,"icon":111},"Overview","\u002Fen\u002Fdevelopment\u002Fai-workflow\u002Foverview","en\u002F2.development\u002F08.ai-workflow\u002F01.overview","i-lucide-bot",{"title":113,"path":114,"stem":115,"icon":116},"Agents","\u002Fen\u002Fdevelopment\u002Fai-workflow\u002Fagents","en\u002F2.development\u002F08.ai-workflow\u002F02.agents","i-lucide-users",{"title":118,"path":119,"stem":120,"icon":121},"Skills","\u002Fen\u002Fdevelopment\u002Fai-workflow\u002Fskills","en\u002F2.development\u002F08.ai-workflow\u002F03.skills","i-lucide-lightbulb",{"title":123,"path":124,"stem":125,"icon":126},"Troubleshooting","\u002Fen\u002Fdevelopment\u002Ftroubleshooting","en\u002F2.development\u002F09.troubleshooting","i-lucide-life-buoy",{"title":128,"icon":129,"path":130,"stem":131,"children":132,"page":60},"Reference","i-lucide-file-code","\u002Fen\u002Freference","en\u002F3.reference",[133,138,142,147,152,156,161,166],{"title":134,"path":135,"stem":136,"icon":137},"Architecture","\u002Fen\u002Freference\u002Farchitecture","en\u002F3.reference\u002F01.architecture","i-lucide-boxes",{"title":139,"path":140,"stem":141,"icon":44},"Transaction Types","\u002Fen\u002Freference\u002Ftransaction-types","en\u002F3.reference\u002F02.transaction-types",{"title":143,"path":144,"stem":145,"icon":146},"Sync","\u002Fen\u002Freference\u002Fsync","en\u002F3.reference\u002F03.sync","i-lucide-refresh-cw",{"title":148,"path":149,"stem":150,"icon":151},"Offline-first","\u002Fen\u002Freference\u002Foffline-first","en\u002F3.reference\u002F04.offline-first","i-lucide-list-ordered",{"title":153,"path":154,"stem":155,"icon":121},"Technical Decisions","\u002Fen\u002Freference\u002Ftech-decisions","en\u002F3.reference\u002F05.tech-decisions",{"title":157,"path":158,"stem":159,"icon":160},"Validation Strategy","\u002Fen\u002Freference\u002Fvalidation-strategy","en\u002F3.reference\u002F06.validation-strategy","i-lucide-shield-check",{"title":162,"path":163,"stem":164,"icon":165},"What Changed Since Firebase","\u002Fen\u002Freference\u002Ffirebase-migration","en\u002F3.reference\u002F07.firebase-migration","i-lucide-hamburger",{"title":167,"path":168,"stem":169,"icon":170},"Performance","\u002Fen\u002Freference\u002Fperformance","en\u002F3.reference\u002F08.performance","i-lucide-gauge",{"title":172,"icon":173,"path":174,"stem":175,"children":176,"page":60},"Premium","i-lucide-star","\u002Fen\u002Fpremium","en\u002F4.premium",[177,181,186],{"title":108,"path":178,"stem":179,"icon":180},"\u002Fen\u002Fpremium\u002Foverview","en\u002F4.premium\u002F01.overview","i-lucide-layers",{"title":182,"path":183,"stem":184,"icon":185},"Telegram Bot","\u002Fen\u002Fpremium\u002Ftelegram-bot","en\u002F4.premium\u002F02.telegram-bot","i-lucide-send",{"title":187,"path":188,"stem":189,"icon":190},"AI Chat","\u002Fen\u002Fpremium\u002Fai-chat","en\u002F4.premium\u002F03.ai-chat","i-lucide-sparkles",{"id":192,"title":134,"body":193,"description":1255,"extension":1256,"links":1257,"meta":1258,"navigation":1259,"path":135,"seo":1260,"stem":136,"__hash__":1262},"docs_en\u002Fen\u002F3.reference\u002F01.architecture.md",{"type":194,"value":195,"toc":1225},"minimark",[196,201,212,220,225,228,308,312,318,325,377,381,400,406,410,416,429,433,439,464,468,481,485,500,504,508,514,574,602,606,609,658,662,682,686,700,727,731,745,749,755,760,764,814,818,862,866,892,896,921,925,932,936,947,951,1204,1208],[197,198,200],"h2",{"id":199},"app-initialization","App Initialization",[202,203,208],"pre",{"className":204,"code":206,"language":207},[205],"language-text","app starts\n├── Plugins\n│   ├── theme.ts (enforce: 'post')\n│   │   └── Read theme from localStorage → update appConfig\n│   └── powersync.client.ts\n│       ├── navigator.storage.persist() - protect IndexedDB from eviction\n│       ├── persisted session? → eager getPowerSyncDb().init() (overlaps app boot)\n│       ├── setUploadErrorHandler → auto-reconcile fatal upload rejections\n│       └── watch uid (immediate)\n│           ├── uid present → connectPowerSync(...)\n│           └── auth resolved, no uid → disconnectPowerSync()\n├── app.vue\n│   ├── useHead() - theme CSS vars (tagPriority: -2)\n│   ├── useGuard() - auth redirect logic\n│   └── render layout + page\n└── useInitApp().initApp() (default layout, via useAsyncData)\n    ├── primeStoresFromCache() - per-user localforage snapshot → instant first paint\n    ├── startWatches() - 5 watchTable subscriptions hydrate the stores\n    │   ├── useCurrenciesStore  - watchTable('SELECT * FROM rates', ...)\n    │   ├── useUserStore        - watchTable('SELECT * FROM user_settings', ...)\n    │   ├── useWalletsStore     - watchTable('SELECT * FROM wallets', ...)\n    │   ├── useCategoriesStore  - watchTable('SELECT * FROM categories', ...)\n    │   └── useTrnsStore        - watchTable('SELECT * FROM trns', ...)\n    └── awaitInitialSync() - first server sync settles in the background (online only)\n","text",[209,210,206],"code",{"__ignoreMap":211},"",[213,214,215,216,219],"p",{},"Each ",[209,217,218],{},"watchTable"," fires immediately with current local SQLite rows and again on every change (local write or incoming sync). There is no separate init + subscribe step.",[221,222,224],"h3",{"id":223},"cache-first-cold-start","Cache-First Cold Start",[213,226,227],{},"The boot is cache-first - there is no splash screen:",[229,230,231,251,262,283],"ul",{},[232,233,234,238,239,242,243,246,247,250],"li",{},[235,236,237],"strong",{},"Blob cache"," (",[209,240,241],{},"app\u002Fapp\u002Fcomposables\u002FuseStoreCache.ts","): one per-user localforage blob (",[209,244,245],{},"finapp.cache.\u003Cuid>",") holding the last session's store snapshot. It is read once at boot and primes all five stores before PowerSync's ",[209,248,249],{},"db.init"," + first SQLite scans finish. Writes are debounced (400 ms) read-modify-writes of touched slices. SQLite stays the source of truth; the blob is only a read cache.",[232,252,253,256,257,261],{},[235,254,255],{},"Reconcile to truth",": the watches overwrite primed data with SQLite rows. An empty ",[258,259,260],"em",{},"first"," emission keeps the primed cache (the first sync just hasn't landed yet on a fresh device); any later empty emission is a genuine wipe and applies.",[232,263,264,238,267,270,271,274,275,278,279,282],{},[235,265,266],{},"Boot state",[209,268,269],{},"useInitApp.ts","): a single ",[209,272,273],{},"bootState"," computed - ",[209,276,277],{},"'ready' | 'onboarding' | 'error'",". The dashboard shows a space-reserving skeleton (",[209,280,281],{},"StatPageSkeleton",") while stores hydrate; onboarding only renders once the first sync genuinely completed and the account is empty; the error screen (with retry) only when the sync failed and there is no local data to fall back to.",[232,284,285,288,289,292,293,296,297,296,300,303,304,307],{},[235,286,287],{},"Instrumentation",": cold-start ",[209,290,291],{},"performance.measure"," marks - ",[209,294,295],{},"cache:prime",", ",[209,298,299],{},"ps:watch:\u003Ctable>",[209,301,302],{},"trns:first-transform",". See ",[305,306,167],"a",{"href":168},".",[197,309,311],{"id":310},"project-structure","Project Structure",[202,313,316],{"className":314,"code":315,"language":207},[205],"app\u002F                        # @finapp\u002Fapp - Nuxt source, Supabase config, PowerSync config\n  app\u002F                      # Nuxt app root (components, composables, middleware, pages, plugins)\n  services\u002Fpowersync\u002F       # Client data layer: schema, db singleton, connector, transforms, mutations\n  supabase\u002F                 # Supabase config: migrations, RLS policies, powersync_setup.sql\n  powersync\u002F                # Self-hosted PowerSync service: docker-compose, config\u002F\ndocs\u002F                       # @finapp\u002Fdocs - documentation site\ni18n\u002Flocales\u002F               # en-US.js, ru-RU.js\n",[209,317,315],{"__ignoreMap":211},[213,319,320,321,324],{},"Conventions worth knowing (not obvious from ",[209,322,323],{},"tree","):",[229,326,327,357,368],{},[232,328,329,332,333,336,337,340,341,344,345,348,349,352,353,356],{},[209,330,331],{},"services\u002Fpowersync\u002F"," is the client data layer: SQLite schema (",[209,334,335],{},"AppSchema.ts","), the ",[209,338,339],{},"PowerSyncDatabase"," singleton (",[209,342,343],{},"db.ts","), upload connector (",[209,346,347],{},"connector.ts","), row\u002Fitem converters (",[209,350,351],{},"transforms.ts","), and write helpers (",[209,354,355],{},"mutations.ts",").",[232,358,359,360,363,364,367],{},"Each feature under ",[209,361,362],{},"app\u002Fapp\u002Fcomponents\u002F\u003Cfeature>\u002F"," owns its Pinia store, form, list, and types - there is no separate ",[209,365,366],{},"stores\u002F"," directory.",[232,369,370,373,374,307],{},[209,371,372],{},"supabase\u002Fmigrations\u002F"," contains all Postgres schema changes. Never edit manually - use ",[209,375,376],{},"supabase migration new",[197,378,380],{"id":379},"store-pattern","Store Pattern",[213,382,383,384,296,387,296,390,296,393,296,396,399],{},"All Pinia stores (",[209,385,386],{},"useTrnsStore",[209,388,389],{},"useWalletsStore",[209,391,392],{},"useCategoriesStore",[209,394,395],{},"useUserStore",[209,397,398],{},"useCurrenciesStore",") follow the same pattern:",[202,401,404],{"className":402,"code":403,"language":207},[205],"items: shallowRef\u003CRecord\u003Cid, item> | null>   # reactive state (shallow for performance)\nwatchTable subscription   # fires on init + every local write or incoming sync\nsave({ id, values })      # optimistic write → upsertRow → rollback on error\ndelete(id)                # optimistic write → deleteRow → rollback on error\n",[209,405,403],{"__ignoreMap":211},[221,407,409],{"id":408},"data-flow-write","Data Flow (write)",[202,411,414],{"className":412,"code":413,"language":207},[205],"user action\n  → save({ id, values })\n    → prev = snapshot of current items\n    → optimistic: items.value = { ...items, [id]: values }\n    → await upsertRow(table, id, row)\n      → success: watchTable fires → store refreshes from SQLite\n      → failure: items.value = prev + showErrorToast\n",[209,415,413],{"__ignoreMap":211},[213,417,418,419,422,423,426,427,307],{},"Write failure rollback handles local SQLite errors. Server-side rejection (RLS, constraint) is handled separately by the upload-error handler, which auto-reconciles: rejected INSERTs are reverted locally (",[209,420,421],{},"sync.errors.uploadReverted","), rejected UPDATE\u002FDELETE offers a full re-sync (",[209,424,425],{},"sync.errors.uploadDiverged","). See ",[305,428,148],{"href":149},[221,430,432],{"id":431},"data-flow-watchtable","Data Flow (watchTable)",[202,434,437],{"className":435,"code":436,"language":207},[205],"watchTable('SELECT * FROM trns', [], onRows, throttleMs)\n  → fires immediately with current local rows\n  → fires again on every table change (local write OR incoming PowerSync sync)\n  → onRows: rows → transform via rowToTrn() → items.value = new object map\n",[209,438,436],{"__ignoreMap":211},[213,440,441,443,444,447,448,451,452,455,456,459,460,463],{},[209,442,386],{}," uses ",[209,445,446],{},"reconcileTrns(prev, rows)"," inside ",[209,449,450],{},"onRows",": returns the same ",[209,453,454],{},"prev"," ref when nothing changed (suppresses the echo of the store's own optimistic write), and reuses unchanged row objects by ",[209,457,458],{},"updatedAt"," so ",[209,461,462],{},"rowToTrn()"," only runs for changed rows.",[221,465,467],{"id":466},"cascade-delete","Cascade Delete",[213,469,470,473,474,477,478,480],{},[209,471,472],{},"deleteWallet"," and ",[209,475,476],{},"deleteCategory"," also delete the entity's trns from the local SQLite DB (otherwise the ",[209,479,218],{}," subscription would re-add them after the parent is removed). Rollback on failure restores both the entity and its trns.",[221,482,484],{"id":483},"demo-mode","Demo Mode",[213,486,487,488,491,492,495,496,499],{},"Demo mode bypasses PowerSync entirely. Stores check ",[209,489,490],{},"isDemo"," and skip all PowerSync calls. In-memory data + localforage persistence is used instead. Demo data (1000 trns, 18 categories, 6 wallets) is generated in ",[209,493,494],{},"useDemo.ts",". Controlled by the ",[209,497,498],{},"finapp.isDemo"," cookie.",[197,501,503],{"id":502},"auth","Auth",[221,505,507],{"id":506},"supabase-auth","Supabase Auth",[213,509,510,513],{},[209,511,512],{},"app\u002Fapp\u002Fcomposables\u002FuseSupabase.ts"," provides:",[229,515,516,532,564],{},[232,517,518,519,522,523,296,526,296,529,356],{},"A singleton ",[209,520,521],{},"supabase-js"," client (",[209,524,525],{},"autoRefreshToken",[209,527,528],{},"persistSession",[209,530,531],{},"detectSessionInUrl: true",[232,533,534,537,538,296,541,296,544,296,547,550,551,554,555,554,558,554,561,307],{},[209,535,536],{},"useSupabaseAuth()"," composable exposing reactive ",[209,539,540],{},"session",[209,542,543],{},"uid",[209,545,546],{},"user",[209,548,549],{},"isAuthReady",", plus ",[209,552,553],{},"signInWithPassword"," \u002F ",[209,556,557],{},"signUp",[209,559,560],{},"signInWithGoogle",[209,562,563],{},"signOut",[232,565,566,567,570,571,573],{},"Session persists in localStorage and auto-refreshes. ",[209,568,569],{},"onAuthStateChange"," keeps a module-level reactive ",[209,572,540],{}," ref in sync.",[213,575,576,577,473,580,238,583,586,587,589,590,593,594,597,598,601],{},"Auth methods: ",[235,578,579],{},"email\u002Fpassword",[235,581,582],{},"Sign in with Google",[209,584,585],{},"signInWithOAuth",", PKCE). ",[209,588,531],{}," lets supabase-js exchange the ",[209,591,592],{},"?code="," on the OAuth return: ",[209,595,596],{},"login.vue"," sends Google back to ",[209,599,600],{},"\u002Flogin",", then navigates to the original destination once the session lands. Google sessions are ordinary Supabase JWTs, so PowerSync (JWKS validation) and the rest of the auth wiring are unchanged.",[221,603,605],{"id":604},"auth-guard","Auth Guard",[213,607,608],{},"Auth uses two layers:",[610,611,612,646],"ol",{},[232,613,614,238,617,620,621,296,624,627,628,630,631,634,635,637,638,641,642,645],{},[235,615,616],{},"Route middleware",[209,618,619],{},"app\u002Fapp\u002Fmiddleware\u002Fauth.global.ts","): synchronous gate on the persisted Supabase session in localStorage (",[209,622,623],{},"hasPersistedSession()",[209,625,626],{},"app\u002Fapp\u002Fcomposables\u002FuseAuthSession.ts","), no network call, works offline. Redirects to ",[209,629,600],{}," (preserving ",[209,632,633],{},"?redirect=",") when absent; bounces ",[209,636,600],{}," → ",[209,639,640],{},"\u002Fdashboard"," when a session is present. Demo mode bypasses this check. On the Google OAuth return the session is not persisted yet, so ",[209,643,644],{},"\u002Flogin?...&code="," is left in place (no bounce) while the code is exchanged.",[232,647,648,238,651,654,655,657],{},[235,649,650],{},"PowerSync plugin",[209,652,653],{},"app\u002Fapp\u002Fplugins\u002Fpowersync.client.ts","): watches the Supabase ",[209,656,543],{}," and connects\u002Fdisconnects PowerSync as the session resolves or clears. The gate reads localStorage directly, so there is no auth cookie to write.",[221,659,661],{"id":660},"redirect-protection","Redirect Protection",[213,663,664,238,667,670,671,673,674,677,678,681],{},[209,665,666],{},"getSafeRedirectPath()",[209,668,669],{},"app\u002Futils\u002Fredirect.ts",") validates ",[209,672,633],{}," query parameters - only relative paths starting with ",[209,675,676],{},"\u002F"," (not ",[209,679,680],{},"\u002F\u002F",") are allowed, preventing open redirect attacks.",[221,683,685],{"id":684},"guard-logic","Guard Logic",[213,687,688,691,692,695,696,699],{},[209,689,690],{},"useGuard()"," in ",[209,693,694],{},"app.vue"," watches ",[209,697,698],{},"currentUser",":",[229,701,702,714,721],{},[232,703,704,705,707,708,710,711,713],{},"Logged in + on ",[209,706,600],{}," → redirect to ",[209,709,640],{}," (or ",[209,712,633],{}," path)",[232,715,716,717,707,719],{},"Not logged in + not on ",[209,718,600],{},[209,720,600],{},[232,722,723,726],{},[209,724,725],{},"isSigningOut"," flag prevents redirect loop during sign-out",[197,728,730],{"id":729},"cross-user-local-db-protection","Cross-User Local DB Protection",[213,732,733,736,737,740,741,744],{},[209,734,735],{},"connectPowerSync(client, powerSyncUrl, userId)"," checks a persisted owner marker (",[209,738,739],{},"finapp.psDbOwnerUid"," in localStorage). If the local SQLite belongs to a different user, the database is ",[235,742,743],{},"wiped before connecting"," (fail-closed). This prevents data leaks when a different account logs in on the same device.",[197,746,748],{"id":747},"sign-out-flow","Sign-Out Flow",[202,750,753],{"className":751,"code":752,"language":207},[205],"signOut()\n  → isSigningOut = true (prevents auth guard redirect loop)\n  → supabase.auth.signOut()\n  → disconnectPowerSync() - disconnectAndClear() wipes local SQLite + clears owner marker\n  → window.location.href = '\u002Flogin' (hard navigation, destroys all JS state)\n",[209,754,752],{"__ignoreMap":211},[213,756,757,758,307],{},"Hard navigation instead of Vue Router - see ",[305,759,153],{"href":154},[197,761,763],{"id":762},"exchange-rates","Exchange Rates",[229,765,766,784,794,805],{},[232,767,768,771,772,775,776,779,780,783],{},[235,769,770],{},"Source:"," Coinbase (base, fiat + crypto) + OpenExchangeRates (fiat overlay), merged into one daily ",[209,773,774],{},"source='merged'"," row by the ",[209,777,778],{},"fetch-rates"," edge function (",[209,781,782],{},"pg_cron",", 06:00 UTC)",[232,785,786,789,790,793],{},[235,787,788],{},"Storage:"," ",[209,791,792],{},"rates"," table in Postgres; global PowerSync stream syncs all rows to every authenticated user",[232,795,796,789,799,801,802],{},[235,797,798],{},"Frontend:",[209,800,398],{}," subscribes via ",[209,803,804],{},"watchTable('SELECT * FROM rates', ...)",[232,806,807,789,810,813],{},[235,808,809],{},"Usage:",[209,811,812],{},"getAmountInRate()"," converts amounts to base currency for statistics",[197,815,817],{"id":816},"pwa","PWA",[229,819,820,831,842,848,851,856],{},[232,821,822,823,826,827,830],{},"Strategy: ",[209,824,825],{},"generateSW"," from ",[209,828,829],{},"@vite-pwa\u002Fnuxt"," (no custom service worker)",[232,832,833,834,837,838,841],{},"Precaching: build assets plus exactly one WASM file - ",[209,835,836],{},"wa-sqlite-async.\u003Chash>.wasm",", the only variant the worker loads (the other emitted variants are cipher\u002Fsync builds the app never uses); ",[209,839,840],{},"maximumFileSizeToCacheInBytes: 3 MiB"," so it isn't dropped by the default 2 MiB cap",[232,843,844,847],{},[209,845,846],{},"navigateFallback: '\u002F'"," serves SPA shell for all navigation (offline support)",[232,849,850],{},"Runtime caching: Google Fonts, Iconify icons (CacheFirst)",[232,852,853,854],{},"Start URL: ",[209,855,640],{},[232,857,858,859],{},"Manifest: ",[209,860,861],{},"display: 'standalone'",[197,863,865],{"id":864},"logging","Logging",[213,867,868,238,871,874,875,878,879,296,882,296,885,888,889,307],{},[209,869,870],{},"createLogger(prefix)",[209,872,873],{},"app\u002Futils\u002Flogger.ts",") provides dev-only logging. In production, only ",[209,876,877],{},".error()"," fires. All store operations and auth flows are instrumented with prefixed logs (",[209,880,881],{},"[wallets]",[209,883,884],{},"[trns]",[209,886,887],{},"[auth\u002Fmiddleware]",", etc.). Check the browser console during ",[209,890,891],{},"pnpm dev",[197,893,895],{"id":894},"modal-state","Modal State",[213,897,898,899,902,903,906,907,910,911,691,913,916,917,920],{},"Modals use local ",[209,900,901],{},"ref","s and ",[209,904,905],{},"v-if"," + ",[209,908,909],{},"@close"," emit pattern. Menu visibility is a module-level ",[209,912,901],{},[209,914,915],{},"useMenuData.ts"," (shared across callers). No global modal registry - modals unmount automatically when parent components unmount (e.g., on sign-out, stores reset to ",[209,918,919],{},"null"," → layout condition becomes false → modals disappear).",[197,922,924],{"id":923},"sidebar-persistence","Sidebar Persistence",[213,926,927,928,931],{},"The desktop sidebar show\u002Fhide state is stored in a cookie (",[209,929,930],{},"finapp.isShowSidebar","), providing persistent state across page loads.",[197,933,935],{"id":934},"bottom-sheet","Bottom Sheet",[213,937,938,939,942,943,946],{},"The mobile transaction form uses a custom ",[209,940,941],{},"BottomSheet"," component with ",[209,944,945],{},"useBottomSheetDrag"," - a hand-written drag implementation supporting touch and mouse. Features: configurable start-closing offset, direction detection, overlay opacity fade, scroll-aware behavior (ignores drag when content is scrolled), sort handle exclusion.",[197,948,950],{"id":949},"key-files-quick-reference","Key Files Quick Reference",[952,953,954,967],"table",{},[955,956,957],"thead",{},[958,959,960,964],"tr",{},[961,962,963],"th",{},"What",[961,965,966],{},"Where",[968,969,970,981,993,1001,1010,1019,1029,1038,1047,1057,1067,1077,1087,1097,1107,1117,1127,1137,1146,1156,1164,1174,1184,1194],"tbody",{},[958,971,972,976],{},[973,974,975],"td",{},"App entry",[973,977,978],{},[209,979,980],{},"app\u002Fapp\u002Fapp.vue",[958,982,983,986],{},[973,984,985],{},"Theme logic",[973,987,988,296,991],{},[209,989,990],{},"app\u002Fapp\u002Fplugins\u002Ftheme.ts",[209,992,980],{},[958,994,995,997],{},[973,996,650],{},[973,998,999],{},[209,1000,653],{},[958,1002,1003,1006],{},[973,1004,1005],{},"Supabase client + auth",[973,1007,1008],{},[209,1009,512],{},[958,1011,1012,1015],{},[973,1013,1014],{},"Synchronous auth gate",[973,1016,1017],{},[209,1018,626],{},[958,1020,1021,1024],{},[973,1022,1023],{},"Boot state \u002F init flow",[973,1025,1026],{},[209,1027,1028],{},"app\u002Fapp\u002Fcomponents\u002Fapp\u002FuseInitApp.ts",[958,1030,1031,1034],{},[973,1032,1033],{},"Cold-start blob cache",[973,1035,1036],{},[209,1037,241],{},[958,1039,1040,1043],{},[973,1041,1042],{},"Auth guard (middleware)",[973,1044,1045],{},[209,1046,619],{},[958,1048,1049,1052],{},[973,1050,1051],{},"Guard logic (app.vue)",[973,1053,1054],{},[209,1055,1056],{},"app\u002Fapp\u002Fcomponents\u002Fuser\u002FuseGuard.ts",[958,1058,1059,1062],{},[973,1060,1061],{},"Store sync utils",[973,1063,1064],{},[209,1065,1066],{},"app\u002Fapp\u002Fcomposables\u002FuseStoreSync.ts",[958,1068,1069,1072],{},[973,1070,1071],{},"PowerSync db singleton",[973,1073,1074],{},[209,1075,1076],{},"app\u002Fservices\u002Fpowersync\u002Fdb.ts",[958,1078,1079,1082],{},[973,1080,1081],{},"SQLite schema",[973,1083,1084],{},[209,1085,1086],{},"app\u002Fservices\u002Fpowersync\u002FAppSchema.ts",[958,1088,1089,1092],{},[973,1090,1091],{},"Upload connector",[973,1093,1094],{},[209,1095,1096],{},"app\u002Fservices\u002Fpowersync\u002Fconnector.ts",[958,1098,1099,1102],{},[973,1100,1101],{},"Row\u002Fitem transforms",[973,1103,1104],{},[209,1105,1106],{},"app\u002Fservices\u002Fpowersync\u002Ftransforms.ts",[958,1108,1109,1112],{},[973,1110,1111],{},"Write helpers",[973,1113,1114],{},[209,1115,1116],{},"app\u002Fservices\u002Fpowersync\u002Fmutations.ts",[958,1118,1119,1122],{},[973,1120,1121],{},"Echo suppression",[973,1123,1124],{},[209,1125,1126],{},"app\u002Fapp\u002Fcomponents\u002Ftrns\u002Freconcile.ts",[958,1128,1129,1132],{},[973,1130,1131],{},"Statistics calc",[973,1133,1134],{},[209,1135,1136],{},"app\u002Fapp\u002Fcomponents\u002Famount\u002FgetTotal.ts",[958,1138,1139,1142],{},[973,1140,1141],{},"Redirect protection",[973,1143,1144],{},[209,1145,669],{},[958,1147,1148,1151],{},[973,1149,1150],{},"Date utils",[973,1152,1153],{},[209,1154,1155],{},"app\u002Fapp\u002Fcomponents\u002Fdate\u002Futils.ts",[958,1157,1158,1160],{},[973,1159,865],{},[973,1161,1162],{},[209,1163,873],{},[958,1165,1166,1169],{},[973,1167,1168],{},"Menu state",[973,1170,1171],{},[209,1172,1173],{},"app\u002Fapp\u002Fcomponents\u002Flayout\u002FuseMenuData.ts",[958,1175,1176,1179],{},[973,1177,1178],{},"Bottom sheet drag",[973,1180,1181],{},[209,1182,1183],{},"app\u002Fapp\u002Fcomponents\u002FbottomSheet\u002FuseBottomSheetDrag.ts",[958,1185,1186,1189],{},[973,1187,1188],{},"Supabase migrations",[973,1190,1191],{},[209,1192,1193],{},"app\u002Fsupabase\u002Fmigrations\u002F",[958,1195,1196,1199],{},[973,1197,1198],{},"PowerSync sync rules",[973,1200,1201],{},[209,1202,1203],{},"app\u002Fpowersync\u002Fconfig\u002Fsync-config.yaml",[197,1205,1207],{"id":1206},"next-steps","Next Steps",[229,1209,1210,1215,1220],{},[232,1211,1212,1214],{},[305,1213,143],{"href":144}," - how PowerSync syncs data between server and local SQLite",[232,1216,1217,1219],{},[305,1218,148],{"href":149}," - offline writes, upload queue, and error handling",[232,1221,1222,1224],{},[305,1223,167],{"href":168}," - cold-start budget, lazy chunks, and instrumentation",{"title":211,"searchDepth":1226,"depth":1226,"links":1227},2,[1228,1232,1233,1239,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254],{"id":199,"depth":1226,"text":200,"children":1229},[1230],{"id":223,"depth":1231,"text":224},3,{"id":310,"depth":1226,"text":311},{"id":379,"depth":1226,"text":380,"children":1234},[1235,1236,1237,1238],{"id":408,"depth":1231,"text":409},{"id":431,"depth":1231,"text":432},{"id":466,"depth":1231,"text":467},{"id":483,"depth":1231,"text":484},{"id":502,"depth":1226,"text":503,"children":1240},[1241,1242,1243,1244],{"id":506,"depth":1231,"text":507},{"id":604,"depth":1231,"text":605},{"id":660,"depth":1231,"text":661},{"id":684,"depth":1231,"text":685},{"id":729,"depth":1226,"text":730},{"id":747,"depth":1226,"text":748},{"id":762,"depth":1226,"text":763},{"id":816,"depth":1226,"text":817},{"id":864,"depth":1226,"text":865},{"id":894,"depth":1226,"text":895},{"id":923,"depth":1226,"text":924},{"id":934,"depth":1226,"text":935},{"id":949,"depth":1226,"text":950},{"id":1206,"depth":1226,"text":1207},"Initialization flow, project structure, store pattern, auth - Supabase + PowerSync.","md",null,{},{"icon":137},{"title":134,"description":1261},"Finapp architecture deep-dive - app initialization, project structure, Pinia store pattern, Supabase Auth, PowerSync offline-first, and PWA setup.","wyXB-iwmqwRZdeAa9TI0DVvPbxDc8QIokBAPzmsAHQ4",[1264,1266],{"title":123,"path":124,"stem":125,"description":1265,"icon":126,"children":-1},"Known gotchas for the local Supabase+PowerSync dev setup and offline-first data layer.",{"title":139,"path":140,"stem":141,"description":1267,"icon":44,"children":-1},"Transaction types, adjustment logic, statistics.",1782114343848]