[{"data":1,"prerenderedAt":854},["ShallowReactive",2],{"navigation_docs_en":3,"-en-reference-sync":191,"-en-reference-sync-surround":849},[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":143,"body":193,"description":841,"extension":842,"links":843,"meta":844,"navigation":845,"path":144,"seo":846,"stem":145,"__hash__":848},"docs_en\u002Fen\u002F3.reference\u002F03.sync.md",{"type":194,"value":195,"toc":829},"minimark",[196,200,205,216,219,223,235,244,277,291,300,306,310,320,354,357,361,379,401,405,415,471,478,482,538,548,568,572,575,596,600,627,631,808,812,825],[197,198,199],"p",{},"PowerSync is the sync layer between the client (local SQLite via wa-sqlite \u002F IndexedDB) and the server (Supabase Postgres). Local SQLite is the single source of truth for the UI - stores read only from SQLite, never from the network directly.",[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","Supabase Postgres (server)\n  ↕  logical replication (powersync_role, powersync publication)\nPowerSync service (self-hosted, :8080)\n  ↕  WebSocket sync stream (Sync Streams edition 3, per-user + global)\nLocal SQLite (client, IDBBatchAtomicVFS \u002F IndexedDB)\n  ↕  watchTable subscriptions\nPinia stores (UI)\n","text",[213,214,210],"code",{"__ignoreMap":215},"",[197,217,218],{},"On connect, PowerSync replays the server state into local SQLite. After that, every server change streams in via the open WebSocket. Local writes go to SQLite immediately (optimistic) and are queued for upload.",[201,220,222],{"id":221},"sync-streams","Sync Streams",[197,224,225,226,229,230,234],{},"Sync rules are defined in ",[213,227,228],{},"app\u002Fpowersync\u002Fconfig\u002Fsync-config.yaml"," using ",[231,232,233],"strong",{},"Sync Streams edition 3",".",[197,236,237],{},[231,238,239,240,243],{},"Per-user stream (",[213,241,242],{},"user_data",", auto_subscribe):",[206,245,249],{"className":246,"code":247,"language":248,"meta":215,"style":215},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","SELECT * FROM categories  WHERE \"userId\" = auth.user_id()\nSELECT * FROM wallets     WHERE \"userId\" = auth.user_id()\nSELECT * FROM trns        WHERE \"userId\" = auth.user_id()\nSELECT * FROM user_settings WHERE \"userId\" = auth.user_id()\n","sql",[213,250,251,259,265,271],{"__ignoreMap":215},[252,253,256],"span",{"class":254,"line":255},"line",1,[252,257,258],{},"SELECT * FROM categories  WHERE \"userId\" = auth.user_id()\n",[252,260,262],{"class":254,"line":261},2,[252,263,264],{},"SELECT * FROM wallets     WHERE \"userId\" = auth.user_id()\n",[252,266,268],{"class":254,"line":267},3,[252,269,270],{},"SELECT * FROM trns        WHERE \"userId\" = auth.user_id()\n",[252,272,274],{"class":254,"line":273},4,[252,275,276],{},"SELECT * FROM user_settings WHERE \"userId\" = auth.user_id()\n",[197,278,279,286,287,290],{},[231,280,281,282,285],{},"Global stream (",[213,283,284],{},"rates","):"," all rows go to every authenticated user; rows are written server-side by the ",[213,288,289],{},"fetch-rates"," edge function, never by clients.",[206,292,294],{"className":246,"code":293,"language":248,"meta":215,"style":215},"SELECT * FROM rates\n",[213,295,296],{"__ignoreMap":215},[252,297,298],{"class":254,"line":255},[252,299,293],{},[197,301,302,305],{},[213,303,304],{},"auth.user_id()"," resolves to the Supabase user UUID from the validated JWT. The PowerSync service validates Supabase JWTs (ES256) via the JWKS endpoint, so no custom token logic is required on the client.",[201,307,309],{"id":308},"watchtable-the-single-subscription-pattern","watchTable - the Single Subscription Pattern",[197,311,312,315,316,319],{},[213,313,314],{},"watchTable(sql, params, onRows, throttleMs?)"," in ",[213,317,318],{},"app\u002Fservices\u002Fpowersync\u002Fdb.ts",":",[321,322,323,331,337,343],"ul",{},[324,325,326,327,330],"li",{},"Executes the SQL query immediately and calls ",[213,328,329],{},"onRows"," with the current local rows.",[324,332,333,334,336],{},"Subscribes to table-change events. Whenever any row in the queried table changes (local write or incoming sync), ",[213,335,329],{}," is called again with fresh rows.",[324,338,339,342],{},[213,340,341],{},"throttleMs"," coalesces rapid bursts of changes (trailing edge). Default: 30ms. Transactions use 120ms to handle the large burst of rows during initial sync without thrashing the UI.",[324,344,345,346,349,350,353],{},"Returns an ",[213,347,348],{},"AbortController"," - call ",[213,351,352],{},".abort()"," to unsubscribe (stores do this on teardown).",[197,355,356],{},"This single mechanism replaces the old init + realtime-subscription split. There is no separate \"load from cache, then subscribe\" step.",[201,358,360],{"id":359},"echo-suppression-transactions","Echo Suppression (Transactions)",[197,362,363,364,367,368,371,372,375,376,285],{},"When a store writes optimistically and then ",[213,365,366],{},"watchTable"," fires for that same write, the UI would flicker. ",[213,369,370],{},"useTrnsStore"," avoids this with ",[213,373,374],{},"reconcileTrns(prev, rows)"," (",[213,377,378],{},"app\u002Fapp\u002Fcomponents\u002Ftrns\u002Freconcile.ts",[321,380,381,387,394],{},[324,382,383,384,234],{},"Compares incoming rows to the previous state by ",[213,385,386],{},"updatedAt",[324,388,389,390,393],{},"If nothing has changed, returns the same ",[213,391,392],{},"prev"," ref (Vue sees no change, no re-render).",[324,395,396,397,400],{},"For changed rows, only runs ",[213,398,399],{},"rowToTrn()"," for the delta - unchanged row objects are reused.",[201,402,404],{"id":403},"upload-queue","Upload Queue",[197,406,407,408,375,411,414],{},"Local writes are queued by PowerSync automatically. ",[213,409,410],{},"SupabaseConnector.uploadData()",[213,412,413],{},"app\u002Fservices\u002Fpowersync\u002Fconnector.ts",") drains the queue:",[416,417,418,431],"table",{},[419,420,421],"thead",{},[422,423,424,428],"tr",{},[425,426,427],"th",{},"PowerSync op",[425,429,430],{},"Supabase call",[432,433,434,447,459],"tbody",{},[422,435,436,442],{},[437,438,439],"td",{},[213,440,441],{},"PUT",[437,443,444],{},[213,445,446],{},"supabase.from(table).upsert(row)",[422,448,449,454],{},[437,450,451],{},[213,452,453],{},"PATCH",[437,455,456],{},[213,457,458],{},"supabase.from(table).update(data).eq('id', id)",[422,460,461,466],{},[437,462,463],{},[213,464,465],{},"DELETE",[437,467,468],{},[213,469,470],{},"supabase.from(table).delete().eq('id', id)",[197,472,473,474,477],{},"The connector runs as the authenticated Supabase user, so RLS policies apply on every upload. This is the write-path security layer - the read path uses the ",[213,475,476],{},"powersync_role"," (BYPASSRLS) for replication, with per-user partitioning enforced by sync rules.",[201,479,481],{"id":480},"fatal-vs-retryable-upload-errors","Fatal vs Retryable Upload Errors",[416,483,484,497],{},[419,485,486],{},[422,487,488,491,494],{},[425,489,490],{},"Error class",[425,492,493],{},"Postgres codes",[425,495,496],{},"Behavior",[432,498,499,523],{},[422,500,501,504,516],{},[437,502,503],{},"Fatal (data \u002F constraint \u002F RLS)",[437,505,506,509,510,509,513],{},[213,507,508],{},"22xxx",", ",[213,511,512],{},"23xxx",[213,514,515],{},"42501",[437,517,518,519,522],{},"Transaction ",[231,520,521],{},"discarded"," - queue unblocked; the discarded ops are auto-reconciled (see below)",[422,524,525,528,531],{},[437,526,527],{},"Retryable (network, timeout)",[437,529,530],{},"everything else",[437,532,533,534,537],{},"Error ",[231,535,536],{},"rethrown"," - PowerSync retries with backoff",[197,539,540,541,375,544,547],{},"Discarded ops are classified by ",[213,542,543],{},"planDivergence()",[213,545,546],{},"app\u002Fservices\u002Fpowersync\u002FuploadReconcile.ts",") and reconciled automatically:",[321,549,550,559],{},[324,551,552,555,556,234],{},[231,553,554],{},"Rejected INSERTs"," - the local rows are deleted (ids are client-generated, the server never accepted them), with a cascade to local trns referencing a reverted wallet\u002Fcategory. Toast: ",[213,557,558],{},"sync.errors.uploadReverted",[324,560,561,564,565,234],{},[231,562,563],{},"Rejected UPDATE\u002FDELETE"," - the server still holds a prior version that PowerSync can't re-pull per row, so the user is offered a full re-sync (wipe + re-pull). Toast: ",[213,566,567],{},"sync.errors.uploadDiverged",[201,569,571],{"id":570},"deletion-handling","Deletion Handling",[197,573,574],{},"PowerSync syncs hard deletes natively - when a row is deleted on the server the PowerSync service streams a delete event to the client and SQLite removes the row. No hash comparison or full-refetch is needed.",[197,576,577,580,581,584,585,588,589,591,592,595],{},[231,578,579],{},"Cascade deletes (client-side):"," ",[213,582,583],{},"deleteWallet"," and ",[213,586,587],{},"deleteCategory"," also delete the entity's trns from local SQLite before returning. Without this, the ",[213,590,366],{}," subscription on ",[213,593,594],{},"trns"," would re-add the orphaned rows to the UI after the parent entity is gone.",[201,597,599],{"id":598},"first-sync","First Sync",[197,601,602,375,605,607,608,611,612,615,616,619,620,315,623,626],{},[213,603,604],{},"waitForFirstSync(timeoutMs=30000)",[213,606,318],{},") resolves ",[213,609,610],{},"true"," once the initial server state has fully streamed into local SQLite, ",[213,613,614],{},"false"," on timeout. The boot does ",[231,617,618],{},"not"," block on it - cached\u002Flocal data paints immediately and the first sync settles in the background (",[213,621,622],{},"awaitInitialSync",[213,624,625],{},"useInitApp.ts","). Its result only gates the onboarding\u002Ferror screens, so a fresh login never flashes onboarding while data is still arriving.",[201,628,630],{"id":629},"key-files","Key Files",[416,632,633,643],{},[419,634,635],{},[422,636,637,640],{},[425,638,639],{},"File",[425,641,642],{},"Role",[432,644,645,665,684,694,719,737,753,764,774,783,793],{},[422,646,647,651],{},[437,648,649],{},[213,650,318],{},[437,652,653,656,657,509,660,509,662],{},[213,654,655],{},"PowerSyncDatabase"," singleton, ",[213,658,659],{},"connectPowerSync",[213,661,366],{},[213,663,664],{},"waitForFirstSync",[422,666,667,671],{},[437,668,669],{},[213,670,413],{},[437,672,673,676,677,680,681],{},[213,674,675],{},"SupabaseConnector"," - ",[213,678,679],{},"fetchCredentials"," + ",[213,682,683],{},"uploadData",[422,685,686,691],{},[437,687,688],{},[213,689,690],{},"app\u002Fservices\u002Fpowersync\u002FAppSchema.ts",[437,692,693],{},"Client SQLite schema",[422,695,696,701],{},[437,697,698],{},[213,699,700],{},"app\u002Fservices\u002Fpowersync\u002Ftransforms.ts",[437,702,703,509,706,509,709,509,712,509,715,718],{},[213,704,705],{},"rowToTrn",[213,707,708],{},"rowToWallet",[213,710,711],{},"rowToCategory",[213,713,714],{},"rowToRates",[213,716,717],{},"trnToRow",", etc.",[422,720,721,726],{},[437,722,723],{},[213,724,725],{},"app\u002Fservices\u002Fpowersync\u002Fmutations.ts",[437,727,728,509,731,509,734],{},[213,729,730],{},"upsertRow",[213,732,733],{},"deleteRow",[213,735,736],{},"upsertRows",[422,738,739,744],{},[437,740,741],{},[213,742,743],{},"app\u002Fapp\u002Fcomponents\u002Ftrns\u002FuseTrnsStore.ts",[437,745,746,748,749,752],{},[213,747,366],{}," subscription + ",[213,750,751],{},"reconcileTrns"," echo suppression",[422,754,755,759],{},[437,756,757],{},[213,758,378],{},[437,760,761,763],{},[213,762,751],{}," - dedup and reuse unchanged row objects",[422,765,766,771],{},[437,767,768],{},[213,769,770],{},"app\u002Fapp\u002Fplugins\u002Fpowersync.client.ts",[437,772,773],{},"Connects PowerSync on sign-in; pauses it (keeps local data + queue) on involuntary session loss; explicit sign-out wipes separately",[422,775,776,780],{},[437,777,778],{},[213,779,228],{},[437,781,782],{},"Sync Streams rules (per-user + global rates)",[422,784,785,790],{},[437,786,787],{},[213,788,789],{},"app\u002Fpowersync\u002Fconfig\u002Fservice.yaml",[437,791,792],{},"PowerSync service config (replication, JWT, port 8080)",[422,794,795,800],{},[437,796,797],{},[213,798,799],{},"app\u002Fsupabase\u002Fpowersync_setup.sql",[437,801,802,680,804,807],{},[213,803,476],{},[213,805,806],{},"powersync"," publication",[201,809,811],{"id":810},"next-steps","Next Steps",[321,813,814,820],{},[324,815,816,819],{},[817,818,148],"a",{"href":149}," - offline writes, upload queue, and fatal error handling",[324,821,822,824],{},[817,823,134],{"href":135}," - app initialization and store pattern",[826,827,828],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":215,"searchDepth":261,"depth":261,"links":830},[831,832,833,834,835,836,837,838,839,840],{"id":203,"depth":261,"text":204},{"id":221,"depth":261,"text":222},{"id":308,"depth":261,"text":309},{"id":359,"depth":261,"text":360},{"id":403,"depth":261,"text":404},{"id":480,"depth":261,"text":481},{"id":570,"depth":261,"text":571},{"id":598,"depth":261,"text":599},{"id":629,"depth":261,"text":630},{"id":810,"depth":261,"text":811},"How PowerSync keeps local SQLite in sync with Supabase Postgres.","md",null,{},{"icon":146},{"title":143,"description":847},"How Finapp syncs data offline-first with PowerSync - local SQLite as source of truth, per-user sync streams, upload queue, throttle\u002Fcoalesce, echo suppression, and deletion handling.","JQF-QjLyLoZPlIlMpste4OWUJgOdrwmx6jRnyP8YfgA",[850,852],{"title":139,"path":140,"stem":141,"description":851,"icon":44,"children":-1},"Transaction types, adjustment logic, statistics.",{"title":148,"path":149,"stem":150,"description":853,"icon":151,"children":-1},"How PowerSync enables offline writes, upload queue, and error surfacing.",1782114344203]