[{"data":1,"prerenderedAt":2320},["ShallowReactive",2],{"navigation_docs_en":3,"-en-development-deployment":191,"-en-development-deployment-surround":2315},[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":88,"body":193,"description":2307,"extension":2308,"links":2309,"meta":2310,"navigation":2311,"path":89,"seo":2312,"stem":90,"__hash__":2314},"docs_en\u002Fen\u002F2.development\u002F05.deployment.md",{"type":194,"value":195,"toc":2282},"minimark",[196,200,205,227,395,400,468,471,514,540,544,587,591,597,659,662,666,676,692,703,707,985,1002,1006,1009,1179,1184,1192,1229,1369,1422,1426,1437,1494,1586,1595,1599,1602,1606,1635,1655,1672,1846,1863,1867,1874,1889,1903,1907,1910,2035,2046,2050,2063,2067,2118,2129,2133,2191,2194,2198,2205,2220,2224,2231,2258,2261,2278],[197,198,199],"p",{},"Finapp is a pnpm workspace monorepo. Both the app and docs deploy from the same repository as separate Vercel projects.",[201,202,204],"h2",{"id":203},"vercel-setup","Vercel Setup",[197,206,207,208,212,213,220,221,226],{},"Create ",[209,210,211],"strong",{},"two projects"," in Vercel, both pointing to the same GitHub repository. Nuxt needs no special Vercel configuration beyond the per-project settings below - see ",[214,215,219],"a",{"href":216,"rel":217},"https:\u002F\u002Fnuxt.com\u002Fdeploy\u002Fvercel",[218],"nofollow","Nuxt on Vercel"," and ",[214,222,225],{"href":223,"rel":224},"https:\u002F\u002Fvercel.com\u002Fdocs\u002Fframeworks\u002Ffull-stack\u002Fnuxt",[218],"Vercel's Nuxt guide"," for the framework basics.",[228,229,232,272],"callout",{"color":230,"icon":231},"info","i-lucide-info",[197,233,234,237,238,242,243,246,247,246,250,246,253,246,256,246,259,246,262,265,266,271],{},[209,235,236],{},"Settings split: file vs project."," Most build settings live in each project's ",[239,240,241],"code",{},"vercel.json"," (",[239,244,245],{},"framework",", ",[239,248,249],{},"buildCommand",[239,251,252],{},"installCommand",[239,254,255],{},"outputDirectory",[239,257,258],{},"ignoreCommand",[239,260,261],{},"env",[239,263,264],{},"rewrites","). ",[209,267,268,269],{},"Root Directory cannot live in ",[239,270,241],{}," because Vercel needs it to know where to look for the file. Set it once in the project's dashboard (Settings -> General -> Root Directory) or via the API:",[273,274,279],"pre",{"className":275,"code":276,"language":277,"meta":278,"style":278},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","TOKEN=$(jq -r .token \"$HOME\u002FLibrary\u002FApplication Support\u002Fcom.vercel.cli\u002Fauth.json\")\ncurl -X PATCH \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  \"https:\u002F\u002Fapi.vercel.com\u002Fv9\u002Fprojects\u002F\u003Cproject-name>?teamId=\u003Cteam-id>\" \\\n  -d '{\"rootDirectory\":\"docs\"}'\n","bash","",[239,280,281,320,335,353,367,380],{"__ignoreMap":278},[282,283,286,290,294,298,302,305,308,311,314,317],"span",{"class":284,"line":285},"line",1,[282,287,289],{"class":288},"sTEyZ","TOKEN",[282,291,293],{"class":292},"sMK4o","=$(",[282,295,297],{"class":296},"sBMFI","jq",[282,299,301],{"class":300},"sfazB"," -r",[282,303,304],{"class":300}," .token",[282,306,307],{"class":292}," \"",[282,309,310],{"class":288},"$HOME",[282,312,313],{"class":300},"\u002FLibrary\u002FApplication Support\u002Fcom.vercel.cli\u002Fauth.json",[282,315,316],{"class":292},"\"",[282,318,319],{"class":292},")\n",[282,321,323,326,329,332],{"class":284,"line":322},2,[282,324,325],{"class":296},"curl",[282,327,328],{"class":300}," -X",[282,330,331],{"class":300}," PATCH",[282,333,334],{"class":288}," \\\n",[282,336,338,341,343,346,349,351],{"class":284,"line":337},3,[282,339,340],{"class":300},"  -H",[282,342,307],{"class":292},[282,344,345],{"class":300},"Authorization: Bearer ",[282,347,348],{"class":288},"$TOKEN",[282,350,316],{"class":292},[282,352,334],{"class":288},[282,354,356,358,360,363,365],{"class":284,"line":355},4,[282,357,340],{"class":300},[282,359,307],{"class":292},[282,361,362],{"class":300},"Content-Type: application\u002Fjson",[282,364,316],{"class":292},[282,366,334],{"class":288},[282,368,370,373,376,378],{"class":284,"line":369},5,[282,371,372],{"class":292},"  \"",[282,374,375],{"class":300},"https:\u002F\u002Fapi.vercel.com\u002Fv9\u002Fprojects\u002F\u003Cproject-name>?teamId=\u003Cteam-id>",[282,377,316],{"class":292},[282,379,334],{"class":288},[282,381,383,386,389,392],{"class":284,"line":382},6,[282,384,385],{"class":300},"  -d",[282,387,388],{"class":292}," '",[282,390,391],{"class":300},"{\"rootDirectory\":\"docs\"}",[282,393,394],{"class":292},"'\n",[396,397,399],"h3",{"id":398},"app-project","App Project",[401,402,403,416],"table",{},[404,405,406],"thead",{},[407,408,409,413],"tr",{},[410,411,412],"th",{},"Setting",[410,414,415],{},"Value",[417,418,419,430,438,448,458],"tbody",{},[407,420,421,425],{},[422,423,424],"td",{},"Root Directory",[422,426,427],{},[239,428,429],{},"app",[407,431,432,435],{},[422,433,434],{},"Framework Preset",[422,436,437],{},"Nuxt.js",[407,439,440,443],{},[422,441,442],{},"Build Command",[422,444,445],{},[239,446,447],{},"pnpm build",[407,449,450,453],{},[422,451,452],{},"Output Directory",[422,454,455],{},[239,456,457],{},".output\u002Fpublic",[407,459,460,463],{},[422,461,462],{},"Install Command",[422,464,465],{},[239,466,467],{},"pnpm install",[197,469,470],{},"Add the following environment variables pointing at your hosted Supabase project and PowerSync service:",[401,472,473,482],{},[404,474,475],{},[407,476,477,480],{},[410,478,479],{},"Variable",[410,481,415],{},[417,483,484,494,504],{},[407,485,486,491],{},[422,487,488],{},[239,489,490],{},"VITE_SUPABASE_URL",[422,492,493],{},"Your Supabase project URL",[407,495,496,501],{},[422,497,498],{},[239,499,500],{},"VITE_SUPABASE_ANON_KEY",[422,502,503],{},"Your Supabase publishable (anon) key",[407,505,506,511],{},[422,507,508],{},[239,509,510],{},"VITE_POWERSYNC_URL",[422,512,513],{},"Your PowerSync service URL",[228,515,517,520,521,524,525,528,529,531,532,535,536,539],{"color":230,"icon":516},"i-lucide-terminal",[209,518,519],{},"Git-connected vs direct CLI deploy."," The two-project setup here is Git-connected (pushes auto-deploy). For a standalone or throwaway environment (e.g. a ",[239,522,523],{},"finapp-dev"," project off a feature branch), skip the Git connection and deploy the working tree straight from the repo root: create\u002Flink a project (",[239,526,527],{},"vercel link",") with its Root Directory set to ",[239,530,429],{},", then ",[239,533,534],{},"vercel deploy --prod",". The whole monorepo uploads from the root and Vercel builds ",[239,537,538],{},"app\u002F"," via the Root Directory setting.",[396,541,543],{"id":542},"docs-project","Docs Project",[401,545,546,554],{},[404,547,548],{},[407,549,550,552],{},[410,551,412],{},[410,553,415],{},[417,555,556,565,571,579],{},[407,557,558,560],{},[422,559,424],{},[422,561,562],{},[239,563,564],{},"docs",[407,566,567,569],{},[422,568,434],{},[422,570,437],{},[407,572,573,575],{},[422,574,442],{},[422,576,577],{},[239,578,447],{},[407,580,581,583],{},[422,582,452],{},[422,584,585],{},[239,586,457],{},[201,588,590],{"id":589},"ignored-build-step","Ignored Build Step",[197,592,593,594,596],{},"To avoid unnecessary builds, configure ",[209,595,590],{}," in each Vercel project settings (Settings -> Git -> Ignored Build Step):",[598,599,600,636],"code-group",{},[273,601,604],{"className":275,"code":602,"filename":603,"language":277,"meta":278,"style":278},"git diff --quiet HEAD^ HEAD -- . ':!docs'\n","App",[239,605,606],{"__ignoreMap":278},[282,607,608,611,614,617,620,623,626,629,631,634],{"class":284,"line":285},[282,609,610],{"class":296},"git",[282,612,613],{"class":300}," diff",[282,615,616],{"class":300}," --quiet",[282,618,619],{"class":300}," HEAD^",[282,621,622],{"class":300}," HEAD",[282,624,625],{"class":300}," --",[282,627,628],{"class":300}," .",[282,630,388],{"class":292},[282,632,633],{"class":300},":!docs",[282,635,394],{"class":292},[273,637,640],{"className":275,"code":638,"filename":639,"language":277,"meta":278,"style":278},"git diff --quiet HEAD^ HEAD -- docs\n","Docs",[239,641,642],{"__ignoreMap":278},[282,643,644,646,648,650,652,654,656],{"class":284,"line":285},[282,645,610],{"class":296},[282,647,613],{"class":300},[282,649,616],{"class":300},[282,651,619],{"class":300},[282,653,622],{"class":300},[282,655,625],{"class":300},[282,657,658],{"class":300}," docs\n",[197,660,661],{},"This way the app won't redeploy when only docs change, and vice versa.",[201,663,665],{"id":664},"backend-supabase-powersync","Backend: Supabase + PowerSync",[197,667,668,669,671,672,675],{},"The Nuxt SPA is statically hosted (Vercel above, but any static host serving ",[239,670,457],{}," after ",[239,673,674],{},"pnpm generate"," works). The backend services are separate:",[677,678,679,686],"ul",{},[680,681,682,685],"li",{},[209,683,684],{},"Supabase"," - hosted Supabase project (cloud or self-hosted) provides Postgres, Auth, and the PostgREST\u002Fsupabase-js write path.",[680,687,688,691],{},[209,689,690],{},"PowerSync"," - a PowerSync service (cloud or self-hosted) handles offline-first sync by replicating from the Supabase Postgres via logical replication.",[197,693,694,695,698,699,702],{},"Set the three ",[239,696,697],{},"VITE_*"," env vars in Vercel to point at your production Supabase project and PowerSync instance. See ",[214,700,68],{"href":701},"\u002Fen\u002Fdevelopment\u002Finstallation#env-variables"," for the variable descriptions.",[396,704,706],{"id":705},"supabase-production-project","Supabase (production project)",[708,709,710,899,978],"ol",{},[680,711,712,713,716,717,722,723,827,830,837,838,841,842,844,845,848,849,852,853,856,857,881,883,884,887,888,891,892,894,895,898],{},"Create a Supabase project (dashboard, or ",[239,714,715],{},"supabase projects create","), link the repo and push the schema (",[214,718,721],{"href":719,"rel":720},"https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fdeployment\u002Fdatabase-migrations",[218],"Supabase migration docs","):",[273,724,727],{"className":275,"code":725,"filename":726,"language":277,"meta":278,"style":278},"cd app\nsupabase projects create finapp --org-id \u003Corg-id> --region \u003Cregion> --db-password \u003Cpw>\nsupabase link --project-ref \u003Cproject-ref>\nsupabase db push        # applies supabase\u002Fmigrations\u002F (tables, RLS, signup trigger)\n","Terminal",[239,728,729,738,793,813],{"__ignoreMap":278},[282,730,731,735],{"class":284,"line":285},[282,732,734],{"class":733},"s2Zo4","cd",[282,736,737],{"class":300}," app\n",[282,739,740,743,746,749,752,755,758,761,764,767,770,772,775,778,780,783,785,787,790],{"class":284,"line":322},[282,741,742],{"class":296},"supabase",[282,744,745],{"class":300}," projects",[282,747,748],{"class":300}," create",[282,750,751],{"class":300}," finapp",[282,753,754],{"class":300}," --org-id",[282,756,757],{"class":292}," \u003C",[282,759,760],{"class":300},"org-i",[282,762,763],{"class":288},"d",[282,765,766],{"class":292},">",[282,768,769],{"class":300}," --region",[282,771,757],{"class":292},[282,773,774],{"class":300},"regio",[282,776,777],{"class":288},"n",[282,779,766],{"class":292},[282,781,782],{"class":300}," --db-password",[282,784,757],{"class":292},[282,786,197],{"class":300},[282,788,789],{"class":288},"w",[282,791,792],{"class":292},">\n",[282,794,795,797,800,803,805,808,811],{"class":284,"line":337},[282,796,742],{"class":296},[282,798,799],{"class":300}," link",[282,801,802],{"class":300}," --project-ref",[282,804,757],{"class":292},[282,806,807],{"class":300},"project-re",[282,809,810],{"class":288},"f",[282,812,792],{"class":292},[282,814,815,817,820,823],{"class":284,"line":355},[282,816,742],{"class":296},[282,818,819],{"class":300}," db",[282,821,822],{"class":300}," push",[282,824,826],{"class":825},"sHwdD","        # applies supabase\u002Fmigrations\u002F (tables, RLS, signup trigger)\n",[828,829],"br",{},[209,831,832,833,836],{},"No IPv6? ",[239,834,835],{},"db push"," will fail."," The direct connection ",[239,839,840],{},"db.\u003Cref>.supabase.co:5432"," is IPv6-only, so on a network without IPv6 ",[239,843,835],{}," errors with ",[239,846,847],{},"socket is not connected",". Push over the IPv4 ",[209,850,851],{},"session pooler"," instead - copy its string from the dashboard's ",[209,854,855],{},"Connect"," tab (Session mode):",[273,858,860],{"className":275,"code":859,"filename":726,"language":277,"meta":278,"style":278},"supabase db push --db-url \"postgresql:\u002F\u002Fpostgres.\u003Cref>:\u003Cpw>@aws-1-\u003Cregion>.pooler.supabase.com:5432\u002Fpostgres\"\n",[239,861,862],{"__ignoreMap":278},[282,863,864,866,868,870,873,875,878],{"class":284,"line":285},[282,865,742],{"class":296},[282,867,819],{"class":300},[282,869,822],{"class":300},[282,871,872],{"class":300}," --db-url",[282,874,307],{"class":292},[282,876,877],{"class":300},"postgresql:\u002F\u002Fpostgres.\u003Cref>:\u003Cpw>@aws-1-\u003Cregion>.pooler.supabase.com:5432\u002Fpostgres",[282,879,880],{"class":292},"\"\n",[828,882],{},"This page uses two ",[209,885,886],{},"different"," connections: the ",[209,889,890],{},"pooler"," (IPv4) for ",[239,893,835],{}," and queries from your machine, and the ",[209,896,897],{},"direct"," connection for PowerSync's logical replication below (PowerSync's own infrastructure has IPv6).",[680,900,901,902,905,906,909,910,913,914,947,949,952,953,956,957,960,961,964,965,968,969,971,972,974,975,977],{},"Ensure the ",[239,903,904],{},"powersync"," publication exists for the synced tables - ",[239,907,908],{},"supabase db query"," runs the setup SQL against the linked project, no ",[239,911,912],{},"psql"," required:",[273,915,917],{"className":275,"code":916,"filename":726,"language":277,"meta":278,"style":278},"supabase db query --linked -f supabase\u002Fpowersync_setup.sql\n# over the pooler a multi-statement file may hit the prepared-statement limit;\n# then run the publication's DROP and CREATE as separate `supabase db query \"...\"` calls\n",[239,918,919,937,942],{"__ignoreMap":278},[282,920,921,923,925,928,931,934],{"class":284,"line":285},[282,922,742],{"class":296},[282,924,819],{"class":300},[282,926,927],{"class":300}," query",[282,929,930],{"class":300}," --linked",[282,932,933],{"class":300}," -f",[282,935,936],{"class":300}," supabase\u002Fpowersync_setup.sql\n",[282,938,939],{"class":284,"line":322},[282,940,941],{"class":825},"# over the pooler a multi-statement file may hit the prepared-statement limit;\n",[282,943,944],{"class":284,"line":337},[282,945,946],{"class":825},"# then run the publication's DROP and CREATE as separate `supabase db query \"...\"` calls\n",[828,948],{},[209,950,951],{},"Cloud vs self-hosted role."," The script's ",[239,954,955],{},"powersync_role"," (REPLICATION + BYPASSRLS) is for ",[209,958,959],{},"self-hosted"," PowerSync. On ",[209,962,963],{},"Supabase Cloud"," the ",[239,966,967],{},"postgres"," role already has REPLICATION + BYPASSRLS (and a non-superuser ",[239,970,967],{}," cannot create a REPLICATION role anyway), so PowerSync Cloud connects as ",[239,973,967],{}," - there only the ",[239,976,904],{}," publication matters.",[680,979,980,981,984],{},"RLS policies and the ",[239,982,983],{},"user_settings"," signup trigger come with the migrations - nothing to configure manually.",[197,986,987,988,991,992,997,998,1001],{},"Self-hosting Supabase instead of the cloud? The Finapp-specific steps are identical (apply the migrations, run ",[239,989,990],{},"powersync_setup.sql",") - follow the ",[214,993,996],{"href":994,"rel":995},"https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fself-hosting\u002Fdocker",[218],"self-hosting with Docker guide"," for the stack itself, then swap the ",[239,999,1000],{},"*.supabase.co"," URL and JWKS endpoint for your instance's own.",[396,1003,1005],{"id":1004},"powersync-production-service","PowerSync (production service)",[197,1007,1008],{},"Two options; both replicate from the Supabase Postgres and validate Supabase JWTs via JWKS:",[677,1010,1011,1138],{},[680,1012,1013,1016,1017,1019,1020,1023,1024,1029,1030,242,1035,1038,1039,1125,1127,1128,1131,1132,220,1135,1137],{},[209,1014,1015],{},"PowerSync Cloud"," (managed): point the source database at the Supabase ",[209,1018,897],{}," connection string (not the pooler - logical replication needs a direct connection), enable Supabase auth, and use the sync rules from ",[239,1021,1022],{},"app\u002Fpowersync\u002Fconfig\u002Fsync-config.yaml",". Do it in the dashboard (",[214,1025,1028],{"href":1026,"rel":1027},"https:\u002F\u002Fdocs.powersync.com\u002Fintegrations\u002Fsupabase\u002Fguide",[218],"Supabase + PowerSync guide",") or with the official ",[214,1031,1034],{"href":1032,"rel":1033},"https:\u002F\u002Fdocs.powersync.com\u002Ftools\u002Fcli",[218],"PowerSync CLI",[239,1036,1037],{},"npm i -g powersync","), which keeps the instance config as code:",[273,1040,1042],{"className":275,"code":1041,"filename":726,"language":277,"meta":278,"style":278},"powersync login\npowersync init cloud                     # scaffolds powersync\u002Fservice.yaml + sync-config.yaml\n# service.yaml: replication -> Supabase direct conn (username postgres);\n#   client_auth -> supabase: true + jwks_uri https:\u002F\u002F\u003Cref>.supabase.co\u002Fauth\u002Fv1\u002F.well-known\u002Fjwks.json\n# use app\u002Fpowersync\u002Fconfig\u002Fsync-config.yaml as the sync rules\npowersync link cloud --instance-id \u003Cid>  # or --create --org-id \u003Corg> --project-id \u003Cproject>\nPOWERSYNC_DATABASE_PASSWORD=\u003Cpw> powersync deploy   # deploys service-config + sync-config\n",[239,1043,1044,1051,1064,1069,1074,1079,1102],{"__ignoreMap":278},[282,1045,1046,1048],{"class":284,"line":285},[282,1047,904],{"class":296},[282,1049,1050],{"class":300}," login\n",[282,1052,1053,1055,1058,1061],{"class":284,"line":322},[282,1054,904],{"class":296},[282,1056,1057],{"class":300}," init",[282,1059,1060],{"class":300}," cloud",[282,1062,1063],{"class":825},"                     # scaffolds powersync\u002Fservice.yaml + sync-config.yaml\n",[282,1065,1066],{"class":284,"line":337},[282,1067,1068],{"class":825},"# service.yaml: replication -> Supabase direct conn (username postgres);\n",[282,1070,1071],{"class":284,"line":355},[282,1072,1073],{"class":825},"#   client_auth -> supabase: true + jwks_uri https:\u002F\u002F\u003Cref>.supabase.co\u002Fauth\u002Fv1\u002F.well-known\u002Fjwks.json\n",[282,1075,1076],{"class":284,"line":369},[282,1077,1078],{"class":825},"# use app\u002Fpowersync\u002Fconfig\u002Fsync-config.yaml as the sync rules\n",[282,1080,1081,1083,1085,1087,1090,1092,1095,1097,1099],{"class":284,"line":382},[282,1082,904],{"class":296},[282,1084,799],{"class":300},[282,1086,1060],{"class":300},[282,1088,1089],{"class":300}," --instance-id",[282,1091,757],{"class":292},[282,1093,1094],{"class":300},"i",[282,1096,763],{"class":288},[282,1098,766],{"class":292},[282,1100,1101],{"class":825},"  # or --create --org-id \u003Corg> --project-id \u003Cproject>\n",[282,1103,1105,1108,1111,1114,1116,1119,1122],{"class":284,"line":1104},7,[282,1106,1107],{"class":288},"POWERSYNC_DATABASE_PASSWORD",[282,1109,1110],{"class":292},"=\u003C",[282,1112,1113],{"class":300},"pw",[282,1115,766],{"class":292},[282,1117,1118],{"class":296}," powersync",[282,1120,1121],{"class":300}," deploy",[282,1123,1124],{"class":825},"   # deploys service-config + sync-config\n",[828,1126],{},"A new PowerSync Cloud ",[209,1129,1130],{},"project"," ships with ",[209,1133,1134],{},"Production",[209,1136,62],{}," instances already created - deploy to one of those instead of creating a third. Projects are created in the dashboard; the CLI manages instances and config, not projects.",[680,1139,1140,1143,1144,1147,1148,1151,1152,1155,1156,1159,1160,1163,1164,1167,1168,220,1173,1178],{},[209,1141,1142],{},"Self-hosted",": ",[239,1145,1146],{},"app\u002Fpowersync\u002F"," (docker-compose + ",[239,1149,1150],{},"config\u002Fservice.yaml",") is the template, but it is ",[209,1153,1154],{},"local-dev only"," - its credentials are committed for a localhost stack. For production: move the replication URI, the bucket-storage Postgres URI, and the API token into secrets; point ",[239,1157,1158],{},"client_auth.jwks_uri"," at ",[239,1161,1162],{},"https:\u002F\u002F\u003Cproject-ref>.supabase.co\u002Fauth\u002Fv1\u002F.well-known\u002Fjwks.json"," and remove ",[239,1165,1166],{},"allow_local_jwks","; give the service its own Postgres for bucket storage (it auto-migrates its schema); terminate TLS in front of port 8080. See the ",[214,1169,1172],{"href":1170,"rel":1171},"https:\u002F\u002Fdocs.powersync.com\u002Fself-hosting\u002Fgetting-started",[218],"self-hosting guide",[214,1174,1177],{"href":1175,"rel":1176},"https:\u002F\u002Fdocs.powersync.com\u002Fconfiguration\u002Fpowersync-service\u002Fself-hosted-instances",[218],"instance configuration reference"," for the full config.",[197,1180,1181,1183],{},[239,1182,510],{}," is the public URL of that service.",[396,1185,1187,1188,1191],{"id":1186},"exchange-rates-fetch-rates","Exchange rates (",[239,1189,1190],{},"fetch-rates",")",[197,1193,1194,1195,1198,1199,242,1202,1205,1206,1209,1210,1213,1214,1217,1218,1221,1222,1225,1226,1228],{},"Currency conversion needs the ",[239,1196,1197],{},"rates"," table populated. A Supabase ",[209,1200,1201],{},"edge function",[239,1203,1204],{},"app\u002Fsupabase\u002Ffunctions\u002Ffetch-rates\u002F",") does that: it pulls two USD-based sources - ",[209,1207,1208],{},"Coinbase"," (no API key, ~all fiat + crypto, the base layer) and ",[209,1211,1212],{},"Open Exchange Rates"," (better fiat precision, overlaid on top) - merges them into one map and writes a single ",[239,1215,1216],{},"source = 'merged'"," row per day. The client reads only the latest row (",[239,1219,1220],{},"SELECT * FROM rates ORDER BY date DESC LIMIT 1","), so the merge happens at write time. Writes use the auto-injected ",[239,1223,1224],{},"service_role"," - ",[239,1227,1197],{}," has a global read policy but no client INSERT policy.",[708,1230,1231,1257,1297],{},[680,1232,1233,1234],{},"Deploy the function:",[273,1235,1237],{"className":275,"code":1236,"filename":726,"language":277,"meta":278,"style":278},"cd app\nsupabase functions deploy fetch-rates\n",[239,1238,1239,1245],{"__ignoreMap":278},[282,1240,1241,1243],{"class":284,"line":285},[282,1242,734],{"class":733},[282,1244,737],{"class":300},[282,1246,1247,1249,1252,1254],{"class":284,"line":322},[282,1248,742],{"class":296},[282,1250,1251],{"class":300}," functions",[282,1253,1121],{"class":300},[282,1255,1256],{"class":300}," fetch-rates\n",[680,1258,1259,1262,1263,1268,1269],{},[209,1260,1261],{},"OER key (optional)."," Set it as a function secret to add precise fiat; without it the function runs Coinbase-only (still ~630 currencies, crypto included). Get a free App ID at ",[214,1264,1267],{"href":1265,"rel":1266},"https:\u002F\u002Fopenexchangerates.org\u002Fsignup",[218],"openexchangerates.org",":",[273,1270,1272],{"className":275,"code":1271,"filename":726,"language":277,"meta":278,"style":278},"supabase secrets set OPEN_EXCHANGE_RATES_KEY=\u003Capp_id>\n",[239,1273,1274],{"__ignoreMap":278},[282,1275,1276,1278,1281,1284,1287,1290,1293,1295],{"class":284,"line":285},[282,1277,742],{"class":296},[282,1279,1280],{"class":300}," secrets",[282,1282,1283],{"class":300}," set",[282,1285,1286],{"class":300}," OPEN_EXCHANGE_RATES_KEY=",[282,1288,1289],{"class":292},"\u003C",[282,1291,1292],{"class":300},"app_i",[282,1294,763],{"class":288},[282,1296,792],{"class":292},[680,1298,1299,1302,1303,1306,1307,1310,1311,1314,1315,1159,1318,1321,1322,1325,1326,1329,1330],{},[209,1300,1301],{},"Daily schedule."," The migration ",[239,1304,1305],{},"supabase\u002Fmigrations\u002F*_schedule_fetch_rates.sql"," enables ",[239,1308,1309],{},"pg_cron"," + ",[239,1312,1313],{},"pg_net"," and schedules ",[239,1316,1317],{},"fetch-rates-daily",[239,1319,1320],{},"06:00 UTC","; it ships with ",[239,1323,1324],{},"supabase db push"," (same IPv6\u002Fpooler caveat as above). The cron reads the function URL and anon key from ",[209,1327,1328],{},"Vault secrets"," - kept out of the migration so it stays portable - so set them once per project (until they exist the scheduled run is a harmless no-op):",[273,1331,1333],{"className":275,"code":1332,"filename":726,"language":277,"meta":278,"style":278},"supabase db query --linked \"select vault.create_secret('https:\u002F\u002F\u003Cref>.supabase.co', 'project_url')\"\nsupabase db query --linked \"select vault.create_secret('\u003Canon-key>', 'anon_key')\"\n",[239,1334,1335,1352],{"__ignoreMap":278},[282,1336,1337,1339,1341,1343,1345,1347,1350],{"class":284,"line":285},[282,1338,742],{"class":296},[282,1340,819],{"class":300},[282,1342,927],{"class":300},[282,1344,930],{"class":300},[282,1346,307],{"class":292},[282,1348,1349],{"class":300},"select vault.create_secret('https:\u002F\u002F\u003Cref>.supabase.co', 'project_url')",[282,1351,880],{"class":292},[282,1353,1354,1356,1358,1360,1362,1364,1367],{"class":284,"line":322},[282,1355,742],{"class":296},[282,1357,819],{"class":300},[282,1359,927],{"class":300},[282,1361,930],{"class":300},[282,1363,307],{"class":292},[282,1365,1366],{"class":300},"select vault.create_secret('\u003Canon-key>', 'anon_key')",[282,1368,880],{"class":292},[228,1370,1371,1388],{"color":230,"icon":516},[197,1372,1373,1376,1377,1380,1381,1384,1385,1387],{},[209,1374,1375],{},"Seed it once."," ",[239,1378,1379],{},"supabase functions"," has no ",[239,1382,1383],{},"invoke"," subcommand - trigger a run with a plain POST so ",[239,1386,1197],{}," is populated before the first scheduled run:",[273,1389,1391],{"className":275,"code":1390,"filename":726,"language":277,"meta":278,"style":278},"curl -X POST \"https:\u002F\u002F\u003Cref>.supabase.co\u002Ffunctions\u002Fv1\u002Ffetch-rates\" \\\n  -H \"Authorization: Bearer \u003Canon-key>\"\n",[239,1392,1393,1411],{"__ignoreMap":278},[282,1394,1395,1397,1399,1402,1404,1407,1409],{"class":284,"line":285},[282,1396,325],{"class":296},[282,1398,328],{"class":300},[282,1400,1401],{"class":300}," POST",[282,1403,307],{"class":292},[282,1405,1406],{"class":300},"https:\u002F\u002F\u003Cref>.supabase.co\u002Ffunctions\u002Fv1\u002Ffetch-rates",[282,1408,316],{"class":292},[282,1410,334],{"class":288},[282,1412,1413,1415,1417,1420],{"class":284,"line":322},[282,1414,340],{"class":300},[282,1416,307],{"class":292},[282,1418,1419],{"class":300},"Authorization: Bearer \u003Canon-key>",[282,1421,880],{"class":292},[201,1423,1425],{"id":1424},"google-sign-in-production","Google Sign-In (Production)",[197,1427,1428,1429,1432,1433,1436],{},"In production the Google provider is configured in the ",[209,1430,1431],{},"hosted Supabase dashboard",", not in ",[239,1434,1435],{},"config.toml"," (that file only drives the local stack).",[708,1438,1439,1460,1467],{},[680,1440,1441,1442,1447,1448,1451,1452],{},"In the ",[214,1443,1446],{"href":1444,"rel":1445},"https:\u002F\u002Fconsole.cloud.google.com\u002Fapis\u002Fcredentials",[218],"Google Cloud Console",", create (or reuse) an ",[209,1449,1450],{},"OAuth 2.0 Client ID"," and add your project's auth callback as an authorized redirect URI:",[273,1453,1458],{"className":1454,"code":1456,"language":1457,"meta":278},[1455],"language-text","https:\u002F\u002F\u003Cyour-supabase-project>.supabase.co\u002Fauth\u002Fv1\u002Fcallback\n","text",[239,1459,1456],{"__ignoreMap":278},[680,1461,1462,1463,1466],{},"In the Supabase dashboard: ",[209,1464,1465],{},"Authentication -> Providers -> Google",", enable it and paste the client id\u002Fsecret.",[680,1468,1469,1470,1473,1474,1477,1478,1481,1482,1485,1486,1489,1490,1493],{},"In ",[209,1471,1472],{},"Authentication -> URL Configuration",", set ",[209,1475,1476],{},"Site URL"," to your deployed app origin (e.g. ",[239,1479,1480],{},"https:\u002F\u002Ffinapp.ilko.me",") and add it (with a ",[239,1483,1484],{},"\u002F**"," wildcard) to ",[209,1487,1488],{},"Redirect URLs",", so the OAuth return to ",[239,1491,1492],{},"\u002Flogin"," is allowed.",[228,1495,1496,1507,1564],{"color":230,"icon":516},[197,1497,1498,1501,1502,722],{},[209,1499,1500],{},"Without the dashboard."," Steps 2-3 can be done in a single Management API call (needs a ",[214,1503,1506],{"href":1504,"rel":1505},"https:\u002F\u002Fsupabase.com\u002Fdashboard\u002Faccount\u002Ftokens",[218],"Supabase access token",[273,1508,1510],{"className":275,"code":1509,"filename":726,"language":277,"meta":278,"style":278},"curl -X PATCH \"https:\u002F\u002Fapi.supabase.com\u002Fv1\u002Fprojects\u002F\u003Cref>\u002Fconfig\u002Fauth\" \\\n  -H \"Authorization: Bearer $SUPABASE_ACCESS_TOKEN\" -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"external_google_enabled\":true,\"external_google_client_id\":\"\u003Cid>\",\"external_google_secret\":\"\u003Csecret>\",\"site_url\":\"https:\u002F\u002F\u003Cyour-app>\",\"uri_allow_list\":\"https:\u002F\u002F\u003Cyour-app>\u002F**\"}'\n",[239,1511,1512,1529,1553],{"__ignoreMap":278},[282,1513,1514,1516,1518,1520,1522,1525,1527],{"class":284,"line":285},[282,1515,325],{"class":296},[282,1517,328],{"class":300},[282,1519,331],{"class":300},[282,1521,307],{"class":292},[282,1523,1524],{"class":300},"https:\u002F\u002Fapi.supabase.com\u002Fv1\u002Fprojects\u002F\u003Cref>\u002Fconfig\u002Fauth",[282,1526,316],{"class":292},[282,1528,334],{"class":288},[282,1530,1531,1533,1535,1537,1540,1542,1545,1547,1549,1551],{"class":284,"line":322},[282,1532,340],{"class":300},[282,1534,307],{"class":292},[282,1536,345],{"class":300},[282,1538,1539],{"class":288},"$SUPABASE_ACCESS_TOKEN",[282,1541,316],{"class":292},[282,1543,1544],{"class":300}," -H",[282,1546,307],{"class":292},[282,1548,362],{"class":300},[282,1550,316],{"class":292},[282,1552,334],{"class":288},[282,1554,1555,1557,1559,1562],{"class":284,"line":337},[282,1556,385],{"class":300},[282,1558,388],{"class":292},[282,1560,1561],{"class":300},"{\"external_google_enabled\":true,\"external_google_client_id\":\"\u003Cid>\",\"external_google_secret\":\"\u003Csecret>\",\"site_url\":\"https:\u002F\u002F\u003Cyour-app>\",\"uri_allow_list\":\"https:\u002F\u002F\u003Cyour-app>\u002F**\"}",[282,1563,394],{"class":292},[197,1565,1566,1567,1570,1571,1574,1575,1577,1578,1581,1582,1585],{},"Do ",[209,1568,1569],{},"not"," use ",[239,1572,1573],{},"supabase config push"," for a hosted project - it pushes the whole ",[239,1576,1435],{},", whose ",[239,1579,1580],{},"site_url"," is the local ",[239,1583,1584],{},"http:\u002F\u002Flocalhost:3050",", overwriting the remote URL config. The Management API changes only the fields you send.",[197,1587,1588,1589,1591,1592,1594],{},"Google needs no extra app env vars - it reuses ",[239,1590,490],{}," \u002F ",[239,1593,500],{},". Supabase issues its own JWT regardless of the sign-in provider, so PowerSync keeps validating it via JWKS unchanged.",[201,1596,1598],{"id":1597},"migrating-to-a-new-prod-project","Migrating to a New Prod Project",[197,1600,1601],{},"Standing up a fresh prod project alongside an existing environment (promoting a dev\u002Flocal dataset, or cutting over from a different backend) adds three steps the generic setup above doesn't cover: importing existing data, pointing Vercel at the new project, and flipping the deploy branch.",[396,1603,1605],{"id":1604},"importing-existing-data-userid-remap","Importing existing data (userId remap)",[197,1607,1608,1609,246,1612,246,1615,246,1618,1620,1621,1623,1624,1627,1628,1630,1631,1634],{},"Four tables are per-user: ",[239,1610,1611],{},"categories",[239,1613,1614],{},"wallets",[239,1616,1617],{},"trns",[239,1619,983],{},". ",[239,1622,1197],{}," is global - ",[209,1625,1626],{},"don't copy it"," (it's populated by ",[239,1629,1190],{},"). The copy is Postgres -> Postgres (the schema is identical across environments), rewriting ",[239,1632,1633],{},"userId"," to the destination project's auth uid.",[228,1636,1638,1639,1641,1642,1645,1646,1649,1650,1654],{"color":1637,"icon":231},"warning","The destination ",[239,1640,1633],{}," is the user's auth uid ",[209,1643,1644],{},"in the new project",", which only exists ",[209,1647,1648],{},"after they sign in once"," there. So finish ",[214,1651,1653],{"href":1652},"#google-sign-in-production","Google sign-in"," and do a first login (a preview deploy works), read the uid, then import.",[197,1656,1657,1658,1660,1661,1664,1665,1667,1668,1671],{},"Stream each table with ",[239,1659,1633],{}," rewritten in the ",[239,1662,1663],{},"SELECT",". For ",[239,1666,983],{}," remap ",[239,1669,1670],{},"id"," too (it equals the uid):",[273,1673,1675],{"className":275,"code":1674,"filename":726,"language":277,"meta":278,"style":278},"SRC_UID=\u003Csource-auth-uid>\nDST_UID=\u003Cnew-prod-auth-uid>\n\n# categories - wallets\u002Ftrns follow the same pattern (full column list, userId substituted).\npsql \"\u003Csource-conn>\" -c \"\\copy (\n  select id, '$DST_UID' as \\\"userId\\\", name, color, icon, \\\"parentId\\\",\n         \\\"showInLastUsed\\\", \\\"showInQuickSelector\\\", \\\"updatedAt\\\"\n  from public.categories where \\\"userId\\\" = '$SRC_UID'\n) to stdout\" \\\n| psql \"\u003Cprod-pooler-conn>\" -c \"\\copy public.categories from stdin\"\n",[239,1676,1677,1689,1701,1707,1712,1731,1762,1791,1811,1821],{"__ignoreMap":278},[282,1678,1679,1682,1684,1687],{"class":284,"line":285},[282,1680,1681],{"class":288},"SRC_UID",[282,1683,1110],{"class":292},[282,1685,1686],{"class":300},"source-auth-uid",[282,1688,792],{"class":292},[282,1690,1691,1694,1696,1699],{"class":284,"line":322},[282,1692,1693],{"class":288},"DST_UID",[282,1695,1110],{"class":292},[282,1697,1698],{"class":300},"new-prod-auth-uid",[282,1700,792],{"class":292},[282,1702,1703],{"class":284,"line":337},[282,1704,1706],{"emptyLinePlaceholder":1705},true,"\n",[282,1708,1709],{"class":284,"line":355},[282,1710,1711],{"class":825},"# categories - wallets\u002Ftrns follow the same pattern (full column list, userId substituted).\n",[282,1713,1714,1716,1718,1721,1723,1726,1728],{"class":284,"line":369},[282,1715,912],{"class":296},[282,1717,307],{"class":292},[282,1719,1720],{"class":300},"\u003Csource-conn>",[282,1722,316],{"class":292},[282,1724,1725],{"class":300}," -c",[282,1727,307],{"class":292},[282,1729,1730],{"class":300},"\\copy (\n",[282,1732,1733,1736,1739,1742,1745,1747,1749,1752,1754,1757,1759],{"class":284,"line":382},[282,1734,1735],{"class":300},"  select id, '",[282,1737,1738],{"class":288},"$DST_UID",[282,1740,1741],{"class":300},"' as ",[282,1743,1744],{"class":288},"\\\"",[282,1746,1633],{"class":300},[282,1748,1744],{"class":288},[282,1750,1751],{"class":300},", name, color, icon, ",[282,1753,1744],{"class":288},[282,1755,1756],{"class":300},"parentId",[282,1758,1744],{"class":288},[282,1760,1761],{"class":300},",\n",[282,1763,1764,1767,1770,1772,1774,1776,1779,1781,1783,1785,1788],{"class":284,"line":1104},[282,1765,1766],{"class":288},"         \\\"",[282,1768,1769],{"class":300},"showInLastUsed",[282,1771,1744],{"class":288},[282,1773,246],{"class":300},[282,1775,1744],{"class":288},[282,1777,1778],{"class":300},"showInQuickSelector",[282,1780,1744],{"class":288},[282,1782,246],{"class":300},[282,1784,1744],{"class":288},[282,1786,1787],{"class":300},"updatedAt",[282,1789,1790],{"class":288},"\\\"\n",[282,1792,1794,1797,1799,1801,1803,1806,1809],{"class":284,"line":1793},8,[282,1795,1796],{"class":300},"  from public.categories where ",[282,1798,1744],{"class":288},[282,1800,1633],{"class":300},[282,1802,1744],{"class":288},[282,1804,1805],{"class":300}," = '",[282,1807,1808],{"class":288},"$SRC_UID",[282,1810,394],{"class":300},[282,1812,1814,1817,1819],{"class":284,"line":1813},9,[282,1815,1816],{"class":300},") to stdout",[282,1818,316],{"class":292},[282,1820,334],{"class":288},[282,1822,1824,1827,1830,1832,1835,1837,1839,1841,1844],{"class":284,"line":1823},10,[282,1825,1826],{"class":292},"|",[282,1828,1829],{"class":296}," psql",[282,1831,307],{"class":292},[282,1833,1834],{"class":300},"\u003Cprod-pooler-conn>",[282,1836,316],{"class":292},[282,1838,1725],{"class":300},[282,1840,307],{"class":292},[282,1842,1843],{"class":300},"\\copy public.categories from stdin",[282,1845,880],{"class":292},[197,1847,1848,1849,1851,1852,1854,1855,1591,1857,1859,1860,1862],{},"Writes to the prod DB go over the IPv4 ",[209,1850,851],{}," (same IPv6 caveat as ",[239,1853,835],{},") as ",[239,1856,967],{},[239,1858,1224],{},", which bypasses RLS. The rows are inert until they match a real uid - once ",[239,1861,1633],{}," equals the signed-in user's uid, PowerSync streams them to the client on the next sync.",[396,1864,1866],{"id":1865},"pointing-vercel-at-the-new-project","Pointing Vercel at the new project",[197,1868,1869,1870,1873],{},"If your local ",[239,1871,1872],{},".vercel"," is linked to a different project (e.g. a dev one), re-link before setting prod env, otherwise the vars land on the wrong project:",[273,1875,1877],{"className":275,"code":1876,"filename":726,"language":277,"meta":278,"style":278},"vercel link            # choose the prod project\n",[239,1878,1879],{"__ignoreMap":278},[282,1880,1881,1884,1886],{"class":284,"line":285},[282,1882,1883],{"class":296},"vercel",[282,1885,799],{"class":300},[282,1887,1888],{"class":825},"            # choose the prod project\n",[197,1890,1891,1892,1591,1894,1591,1896,1898,1899,1902],{},"Set ",[239,1893,490],{},[239,1895,500],{},[239,1897,510],{}," to the prod values and remove any legacy-backend vars. ",[209,1900,1901],{},"Don't trigger a prod rebuild yet"," if the default branch still serves the old stack - wait for the branch cutover below, or prod builds the old code against the new env.",[396,1904,1906],{"id":1905},"branch-cutover-git-connected-prod","Branch cutover (git-connected prod)",[197,1908,1909],{},"When the new stack lives on a feature branch and prod auto-deploys from the default branch, advance the default branch instead of force-pushing:",[708,1911,1912,1970],{},[680,1913,1914,1917,1918],{},[209,1915,1916],{},"Archive the current default branch"," so the old stack stays recoverable:",[273,1919,1921],{"className":275,"code":1920,"filename":726,"language":277,"meta":278,"style":278},"git branch \u003Carchive> \u003Cdefault>      # e.g. git branch convex main\ngit push origin \u003Carchive>\n",[239,1922,1923,1953],{"__ignoreMap":278},[282,1924,1925,1927,1930,1932,1935,1938,1940,1942,1945,1948,1950],{"class":284,"line":285},[282,1926,610],{"class":296},[282,1928,1929],{"class":300}," branch",[282,1931,757],{"class":292},[282,1933,1934],{"class":300},"archiv",[282,1936,1937],{"class":288},"e",[282,1939,766],{"class":292},[282,1941,757],{"class":292},[282,1943,1944],{"class":300},"defaul",[282,1946,1947],{"class":288},"t",[282,1949,766],{"class":292},[282,1951,1952],{"class":825},"      # e.g. git branch convex main\n",[282,1954,1955,1957,1959,1962,1964,1966,1968],{"class":284,"line":322},[282,1956,610],{"class":296},[282,1958,822],{"class":300},[282,1960,1961],{"class":300}," origin",[282,1963,757],{"class":292},[282,1965,1934],{"class":300},[282,1967,1937],{"class":288},[282,1969,792],{"class":292},[680,1971,1972,1975,1976,1979,1980],{},[209,1973,1974],{},"Fast-forward the default branch"," to the feature tip (no force-push when the feature is a direct descendant - check with ",[239,1977,1978],{},"git rev-list --left-right --count \u003Cdefault>...\u003Cfeature>",", the left count must be 0):",[273,1981,1983],{"className":275,"code":1982,"filename":726,"language":277,"meta":278,"style":278},"git checkout \u003Cdefault>\ngit merge --ff-only \u003Cfeature>\ngit push origin \u003Cdefault>\n",[239,1984,1985,2000,2019],{"__ignoreMap":278},[282,1986,1987,1989,1992,1994,1996,1998],{"class":284,"line":285},[282,1988,610],{"class":296},[282,1990,1991],{"class":300}," checkout",[282,1993,757],{"class":292},[282,1995,1944],{"class":300},[282,1997,1947],{"class":288},[282,1999,792],{"class":292},[282,2001,2002,2004,2007,2010,2012,2015,2017],{"class":284,"line":322},[282,2003,610],{"class":296},[282,2005,2006],{"class":300}," merge",[282,2008,2009],{"class":300}," --ff-only",[282,2011,757],{"class":292},[282,2013,2014],{"class":300},"featur",[282,2016,1937],{"class":288},[282,2018,792],{"class":292},[282,2020,2021,2023,2025,2027,2029,2031,2033],{"class":284,"line":337},[282,2022,610],{"class":296},[282,2024,822],{"class":300},[282,2026,1961],{"class":300},[282,2028,757],{"class":292},[282,2030,1944],{"class":300},[282,2032,1947],{"class":288},[282,2034,792],{"class":292},[197,2036,2037,2038,2041,2042,2045],{},"The push triggers the prod Vercel build on the new stack. ",[209,2039,2040],{},"Rollback"," is cheap: re-point the default branch at the archive branch (",[239,2043,2044],{},"git push origin \u003Carchive>:\u003Cdefault> --force-with-lease","), or instant-rollback to the previous deploy in Vercel.",[201,2047,2049],{"id":2048},"premium-finapp-premium","Premium (Finapp Premium)",[197,2051,2052,2053,2058,2059,2062],{},"Finapp Premium lives in a separate repository (",[214,2054,2057],{"href":2055,"rel":2056},"https:\u002F\u002Fgithub.com\u002Filkome\u002Ffinapp-premium",[218],"finapp-premium",") and extends the base app as a ",[209,2060,2061],{},"Nuxt layer",".",[396,2064,2066],{"id":2065},"vercel-settings","Vercel Settings",[401,2068,2069,2077],{},[404,2070,2071],{},[407,2072,2073,2075],{},[410,2074,412],{},[410,2076,415],{},[417,2078,2079,2087,2093,2102,2110],{},[407,2080,2081,2083],{},[422,2082,424],{},[422,2084,2085],{},[239,2086,2062],{},[407,2088,2089,2091],{},[422,2090,434],{},[422,2092,437],{},[407,2094,2095,2097],{},[422,2096,442],{},[422,2098,2099],{},[239,2100,2101],{},"npx nuxt prepare base\u002Fapp && pnpm generate",[407,2103,2104,2106],{},[422,2105,452],{},[422,2107,2108],{},[239,2109,457],{},[407,2111,2112,2114],{},[422,2113,462],{},[422,2115,2116],{},[239,2117,467],{},[197,2119,2120,2121,2124,2125,2128],{},"No ",[239,2122,2123],{},"FINAPP_BASE_PATH"," environment variable is needed on Vercel. The default path is ",[239,2126,2127],{},".\u002Fbase\u002Fapp",", which points to the checked-out base submodule.",[396,2130,2132],{"id":2131},"how-it-works","How It Works",[677,2134,2135,2144,2157,2170,2180],{},[680,2136,2137,2140,2141,2143],{},[239,2138,2139],{},"nuxt.config.ts"," uses ",[239,2142,2123],{}," env var to resolve the base layer",[680,2145,2146,2147,2149,2150,2152,2153,2156],{},"Default (CI\u002FVercel): ",[239,2148,2127],{}," - the ",[239,2151,538],{}," subdirectory inside the ",[239,2154,2155],{},"base"," Git submodule",[680,2158,2159,2162,2163,2166,2167],{},[239,2160,2161],{},"base\u002F"," is a Git submodule pointing to the ",[239,2164,2165],{},"main"," branch of ",[239,2168,2169],{},"ilkome\u002Ffinapp",[680,2171,2172,2175,2176,2179],{},[239,2173,2174],{},"npx nuxt prepare base\u002Fapp"," generates ",[239,2177,2178],{},"base\u002Fapp\u002F.nuxt\u002Ftsconfig.json",", which Vite needs to resolve TypeScript config from the submodule layer",[680,2181,2182,2183,2186,2187,2190],{},"Local dev: set ",[239,2184,2185],{},"FINAPP_BASE_PATH=..\u002Fmono\u002Fapp"," in ",[239,2188,2189],{},".env"," to use the local monorepo with hot reload",[228,2192,2193],{"color":230,"icon":231},"Vercel initializes Git submodules during checkout. The base submodule repository must be public.",[396,2195,2197],{"id":2196},"local-development","Local Development",[197,2199,1891,2200,2186,2202,2204],{},[239,2201,2123],{},[239,2203,2189],{}," to point to the monorepo app directory:",[273,2206,2208],{"className":275,"code":2207,"filename":2189,"language":277,"meta":278,"style":278},"FINAPP_BASE_PATH=..\u002Fmono\u002Fapp\n",[239,2209,2210],{"__ignoreMap":278},[282,2211,2212,2214,2217],{"class":284,"line":285},[282,2213,2123],{"class":288},[282,2215,2216],{"class":292},"=",[282,2218,2219],{"class":300},"..\u002Fmono\u002Fapp\n",[396,2221,2223],{"id":2222},"git-submodule","Git Submodule",[197,2225,2226,2227,2166,2229,1268],{},"The submodule should point to the ",[239,2228,2165],{},[239,2230,2169],{},[273,2232,2234],{"className":275,"code":2233,"filename":726,"language":277,"meta":278,"style":278},"git submodule add -b main https:\u002F\u002Fgithub.com\u002Filkome\u002Ffinapp.git base\n",[239,2235,2236],{"__ignoreMap":278},[282,2237,2238,2240,2243,2246,2249,2252,2255],{"class":284,"line":285},[282,2239,610],{"class":296},[282,2241,2242],{"class":300}," submodule",[282,2244,2245],{"class":300}," add",[282,2247,2248],{"class":300}," -b",[282,2250,2251],{"class":300}," main",[282,2253,2254],{"class":300}," https:\u002F\u002Fgithub.com\u002Filkome\u002Ffinapp.git",[282,2256,2257],{"class":300}," base\n",[197,2259,2260],{},"After a fresh clone:",[273,2262,2264],{"className":275,"code":2263,"filename":726,"language":277,"meta":278,"style":278},"git submodule update --init\n",[239,2265,2266],{"__ignoreMap":278},[282,2267,2268,2270,2272,2275],{"class":284,"line":285},[282,2269,610],{"class":296},[282,2271,2242],{"class":300},[282,2273,2274],{"class":300}," update",[282,2276,2277],{"class":300}," --init\n",[2279,2280,2281],"style",{},"html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}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);}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"title":278,"searchDepth":322,"depth":322,"links":2283},[2284,2288,2289,2295,2296,2301],{"id":203,"depth":322,"text":204,"children":2285},[2286,2287],{"id":398,"depth":337,"text":399},{"id":542,"depth":337,"text":543},{"id":589,"depth":322,"text":590},{"id":664,"depth":322,"text":665,"children":2290},[2291,2292,2293],{"id":705,"depth":337,"text":706},{"id":1004,"depth":337,"text":1005},{"id":1186,"depth":337,"text":2294},"Exchange rates (fetch-rates)",{"id":1424,"depth":322,"text":1425},{"id":1597,"depth":322,"text":1598,"children":2297},[2298,2299,2300],{"id":1604,"depth":337,"text":1605},{"id":1865,"depth":337,"text":1866},{"id":1905,"depth":337,"text":1906},{"id":2048,"depth":322,"text":2049,"children":2302},[2303,2304,2305,2306],{"id":2065,"depth":337,"text":2066},{"id":2131,"depth":337,"text":2132},{"id":2196,"depth":337,"text":2197},{"id":2222,"depth":337,"text":2223},"Deploy Finapp and docs to Vercel from a monorepo, with Supabase and PowerSync as the backend.","md",null,{},{"icon":91},{"title":88,"description":2313},"How to deploy Finapp app and docs to Vercel with Supabase Postgres and PowerSync.","Gov-pw35oAbLXYZgMzX0iqvvaQa5A_CScHXBy0x3PfI",[2316,2318],{"title":83,"path":84,"stem":85,"description":2317,"icon":86,"children":-1},"Historical overview of the Finapp backend migration - from Firebase to Supabase + PowerSync.",{"title":93,"path":94,"stem":95,"description":2319,"icon":96,"children":-1},"Unit and E2E testing scripts.",1782114343672]