[{"data":1,"prerenderedAt":666},["ShallowReactive",2],{"navigation_docs_en":3,"-en-reference-offline-first":191,"-en-reference-offline-first-surround":661},[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":148,"body":193,"description":653,"extension":654,"links":655,"meta":656,"navigation":657,"path":149,"seo":658,"stem":150,"__hash__":660},"docs_en\u002Fen\u002F3.reference\u002F04.offline-first.md",{"type":194,"value":195,"toc":637},"minimark",[196,200,205,216,219,223,226,242,245,249,265,349,353,359,364,390,414,418,425,429,447,450,454,465,469,472,479,483,620,624],[197,198,199],"p",{},"Finapp is offline-first via PowerSync. Local SQLite is the single source of truth. All reads come from SQLite; all writes go to SQLite first and are queued for upload to Supabase Postgres.",[201,202,204],"h2",{"id":203},"how-it-works","How It Works",[206,207,212],"pre",{"className":208,"code":210,"language":211},[209],"language-text","user action (create\u002Fupdate\u002Fdelete)\n  → optimistic: items.value updated in store immediately\n  → upsertRow \u002F deleteRow writes to local SQLite (sync)\n    → success: watchTable fires → store refreshes from SQLite\n    → failure: rollback prev snapshot + showErrorToast (local write error)\n  → PowerSync queues the op for upload\n    → online: connector drains queue → Supabase upsert\u002Fupdate\u002Fdelete\n    → offline: queue persists in SQLite; drained on reconnect\n","text",[213,214,210],"code",{"__ignoreMap":215},"",[197,217,218],{},"There is no manual queue management, no collapsing rules, and no frontend-local ID system. PowerSync owns the upload queue.",[201,220,222],{"id":221},"offline-writes","Offline Writes",[197,224,225],{},"When the device is offline:",[227,228,229,233,236,239],"ul",{},[230,231,232],"li",{},"Reads continue to work - all data is already in local SQLite.",[230,234,235],{},"Writes go to local SQLite immediately (optimistic). The user sees the change right away.",[230,237,238],{},"The upload op is added to PowerSync's internal queue.",[230,240,241],{},"On reconnect, PowerSync drains the queue automatically.",[197,243,244],{},"No user action is needed to trigger sync after reconnect.",[201,246,248],{"id":247},"session-loss-while-offline","Session Loss While Offline",[197,250,251,252,256,257,260,261,264],{},"Going offline does not sign the user out. A failed token refresh while offline is ",[253,254,255],"strong",{},"retryable"," - supabase-js keeps the persisted session (localStorage) and emits no ",[213,258,259],{},"SIGNED_OUT",", so the route guard (",[213,262,263],{},"hasPersistedSession()",") keeps the user in the app and the unsynced queue keeps filling. Two cases are handled so offline writes survive:",[227,266,267,304],{},[230,268,269,272,273,276,277,280,281,284,285,288,289,292,293,296,297,299,300,303],{},[253,270,271],{},"Reactive session not resolved yet (offline cold start)."," After the access token expires, a fresh ",[213,274,275],{},"getSession()"," resolves to ",[213,278,279],{},"null",", so the reactive ",[213,282,283],{},"uid"," is null until reconnect. Writes stamp ",[213,286,287],{},"userId"," via ",[213,290,291],{},"resolveWriteUid(uid.value)"," (",[213,294,295],{},"app\u002Fapp\u002Fcomposables\u002FuseAuthSession.ts","), which falls back to the synchronously-persisted uid. Without it the row would carry an empty ",[213,298,287],{},", which RLS rejects (",[213,301,302],{},"42501",") on upload - silently dropping the offline write.",[230,305,306,309,310,312,313,316,317,320,321,324,325,328,329,332,333,336,337,340,341,344,345,348],{},[253,307,308],{},"Session lost involuntarily (token revoked \u002F expired server-side)."," Surfaces on reconnect as a genuine ",[213,311,259],{},". The auth watcher then ",[253,314,315],{},"pauses"," sync (",[213,318,319],{},"pausePowerSync"," - ",[213,322,323],{},"db.disconnect()",", no clear) instead of wiping, keeping local SQLite ",[253,326,327],{},"and the unsynced queue",". When pending writes exist, a re-auth toast is shown (",[213,330,331],{},"sync.errors.sessionLostPending","); re-authenticating as the same user reconnects and drains the queue. Only an ",[253,334,335],{},"explicit"," sign-out (",[213,338,339],{},"useUserStore.signOut()",") wipes local data via ",[213,342,343],{},"disconnectPowerSync"," → ",[213,346,347],{},"disconnectAndClear",".",[201,350,352],{"id":351},"upload-error-handling","Upload Error Handling",[197,354,355,358],{},[213,356,357],{},"SupabaseConnector.uploadData()"," distinguishes fatal errors from retryable ones:",[360,361,363],"h3",{"id":362},"fatal-errors-discarded","Fatal errors (discarded)",[197,365,366,367,370,371,374,375,377,378,381,382,385,386,389],{},"Postgres error classes ",[213,368,369],{},"22xxx"," (data exception), ",[213,372,373],{},"23xxx"," (integrity constraint violation), and ",[213,376,302],{}," (RLS - insufficient privilege) are treated as fatal. The failing op ",[253,379,380],{},"and every op queued after it"," are discarded so the queue is not blocked, then auto-reconciled by the plugin (",[213,383,384],{},"planDivergence",", ",[213,387,388],{},"app\u002Fservices\u002Fpowersync\u002FuploadReconcile.ts","):",[227,391,392,401],{},[230,393,394,397,398,348],{},[253,395,396],{},"Rejected INSERTs",": the local rows are deleted to converge (the server never accepted them). A reverted wallet\u002Fcategory cascades to local trns referencing it, so no orphans survive. Toast: ",[213,399,400],{},"sync.errors.uploadReverted",[230,402,403,406,407,410,411,348],{},[253,404,405],{},"Rejected UPDATE\u002FDELETE",": the server still holds the prior version and PowerSync can't re-pull a single row, so the user is offered a destructive full re-sync (",[213,408,409],{},"forceResync",": wipe local SQLite + re-pull server truth). Toast: ",[213,412,413],{},"sync.errors.uploadDiverged",[360,415,417],{"id":416},"retryable-errors","Retryable errors",[197,419,420,421,424],{},"Network errors, timeouts, and all other non-fatal errors cause the error to be ",[253,422,423],{},"rethrown",". PowerSync catches the rethrow and retries with backoff when connectivity is restored.",[201,426,428],{"id":427},"cascade-deletes","Cascade Deletes",[197,430,431,434,435,438,439,442,443,446],{},[213,432,433],{},"deleteWallet"," and ",[213,436,437],{},"deleteCategory"," also delete the entity's trns from local SQLite before returning. This is required because the ",[213,440,441],{},"watchTable"," subscription on ",[213,444,445],{},"trns"," would otherwise re-add the orphaned rows to the store after the parent entity is removed from SQLite. Rollback on a local write failure restores both the entity and its trns.",[197,448,449],{},"The server enforces no foreign key constraints (PowerSync upload order is not guaranteed), so referential integrity lives in app logic.",[201,451,453],{"id":452},"optimistic-rollback","Optimistic Rollback",[197,455,456,457,460,461,464],{},"Every store write snapshots ",[213,458,459],{},"prev = items.value"," before mutating. If the local SQLite write throws, ",[213,462,463],{},"items.value = prev"," is restored and an error toast is shown. This covers local write errors (e.g., SQLite constraint, disk full). Server-rejection errors (RLS, constraint) are handled separately by the upload-error handler.",[201,466,468],{"id":467},"no-manual-queue","No Manual Queue",[197,470,471],{},"PowerSync owns the upload queue natively - there is no hand-written queue, no replay step, no op-merging, and no frontend ID remapping.",[197,473,474,475,478],{},"Client IDs are regular UUIDs (generated client-side via ",[213,476,477],{},"crypto.randomUUID()"," or equivalent). They are stable and serve as the permanent primary key on both device and server, so nothing needs remapping after upload.",[201,480,482],{"id":481},"key-files","Key Files",[484,485,486,499],"table",{},[487,488,489],"thead",{},[490,491,492,496],"tr",{},[493,494,495],"th",{},"File",[493,497,498],{},"Role",[500,501,502,515,531,546,562,572,582,592,601],"tbody",{},[490,503,504,510],{},[505,506,507],"td",{},[213,508,509],{},"app\u002Fservices\u002Fpowersync\u002Fconnector.ts",[505,511,512,514],{},[213,513,357],{}," - drains queue, fatal\u002Fretryable split",[490,516,517,522],{},[505,518,519],{},[213,520,521],{},"app\u002Fservices\u002Fpowersync\u002Fmutations.ts",[505,523,524,385,527,530],{},[213,525,526],{},"upsertRow",[213,528,529],{},"deleteRow"," - write to local SQLite",[490,532,533,538],{},[505,534,535],{},[213,536,537],{},"app\u002Fservices\u002Fpowersync\u002Fdb.ts",[505,539,540,542,543,545],{},[213,541,441],{}," - subscribe to SQLite changes; ",[213,544,319],{}," - stop sync, keep local data + queue",[490,547,548,552],{},[505,549,550],{},[213,551,295],{},[505,553,554,557,558,561],{},[213,555,556],{},"resolveWriteUid"," - persisted-uid fallback for row writes; ",[213,559,560],{},"hasPersistedSession"," route gate",[490,563,564,569],{},[505,565,566],{},[213,567,568],{},"app\u002Fapp\u002Fplugins\u002Fpowersync.client.ts",[505,570,571],{},"Registers upload-error handler (toast); pauses sync (keeps data) on involuntary session loss",[490,573,574,579],{},[505,575,576],{},[213,577,578],{},"app\u002Fapp\u002Fcomponents\u002Ftrns\u002FuseTrnsStore.ts",[505,580,581],{},"Optimistic write + rollback pattern, cascade delete trns",[490,583,584,589],{},[505,585,586],{},[213,587,588],{},"app\u002Fapp\u002Fcomponents\u002Fwallets\u002FuseWalletsStore.ts",[505,590,591],{},"Optimistic write + rollback + cascade delete trns",[490,593,594,599],{},[505,595,596],{},[213,597,598],{},"app\u002Fapp\u002Fcomponents\u002Fcategories\u002FuseCategoriesStore.ts",[505,600,591],{},[490,602,603,608],{},[505,604,605],{},[213,606,607],{},"app\u002Fapp\u002Fcomposables\u002FuseStoreSync.ts",[505,609,610,385,613,616,617],{},[213,611,612],{},"showErrorToast",[213,614,615],{},"showSuccessToast",", demo-only ",[213,618,619],{},"createDebouncedPersist",[201,621,623],{"id":622},"next-steps","Next Steps",[227,625,626,632],{},[230,627,628,631],{},[629,630,143],"a",{"href":144}," - PowerSync sync streams, upload queue, and deletion handling",[230,633,634,636],{},[629,635,134],{"href":135}," - app initialization and store pattern",{"title":215,"searchDepth":638,"depth":638,"links":639},2,[640,641,642,643,648,649,650,651,652],{"id":203,"depth":638,"text":204},{"id":221,"depth":638,"text":222},{"id":247,"depth":638,"text":248},{"id":351,"depth":638,"text":352,"children":644},[645,647],{"id":362,"depth":646,"text":363},3,{"id":416,"depth":646,"text":417},{"id":427,"depth":638,"text":428},{"id":452,"depth":638,"text":453},{"id":467,"depth":638,"text":468},{"id":481,"depth":638,"text":482},{"id":622,"depth":638,"text":623},"How PowerSync enables offline writes, upload queue, and error surfacing.","md",null,{},{"icon":151},{"title":148,"description":659},"Finapp is offline-first via PowerSync - local SQLite holds all data, writes are optimistic and queued locally, fatal upload errors are discarded and surfaced via toast.","sOA3Uv_DQhalH_HlmozwUC0WzYd36yofMcdyIjt6fL8",[662,664],{"title":143,"path":144,"stem":145,"description":663,"icon":146,"children":-1},"How PowerSync keeps local SQLite in sync with Supabase Postgres.",{"title":153,"path":154,"stem":155,"description":665,"icon":121,"children":-1},"Rationale behind key design choices.",1782114344513]