[{"data":1,"prerenderedAt":2295},["ShallowReactive",2],{"navigation_docs_ru":3,"-ru-development-deployment":167,"-ru-development-deployment-surround":2290},[4,61,101,146],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":60},"Руководство","i-lucide-book-open","\u002Fru\u002Fguide","ru\u002F1.guide",[10,15,20,25,30,35,40,45,50,55],{"title":11,"path":12,"stem":13,"icon":14},"Введение","\u002Fru\u002Fguide\u002Fintroduction","ru\u002F1.guide\u002F01.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Установка","\u002Fru\u002Fguide\u002Finstallation","ru\u002F1.guide\u002F02.installation","i-lucide-smartphone",{"title":21,"path":22,"stem":23,"icon":24},"Авторизация","\u002Fru\u002Fguide\u002Fauth","ru\u002F1.guide\u002F03.auth","i-lucide-lock",{"title":26,"path":27,"stem":28,"icon":29},"Кошельки","\u002Fru\u002Fguide\u002Fwallets","ru\u002F1.guide\u002F04.wallets","i-lucide-wallet",{"title":31,"path":32,"stem":33,"icon":34},"Категории","\u002Fru\u002Fguide\u002Fcategories","ru\u002F1.guide\u002F05.categories","i-lucide-tags",{"title":36,"path":37,"stem":38,"icon":39},"Транзакции","\u002Fru\u002Fguide\u002Ftransactions","ru\u002F1.guide\u002F06.transactions","i-lucide-receipt",{"title":41,"path":42,"stem":43,"icon":44},"Переводы","\u002Fru\u002Fguide\u002Ftransfers","ru\u002F1.guide\u002F07.transfers","i-lucide-arrow-left-right",{"title":46,"path":47,"stem":48,"icon":49},"Статистика","\u002Fru\u002Fguide\u002Fstatistics","ru\u002F1.guide\u002F08.statistics","i-lucide-bar-chart-3",{"title":51,"path":52,"stem":53,"icon":54},"Тема","\u002Fru\u002Fguide\u002Ftheme","ru\u002F1.guide\u002F09.theme","i-lucide-palette",{"title":56,"path":57,"stem":58,"icon":59},"Настройки","\u002Fru\u002Fguide\u002Fsettings","ru\u002F1.guide\u002F10.settings","i-lucide-settings",false,{"title":62,"icon":63,"path":64,"stem":65,"children":66,"page":60},"Разработка","i-lucide-code","\u002Fru\u002Fdevelopment","ru\u002F2.development",[67,71,76,81,86,91,96],{"title":16,"path":68,"stem":69,"icon":70},"\u002Fru\u002Fdevelopment\u002Finstallation","ru\u002F2.development\u002F01.installation","i-lucide-download",{"title":72,"path":73,"stem":74,"icon":75},"Граф кодовой базы","\u002Fru\u002Fdevelopment\u002Funderstand-anything","ru\u002F2.development\u002F02.understand-anything","i-lucide-network",{"title":77,"path":78,"stem":79,"icon":80},"Офлайн и PWA","\u002Fru\u002Fdevelopment\u002Foffline","ru\u002F2.development\u002F03.offline","i-lucide-wifi-off",{"title":82,"path":83,"stem":84,"icon":85},"История миграций данных","\u002Fru\u002Fdevelopment\u002Fmigration","ru\u002F2.development\u002F04.migration","i-lucide-database",{"title":87,"path":88,"stem":89,"icon":90},"Деплой","\u002Fru\u002Fdevelopment\u002Fdeployment","ru\u002F2.development\u002F05.deployment","i-lucide-rocket",{"title":92,"path":93,"stem":94,"icon":95},"Тестирование","\u002Fru\u002Fdevelopment\u002Ftesting","ru\u002F2.development\u002F06.testing","i-lucide-flask-conical",{"title":97,"path":98,"stem":99,"icon":100},"Дата-утилиты","\u002Fru\u002Fdevelopment\u002Fdate-utilities","ru\u002F2.development\u002F07.date-utilities","i-lucide-calendar",{"title":102,"icon":103,"path":104,"stem":105,"children":106,"page":60},"Справочник","i-lucide-file-code","\u002Fru\u002Freference","ru\u002F3.reference",[107,112,116,121,126,131,136,141],{"title":108,"path":109,"stem":110,"icon":111},"Архитектура","\u002Fru\u002Freference\u002Farchitecture","ru\u002F3.reference\u002F01.architecture","i-lucide-boxes",{"title":113,"path":114,"stem":115,"icon":44},"Типы транзакций","\u002Fru\u002Freference\u002Ftransaction-types","ru\u002F3.reference\u002F02.transaction-types",{"title":117,"path":118,"stem":119,"icon":120},"Синхронизация","\u002Fru\u002Freference\u002Fsync","ru\u002F3.reference\u002F03.sync","i-lucide-refresh-cw",{"title":122,"path":123,"stem":124,"icon":125},"Офлайн-first","\u002Fru\u002Freference\u002Foffline-first","ru\u002F3.reference\u002F04.offline-first","i-lucide-list-ordered",{"title":127,"path":128,"stem":129,"icon":130},"Тех. решения","\u002Fru\u002Freference\u002Ftech-decisions","ru\u002F3.reference\u002F05.tech-decisions","i-lucide-lightbulb",{"title":132,"path":133,"stem":134,"icon":135},"Валидация","\u002Fru\u002Freference\u002Fvalidation-strategy","ru\u002F3.reference\u002F06.validation-strategy","i-lucide-shield-check",{"title":137,"path":138,"stem":139,"icon":140},"Что изменилось со времён Firebase","\u002Fru\u002Freference\u002Ffirebase-migration","ru\u002F3.reference\u002F07.firebase-migration","i-lucide-hamburger",{"title":142,"path":143,"stem":144,"icon":145},"Производительность","\u002Fru\u002Freference\u002Fperformance","ru\u002F3.reference\u002F08.performance","i-lucide-gauge",{"title":147,"icon":148,"path":149,"stem":150,"children":151,"page":60},"Премиум","i-lucide-star","\u002Fru\u002Fpremium","ru\u002F4.premium",[152,157,162],{"title":153,"path":154,"stem":155,"icon":156},"Обзор","\u002Fru\u002Fpremium\u002Foverview","ru\u002F4.premium\u002F01.overview","i-lucide-layers",{"title":158,"path":159,"stem":160,"icon":161},"Telegram-бот","\u002Fru\u002Fpremium\u002Ftelegram-bot","ru\u002F4.premium\u002F02.telegram-bot","i-lucide-send",{"title":163,"path":164,"stem":165,"icon":166},"AI Chat","\u002Fru\u002Fpremium\u002Fai-chat","ru\u002F4.premium\u002F03.ai-chat","i-lucide-sparkles",{"id":168,"title":87,"body":169,"description":2282,"extension":2283,"links":2284,"meta":2285,"navigation":2286,"path":88,"seo":2287,"stem":89,"__hash__":2289},"docs_ru\u002Fru\u002F2.development\u002F05.deployment.md",{"type":170,"value":171,"toc":2257},"minimark",[172,176,181,203,371,376,444,447,490,516,520,563,567,573,635,638,642,652,668,678,682,959,976,980,983,1155,1160,1168,1206,1347,1400,1404,1415,1471,1562,1571,1575,1578,1582,1611,1631,1648,1822,1840,1844,1851,1866,1880,1884,1887,2012,2023,2027,2039,2043,2094,2105,2109,2165,2168,2172,2180,2195,2199,2206,2233,2236,2253],[173,174,175],"p",{},"Finapp - это монорепа на pnpm workspaces. Приложение и документация деплоятся из одного репозитория как два отдельных проекта в Vercel.",[177,178,180],"h2",{"id":179},"настройка-vercel","Настройка Vercel",[173,182,183,184,188,189,196,197,202],{},"Создайте ",[185,186,187],"strong",{},"два проекта"," в Vercel, оба указывают на один GitHub-репозиторий. Nuxt не требует особой настройки Vercel помимо настроек проектов ниже - базовая интеграция описана в ",[190,191,195],"a",{"href":192,"rel":193},"https:\u002F\u002Fnuxt.com\u002Fdeploy\u002Fvercel",[194],"nofollow","Nuxt on Vercel"," и ",[190,198,201],{"href":199,"rel":200},"https:\u002F\u002Fvercel.com\u002Fdocs\u002Fframeworks\u002Ffull-stack\u002Fnuxt",[194],"гайде Vercel по Nuxt",".",[204,205,208,248],"callout",{"color":206,"icon":207},"info","i-lucide-info",[173,209,210,213,214,218,219,222,223,222,226,222,229,222,232,222,235,222,238,241,242,247],{},[185,211,212],{},"Где что настраивать: файл vs проект."," Большинство build-настроек живут в ",[215,216,217],"code",{},"vercel.json"," каждого проекта (",[215,220,221],{},"framework",", ",[215,224,225],{},"buildCommand",[215,227,228],{},"installCommand",[215,230,231],{},"outputDirectory",[215,233,234],{},"ignoreCommand",[215,236,237],{},"env",[215,239,240],{},"rewrites","). ",[185,243,244,245],{},"Root Directory нельзя положить в ",[215,246,217],{},", потому что Vercel должен знать его заранее, чтобы понять где искать файл. Ставится один раз в dashboard (Settings -> General -> Root Directory) или через API:",[249,250,255],"pre",{"className":251,"code":252,"language":253,"meta":254,"style":254},"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","",[215,256,257,296,311,329,343,356],{"__ignoreMap":254},[258,259,262,266,270,274,278,281,284,287,290,293],"span",{"class":260,"line":261},"line",1,[258,263,265],{"class":264},"sTEyZ","TOKEN",[258,267,269],{"class":268},"sMK4o","=$(",[258,271,273],{"class":272},"sBMFI","jq",[258,275,277],{"class":276},"sfazB"," -r",[258,279,280],{"class":276}," .token",[258,282,283],{"class":268}," \"",[258,285,286],{"class":264},"$HOME",[258,288,289],{"class":276},"\u002FLibrary\u002FApplication Support\u002Fcom.vercel.cli\u002Fauth.json",[258,291,292],{"class":268},"\"",[258,294,295],{"class":268},")\n",[258,297,299,302,305,308],{"class":260,"line":298},2,[258,300,301],{"class":272},"curl",[258,303,304],{"class":276}," -X",[258,306,307],{"class":276}," PATCH",[258,309,310],{"class":264}," \\\n",[258,312,314,317,319,322,325,327],{"class":260,"line":313},3,[258,315,316],{"class":276},"  -H",[258,318,283],{"class":268},[258,320,321],{"class":276},"Authorization: Bearer ",[258,323,324],{"class":264},"$TOKEN",[258,326,292],{"class":268},[258,328,310],{"class":264},[258,330,332,334,336,339,341],{"class":260,"line":331},4,[258,333,316],{"class":276},[258,335,283],{"class":268},[258,337,338],{"class":276},"Content-Type: application\u002Fjson",[258,340,292],{"class":268},[258,342,310],{"class":264},[258,344,346,349,352,354],{"class":260,"line":345},5,[258,347,348],{"class":268},"  \"",[258,350,351],{"class":276},"https:\u002F\u002Fapi.vercel.com\u002Fv9\u002Fprojects\u002F\u003Cproject-name>?teamId=\u003Cteam-id>",[258,353,292],{"class":268},[258,355,310],{"class":264},[258,357,359,362,365,368],{"class":260,"line":358},6,[258,360,361],{"class":276},"  -d",[258,363,364],{"class":268}," '",[258,366,367],{"class":276},"{\"rootDirectory\":\"docs\"}",[258,369,370],{"class":268},"'\n",[372,373,375],"h3",{"id":374},"проект-приложения","Проект приложения",[377,378,379,392],"table",{},[380,381,382],"thead",{},[383,384,385,389],"tr",{},[386,387,388],"th",{},"Настройка",[386,390,391],{},"Значение",[393,394,395,406,414,424,434],"tbody",{},[383,396,397,401],{},[398,399,400],"td",{},"Root Directory",[398,402,403],{},[215,404,405],{},"app",[383,407,408,411],{},[398,409,410],{},"Framework Preset",[398,412,413],{},"Nuxt.js",[383,415,416,419],{},[398,417,418],{},"Build Command",[398,420,421],{},[215,422,423],{},"pnpm build",[383,425,426,429],{},[398,427,428],{},"Output Directory",[398,430,431],{},[215,432,433],{},".output\u002Fpublic",[383,435,436,439],{},[398,437,438],{},"Install Command",[398,440,441],{},[215,442,443],{},"pnpm install",[173,445,446],{},"Добавьте переменные окружения, указывающие на ваш хостируемый проект Supabase и сервис PowerSync:",[377,448,449,458],{},[380,450,451],{},[383,452,453,456],{},[386,454,455],{},"Переменная",[386,457,391],{},[393,459,460,470,480],{},[383,461,462,467],{},[398,463,464],{},[215,465,466],{},"VITE_SUPABASE_URL",[398,468,469],{},"URL вашего проекта Supabase",[383,471,472,477],{},[398,473,474],{},[215,475,476],{},"VITE_SUPABASE_ANON_KEY",[398,478,479],{},"Публичный (anon) ключ Supabase",[383,481,482,487],{},[398,483,484],{},[215,485,486],{},"VITE_POWERSYNC_URL",[398,488,489],{},"URL вашего сервиса PowerSync",[204,491,493,496,497,500,501,504,505,507,508,511,512,515],{"color":206,"icon":492},"i-lucide-terminal",[185,494,495],{},"Git-connected vs прямой CLI-деплой."," Схема из двух проектов выше - git-connected (пуши деплоятся автоматически). Для отдельного или временного окружения (например, проект ",[215,498,499],{},"finapp-dev"," на feature-ветке) можно обойтись без git-привязки и задеплоить рабочее дерево прямо из корня репозитория: создайте\u002Fслинкуйте проект (",[215,502,503],{},"vercel link",") с Root Directory = ",[215,506,405],{},", затем ",[215,509,510],{},"vercel deploy --prod",". Вся монорепа грузится из корня, а Vercel собирает ",[215,513,514],{},"app\u002F"," по настройке Root Directory.",[372,517,519],{"id":518},"проект-документации","Проект документации",[377,521,522,530],{},[380,523,524],{},[383,525,526,528],{},[386,527,388],{},[386,529,391],{},[393,531,532,541,547,555],{},[383,533,534,536],{},[398,535,400],{},[398,537,538],{},[215,539,540],{},"docs",[383,542,543,545],{},[398,544,410],{},[398,546,413],{},[383,548,549,551],{},[398,550,418],{},[398,552,553],{},[215,554,423],{},[383,556,557,559],{},[398,558,428],{},[398,560,561],{},[215,562,433],{},[177,564,566],{"id":565},"ignored-build-step","Ignored Build Step",[173,568,569,570,572],{},"Чтобы избежать лишних билдов, настройте ",[185,571,566],{}," в каждом проекте Vercel (Settings -> Git -> Ignored Build Step):",[574,575,576,612],"code-group",{},[249,577,580],{"className":251,"code":578,"filename":579,"language":253,"meta":254,"style":254},"git diff --quiet HEAD^ HEAD -- . ':!docs'\n","App",[215,581,582],{"__ignoreMap":254},[258,583,584,587,590,593,596,599,602,605,607,610],{"class":260,"line":261},[258,585,586],{"class":272},"git",[258,588,589],{"class":276}," diff",[258,591,592],{"class":276}," --quiet",[258,594,595],{"class":276}," HEAD^",[258,597,598],{"class":276}," HEAD",[258,600,601],{"class":276}," --",[258,603,604],{"class":276}," .",[258,606,364],{"class":268},[258,608,609],{"class":276},":!docs",[258,611,370],{"class":268},[249,613,616],{"className":251,"code":614,"filename":615,"language":253,"meta":254,"style":254},"git diff --quiet HEAD^ HEAD -- docs\n","Docs",[215,617,618],{"__ignoreMap":254},[258,619,620,622,624,626,628,630,632],{"class":260,"line":261},[258,621,586],{"class":272},[258,623,589],{"class":276},[258,625,592],{"class":276},[258,627,595],{"class":276},[258,629,598],{"class":276},[258,631,601],{"class":276},[258,633,634],{"class":276}," docs\n",[173,636,637],{},"Так приложение не будет передеплоиваться при изменениях только в документации, и наоборот.",[177,639,641],{"id":640},"бэкенд-supabase-powersync","Бэкенд: Supabase + PowerSync",[173,643,644,645,647,648,651],{},"Nuxt SPA хостится статически (выше описан Vercel, но подойдёт любой статический хостинг, отдающий ",[215,646,433],{}," после ",[215,649,650],{},"pnpm generate","). Бэкенд-сервисы отдельные:",[653,654,655,662],"ul",{},[656,657,658,661],"li",{},[185,659,660],{},"Supabase"," - хостируемый проект Supabase (облачный или self-hosted) предоставляет Postgres, Auth и путь записи через PostgREST\u002Fsupabase-js.",[656,663,664,667],{},[185,665,666],{},"PowerSync"," - сервис PowerSync (облачный или self-hosted) обеспечивает offline-first синхронизацию, реплицируя данные из Supabase Postgres через логическую репликацию.",[173,669,670,671,674,675,202],{},"Установите три ",[215,672,673],{},"VITE_*"," переменные в Vercel, указывающие на продакшен-проект Supabase и экземпляр PowerSync. Описание переменных - в разделе ",[190,676,16],{"href":677},"\u002Fru\u002Fdevelopment\u002Finstallation#%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%BD%D1%8B%D0%B5-%D0%BE%D0%BA%D1%80%D1%83%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F",[372,679,681],{"id":680},"supabase-продакшен-проект","Supabase (продакшен-проект)",[683,684,685,874,952],"ol",{},[656,686,687,688,691,692,697,698,802,805,812,813,816,817,819,820,823,824,827,828,831,832,856,858,859,862,863,866,867,869,870,873],{},"Создайте проект Supabase (через дашборд или ",[215,689,690],{},"supabase projects create","), привяжите репозиторий и примените схему (",[190,693,696],{"href":694,"rel":695},"https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fdeployment\u002Fdatabase-migrations",[194],"доки Supabase по миграциям","):",[249,699,702],{"className":251,"code":700,"filename":701,"language":253,"meta":254,"style":254},"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",[215,703,704,713,768,788],{"__ignoreMap":254},[258,705,706,710],{"class":260,"line":261},[258,707,709],{"class":708},"s2Zo4","cd",[258,711,712],{"class":276}," app\n",[258,714,715,718,721,724,727,730,733,736,739,742,745,747,750,753,755,758,760,762,765],{"class":260,"line":298},[258,716,717],{"class":272},"supabase",[258,719,720],{"class":276}," projects",[258,722,723],{"class":276}," create",[258,725,726],{"class":276}," finapp",[258,728,729],{"class":276}," --org-id",[258,731,732],{"class":268}," \u003C",[258,734,735],{"class":276},"org-i",[258,737,738],{"class":264},"d",[258,740,741],{"class":268},">",[258,743,744],{"class":276}," --region",[258,746,732],{"class":268},[258,748,749],{"class":276},"regio",[258,751,752],{"class":264},"n",[258,754,741],{"class":268},[258,756,757],{"class":276}," --db-password",[258,759,732],{"class":268},[258,761,173],{"class":276},[258,763,764],{"class":264},"w",[258,766,767],{"class":268},">\n",[258,769,770,772,775,778,780,783,786],{"class":260,"line":313},[258,771,717],{"class":272},[258,773,774],{"class":276}," link",[258,776,777],{"class":276}," --project-ref",[258,779,732],{"class":268},[258,781,782],{"class":276},"project-re",[258,784,785],{"class":264},"f",[258,787,767],{"class":268},[258,789,790,792,795,798],{"class":260,"line":331},[258,791,717],{"class":272},[258,793,794],{"class":276}," db",[258,796,797],{"class":276}," push",[258,799,801],{"class":800},"sHwdD","        # applies supabase\u002Fmigrations\u002F (tables, RLS, signup trigger)\n",[803,804],"br",{},[185,806,807,808,811],{},"Нет IPv6? ",[215,809,810],{},"db push"," упадёт."," Прямое подключение ",[215,814,815],{},"db.\u003Cref>.supabase.co:5432"," - только IPv6, поэтому в сети без IPv6 ",[215,818,810],{}," падает с ",[215,821,822],{},"socket is not connected",". Пушьте через IPv4 ",[185,825,826],{},"session-пулер"," - строку возьмите во вкладке ",[185,829,830],{},"Connect"," дашборда (режим Session):",[249,833,835],{"className":251,"code":834,"filename":701,"language":253,"meta":254,"style":254},"supabase db push --db-url \"postgresql:\u002F\u002Fpostgres.\u003Cref>:\u003Cpw>@aws-1-\u003Cregion>.pooler.supabase.com:5432\u002Fpostgres\"\n",[215,836,837],{"__ignoreMap":254},[258,838,839,841,843,845,848,850,853],{"class":260,"line":261},[258,840,717],{"class":272},[258,842,794],{"class":276},[258,844,797],{"class":276},[258,846,847],{"class":276}," --db-url",[258,849,283],{"class":268},[258,851,852],{"class":276},"postgresql:\u002F\u002Fpostgres.\u003Cref>:\u003Cpw>@aws-1-\u003Cregion>.pooler.supabase.com:5432\u002Fpostgres",[258,854,855],{"class":268},"\"\n",[803,857],{},"На этой странице используются два ",[185,860,861],{},"разных"," подключения: ",[185,864,865],{},"пулер"," (IPv4) - для ",[215,868,810],{}," и запросов с вашей машины, и ",[185,871,872],{},"прямое"," подключение - для логической репликации PowerSync ниже (у инфраструктуры PowerSync IPv6 есть).",[656,875,876,877,880,881,884,885,888,889,922,924,927,928,931,932,935,936,939,940,943,944,946,947,949,950,202],{},"Убедитесь, что публикация ",[215,878,879],{},"powersync"," существует для синхронизируемых таблиц - ",[215,882,883],{},"supabase db query"," выполняет setup-SQL на привязанном проекте, ",[215,886,887],{},"psql"," не нужен:",[249,890,892],{"className":251,"code":891,"filename":701,"language":253,"meta":254,"style":254},"supabase db query --linked -f supabase\u002Fpowersync_setup.sql\n# через пулер мультистейтмент-файл может упереться в prepared-statement limit;\n# тогда выполните DROP и CREATE публикации отдельными вызовами `supabase db query \"...\"`\n",[215,893,894,912,917],{"__ignoreMap":254},[258,895,896,898,900,903,906,909],{"class":260,"line":261},[258,897,717],{"class":272},[258,899,794],{"class":276},[258,901,902],{"class":276}," query",[258,904,905],{"class":276}," --linked",[258,907,908],{"class":276}," -f",[258,910,911],{"class":276}," supabase\u002Fpowersync_setup.sql\n",[258,913,914],{"class":260,"line":298},[258,915,916],{"class":800},"# через пулер мультистейтмент-файл может упереться в prepared-statement limit;\n",[258,918,919],{"class":260,"line":313},[258,920,921],{"class":800},"# тогда выполните DROP и CREATE публикации отдельными вызовами `supabase db query \"...\"`\n",[803,923],{},[185,925,926],{},"Роль: cloud vs self-hosted."," ",[215,929,930],{},"powersync_role"," (REPLICATION + BYPASSRLS) из скрипта нужен для ",[185,933,934],{},"self-hosted"," PowerSync. В ",[185,937,938],{},"Supabase Cloud"," у роли ",[215,941,942],{},"postgres"," уже есть REPLICATION + BYPASSRLS (а не-суперюзер ",[215,945,942],{}," всё равно не может создать REPLICATION-роль), поэтому PowerSync Cloud подключается как ",[215,948,942],{}," - там важна только публикация ",[215,951,879],{},[656,953,954,955,958],{},"RLS-политики и триггер ",[215,956,957],{},"user_settings"," при регистрации приходят вместе с миграциями - вручную ничего настраивать не нужно.",[173,960,961,962,965,966,971,972,975],{},"Поднимаете self-hosted Supabase вместо облака? Шаги, специфичные для Finapp, идентичны (применить миграции, выполнить ",[215,963,964],{},"powersync_setup.sql",") - сам стек разворачивается по ",[190,967,970],{"href":968,"rel":969},"https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fself-hosting\u002Fdocker",[194],"гайду self-hosting с Docker",", после чего подставьте вместо ",[215,973,974],{},"*.supabase.co"," URL и JWKS-эндпоинт адреса своего инстанса.",[372,977,979],{"id":978},"powersync-продакшен-сервис","PowerSync (продакшен-сервис)",[173,981,982],{},"Два варианта; оба реплицируют из Supabase Postgres и проверяют JWT Supabase через JWKS:",[653,984,985,1115],{},[656,986,987,990,991,994,995,998,999,1004,1005,1010,1011,1014,1015,1101,1103,1104,1107,1108,196,1111,1114],{},[185,988,989],{},"PowerSync Cloud"," (управляемый): укажите в качестве исходной базы ",[185,992,993],{},"прямую"," строку подключения Supabase (не pooler - логической репликации нужно прямое подключение), включите Supabase auth и используйте правила синхронизации из ",[215,996,997],{},"app\u002Fpowersync\u002Fconfig\u002Fsync-config.yaml",". Сделать это можно в дашборде (",[190,1000,1003],{"href":1001,"rel":1002},"https:\u002F\u002Fdocs.powersync.com\u002Fintegrations\u002Fsupabase\u002Fguide",[194],"гайд Supabase + PowerSync",") или официальным ",[190,1006,1009],{"href":1007,"rel":1008},"https:\u002F\u002Fdocs.powersync.com\u002Ftools\u002Fcli",[194],"PowerSync CLI"," (",[215,1012,1013],{},"npm i -g powersync","), который держит конфиг инстанса как код:",[249,1016,1018],{"className":251,"code":1017,"filename":701,"language":253,"meta":254,"style":254},"powersync login\npowersync init cloud                     # создаёт powersync\u002Fservice.yaml + sync-config.yaml\n# service.yaml: replication -> прямое подключение Supabase (username postgres);\n#   client_auth -> supabase: true + jwks_uri https:\u002F\u002F\u003Cref>.supabase.co\u002Fauth\u002Fv1\u002F.well-known\u002Fjwks.json\n# в качестве sync rules используйте app\u002Fpowersync\u002Fconfig\u002Fsync-config.yaml\npowersync link cloud --instance-id \u003Cid>  # или --create --org-id \u003Corg> --project-id \u003Cproject>\nPOWERSYNC_DATABASE_PASSWORD=\u003Cpw> powersync deploy   # деплоит service-config + sync-config\n",[215,1019,1020,1027,1040,1045,1050,1055,1078],{"__ignoreMap":254},[258,1021,1022,1024],{"class":260,"line":261},[258,1023,879],{"class":272},[258,1025,1026],{"class":276}," login\n",[258,1028,1029,1031,1034,1037],{"class":260,"line":298},[258,1030,879],{"class":272},[258,1032,1033],{"class":276}," init",[258,1035,1036],{"class":276}," cloud",[258,1038,1039],{"class":800},"                     # создаёт powersync\u002Fservice.yaml + sync-config.yaml\n",[258,1041,1042],{"class":260,"line":313},[258,1043,1044],{"class":800},"# service.yaml: replication -> прямое подключение Supabase (username postgres);\n",[258,1046,1047],{"class":260,"line":331},[258,1048,1049],{"class":800},"#   client_auth -> supabase: true + jwks_uri https:\u002F\u002F\u003Cref>.supabase.co\u002Fauth\u002Fv1\u002F.well-known\u002Fjwks.json\n",[258,1051,1052],{"class":260,"line":345},[258,1053,1054],{"class":800},"# в качестве sync rules используйте app\u002Fpowersync\u002Fconfig\u002Fsync-config.yaml\n",[258,1056,1057,1059,1061,1063,1066,1068,1071,1073,1075],{"class":260,"line":358},[258,1058,879],{"class":272},[258,1060,774],{"class":276},[258,1062,1036],{"class":276},[258,1064,1065],{"class":276}," --instance-id",[258,1067,732],{"class":268},[258,1069,1070],{"class":276},"i",[258,1072,738],{"class":264},[258,1074,741],{"class":268},[258,1076,1077],{"class":800},"  # или --create --org-id \u003Corg> --project-id \u003Cproject>\n",[258,1079,1081,1084,1087,1090,1092,1095,1098],{"class":260,"line":1080},7,[258,1082,1083],{"class":264},"POWERSYNC_DATABASE_PASSWORD",[258,1085,1086],{"class":268},"=\u003C",[258,1088,1089],{"class":276},"pw",[258,1091,741],{"class":268},[258,1093,1094],{"class":272}," powersync",[258,1096,1097],{"class":276}," deploy",[258,1099,1100],{"class":800},"   # деплоит service-config + sync-config\n",[803,1102],{},"Новый ",[185,1105,1106],{},"проект"," в PowerSync Cloud сразу содержит инстансы ",[185,1109,1110],{},"Production",[185,1112,1113],{},"Development"," - деплойте в один из них, а не создавайте третий. Сами проекты создаются в дашборде; CLI управляет инстансами и конфигом, но не проектами.",[656,1116,1117,1120,1121,1124,1125,1128,1129,1132,1133,1136,1137,1140,1141,1144,1145,196,1150,202],{},[185,1118,1119],{},"Self-hosted",": ",[215,1122,1123],{},"app\u002Fpowersync\u002F"," (docker-compose + ",[215,1126,1127],{},"config\u002Fservice.yaml",") - это шаблон, но он предназначен ",[185,1130,1131],{},"только для локальной разработки"," - его учётные данные закоммичены для localhost-стека. Для продакшена: вынесите URI репликации, Postgres URI bucket-хранилища и API-токен в секреты; укажите в ",[215,1134,1135],{},"client_auth.jwks_uri"," адрес ",[215,1138,1139],{},"https:\u002F\u002F\u003Cproject-ref>.supabase.co\u002Fauth\u002Fv1\u002F.well-known\u002Fjwks.json"," и уберите ",[215,1142,1143],{},"allow_local_jwks","; выделите сервису собственный Postgres для bucket-хранилища (свою схему он мигрирует сам); терминируйте TLS перед портом 8080. Полная конфигурация - в ",[190,1146,1149],{"href":1147,"rel":1148},"https:\u002F\u002Fdocs.powersync.com\u002Fself-hosting\u002Fgetting-started",[194],"гайде по self-hosting",[190,1151,1154],{"href":1152,"rel":1153},"https:\u002F\u002Fdocs.powersync.com\u002Fconfiguration\u002Fpowersync-service\u002Fself-hosted-instances",[194],"референсе конфигурации инстанса",[173,1156,1157,1159],{},[215,1158,486],{}," - публичный URL этого сервиса.",[372,1161,1163,1164,1167],{"id":1162},"курсы-валют-fetch-rates","Курсы валют (",[215,1165,1166],{},"fetch-rates",")",[173,1169,1170,1171,1174,1175,1178,1179,1182,1183,1186,1187,1190,1191,1194,1195,1198,1199,1202,1203,1205],{},"Для конвертации валют нужна наполненная таблица ",[215,1172,1173],{},"rates",". Этим занимается ",[185,1176,1177],{},"edge-функция"," Supabase (",[215,1180,1181],{},"app\u002Fsupabase\u002Ffunctions\u002Ffetch-rates\u002F","): она тянет два USD-базовых источника - ",[185,1184,1185],{},"Coinbase"," (без API-ключа, ~весь фиат + крипта, базовый слой) и ",[185,1188,1189],{},"Open Exchange Rates"," (точнее по фиату, накладывается сверху), - сливает их в одну карту и пишет одну строку ",[215,1192,1193],{},"source = 'merged'"," в день. Клиент читает только последнюю строку (",[215,1196,1197],{},"SELECT * FROM rates ORDER BY date DESC LIMIT 1","), поэтому мёрдж происходит при записи. Запись идёт под авто-инжектируемым ",[215,1200,1201],{},"service_role"," - у ",[215,1204,1173],{}," есть глобальная политика чтения, но нет клиентской политики INSERT.",[683,1207,1208,1234,1274],{},[656,1209,1210,1211],{},"Задеплоить функцию:",[249,1212,1214],{"className":251,"code":1213,"filename":701,"language":253,"meta":254,"style":254},"cd app\nsupabase functions deploy fetch-rates\n",[215,1215,1216,1222],{"__ignoreMap":254},[258,1217,1218,1220],{"class":260,"line":261},[258,1219,709],{"class":708},[258,1221,712],{"class":276},[258,1223,1224,1226,1229,1231],{"class":260,"line":298},[258,1225,717],{"class":272},[258,1227,1228],{"class":276}," functions",[258,1230,1097],{"class":276},[258,1232,1233],{"class":276}," fetch-rates\n",[656,1235,1236,1239,1240,1245,1246],{},[185,1237,1238],{},"Ключ OER (опционально)."," Задайте его секретом функции, чтобы добавить точный фиат; без него функция работает только на Coinbase (всё равно ~630 валют, включая крипту). Бесплатный App ID - на ",[190,1241,1244],{"href":1242,"rel":1243},"https:\u002F\u002Fopenexchangerates.org\u002Fsignup",[194],"openexchangerates.org",":",[249,1247,1249],{"className":251,"code":1248,"filename":701,"language":253,"meta":254,"style":254},"supabase secrets set OPEN_EXCHANGE_RATES_KEY=\u003Capp_id>\n",[215,1250,1251],{"__ignoreMap":254},[258,1252,1253,1255,1258,1261,1264,1267,1270,1272],{"class":260,"line":261},[258,1254,717],{"class":272},[258,1256,1257],{"class":276}," secrets",[258,1259,1260],{"class":276}," set",[258,1262,1263],{"class":276}," OPEN_EXCHANGE_RATES_KEY=",[258,1265,1266],{"class":268},"\u003C",[258,1268,1269],{"class":276},"app_i",[258,1271,738],{"class":264},[258,1273,767],{"class":268},[656,1275,1276,1279,1280,1283,1284,1287,1288,1291,1292,1295,1296,1299,1300,1303,1304,1307,1308],{},[185,1277,1278],{},"Ежедневное расписание."," Миграция ",[215,1281,1282],{},"supabase\u002Fmigrations\u002F*_schedule_fetch_rates.sql"," включает ",[215,1285,1286],{},"pg_cron"," + ",[215,1289,1290],{},"pg_net"," и ставит ",[215,1293,1294],{},"fetch-rates-daily"," на ",[215,1297,1298],{},"06:00 UTC","; применяется через ",[215,1301,1302],{},"supabase db push"," (та же оговорка про IPv6\u002Fpooler, что и выше). Крон читает URL функции и anon-ключ из ",[185,1305,1306],{},"секретов Vault"," - они вынесены из миграции ради переносимости, - поэтому задайте их один раз на проект (пока их нет, запуск по расписанию безвредно ничего не делает):",[249,1309,1311],{"className":251,"code":1310,"filename":701,"language":253,"meta":254,"style":254},"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",[215,1312,1313,1330],{"__ignoreMap":254},[258,1314,1315,1317,1319,1321,1323,1325,1328],{"class":260,"line":261},[258,1316,717],{"class":272},[258,1318,794],{"class":276},[258,1320,902],{"class":276},[258,1322,905],{"class":276},[258,1324,283],{"class":268},[258,1326,1327],{"class":276},"select vault.create_secret('https:\u002F\u002F\u003Cref>.supabase.co', 'project_url')",[258,1329,855],{"class":268},[258,1331,1332,1334,1336,1338,1340,1342,1345],{"class":260,"line":298},[258,1333,717],{"class":272},[258,1335,794],{"class":276},[258,1337,902],{"class":276},[258,1339,905],{"class":276},[258,1341,283],{"class":268},[258,1343,1344],{"class":276},"select vault.create_secret('\u003Canon-key>', 'anon_key')",[258,1346,855],{"class":268},[204,1348,1349,1366],{"color":206,"icon":492},[173,1350,1351,1354,1355,1358,1359,1362,1363,1365],{},[185,1352,1353],{},"Засидить один раз."," У ",[215,1356,1357],{},"supabase functions"," нет подкоманды ",[215,1360,1361],{},"invoke"," - запустите функцию обычным POST, чтобы ",[215,1364,1173],{}," наполнилась до первого запуска по расписанию:",[249,1367,1369],{"className":251,"code":1368,"filename":701,"language":253,"meta":254,"style":254},"curl -X POST \"https:\u002F\u002F\u003Cref>.supabase.co\u002Ffunctions\u002Fv1\u002Ffetch-rates\" \\\n  -H \"Authorization: Bearer \u003Canon-key>\"\n",[215,1370,1371,1389],{"__ignoreMap":254},[258,1372,1373,1375,1377,1380,1382,1385,1387],{"class":260,"line":261},[258,1374,301],{"class":272},[258,1376,304],{"class":276},[258,1378,1379],{"class":276}," POST",[258,1381,283],{"class":268},[258,1383,1384],{"class":276},"https:\u002F\u002F\u003Cref>.supabase.co\u002Ffunctions\u002Fv1\u002Ffetch-rates",[258,1386,292],{"class":268},[258,1388,310],{"class":264},[258,1390,1391,1393,1395,1398],{"class":260,"line":298},[258,1392,316],{"class":276},[258,1394,283],{"class":268},[258,1396,1397],{"class":276},"Authorization: Bearer \u003Canon-key>",[258,1399,855],{"class":268},[177,1401,1403],{"id":1402},"вход-через-google-продакшен","Вход через Google (продакшен)",[173,1405,1406,1407,1410,1411,1414],{},"В продакшене провайдер Google настраивается в ",[185,1408,1409],{},"дашборде хостируемого Supabase",", а не в ",[215,1412,1413],{},"config.toml"," (этот файл управляет только локальным стеком).",[683,1416,1417,1438,1445],{},[656,1418,1419,1420,1425,1426,1429,1430],{},"В ",[190,1421,1424],{"href":1422,"rel":1423},"https:\u002F\u002Fconsole.cloud.google.com\u002Fapis\u002Fcredentials",[194],"Google Cloud Console"," создайте (или используйте существующий) ",[185,1427,1428],{},"OAuth 2.0 Client ID"," и добавьте callback вашего проекта как разрешённый redirect URI:",[249,1431,1436],{"className":1432,"code":1434,"language":1435,"meta":254},[1433],"language-text","https:\u002F\u002F\u003Cваш-проект-supabase>.supabase.co\u002Fauth\u002Fv1\u002Fcallback\n","text",[215,1437,1434],{"__ignoreMap":254},[656,1439,1440,1441,1444],{},"В дашборде Supabase: ",[185,1442,1443],{},"Authentication -> Providers -> Google",", включите провайдер и вставьте client id\u002Fsecret.",[656,1446,1419,1447,1450,1451,1454,1455,1458,1459,1462,1463,1466,1467,1470],{},[185,1448,1449],{},"Authentication -> URL Configuration"," задайте ",[185,1452,1453],{},"Site URL"," = origin вашего приложения (например, ",[215,1456,1457],{},"https:\u002F\u002Ffinapp.ilko.me",") и добавьте его (с шаблоном ",[215,1460,1461],{},"\u002F**",") в ",[185,1464,1465],{},"Redirect URLs",", чтобы возврат после OAuth на ",[215,1468,1469],{},"\u002Flogin"," был разрешён.",[204,1472,1473,1484,1541],{"color":206,"icon":492},[173,1474,1475,1478,1479,697],{},[185,1476,1477],{},"Без дашборда."," Шаги 2-3 можно выполнить одним вызовом Management API (нужен ",[190,1480,1483],{"href":1481,"rel":1482},"https:\u002F\u002Fsupabase.com\u002Fdashboard\u002Faccount\u002Ftokens",[194],"access-токен Supabase",[249,1485,1487],{"className":251,"code":1486,"filename":701,"language":253,"meta":254,"style":254},"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",[215,1488,1489,1506,1530],{"__ignoreMap":254},[258,1490,1491,1493,1495,1497,1499,1502,1504],{"class":260,"line":261},[258,1492,301],{"class":272},[258,1494,304],{"class":276},[258,1496,307],{"class":276},[258,1498,283],{"class":268},[258,1500,1501],{"class":276},"https:\u002F\u002Fapi.supabase.com\u002Fv1\u002Fprojects\u002F\u003Cref>\u002Fconfig\u002Fauth",[258,1503,292],{"class":268},[258,1505,310],{"class":264},[258,1507,1508,1510,1512,1514,1517,1519,1522,1524,1526,1528],{"class":260,"line":298},[258,1509,316],{"class":276},[258,1511,283],{"class":268},[258,1513,321],{"class":276},[258,1515,1516],{"class":264},"$SUPABASE_ACCESS_TOKEN",[258,1518,292],{"class":268},[258,1520,1521],{"class":276}," -H",[258,1523,283],{"class":268},[258,1525,338],{"class":276},[258,1527,292],{"class":268},[258,1529,310],{"class":264},[258,1531,1532,1534,1536,1539],{"class":260,"line":313},[258,1533,361],{"class":276},[258,1535,364],{"class":268},[258,1537,1538],{"class":276},"{\"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**\"}",[258,1540,370],{"class":268},[173,1542,1543,1546,1547,1550,1551,1553,1554,1557,1558,1561],{},[185,1544,1545],{},"Не"," используйте для хостируемого проекта ",[215,1548,1549],{},"supabase config push"," - он отправляет весь ",[215,1552,1413],{},", где ",[215,1555,1556],{},"site_url"," = локальный ",[215,1559,1560],{},"http:\u002F\u002Flocalhost:3050",", и перезатрёт URL-конфиг на remote. Management API меняет только переданные поля.",[173,1563,1564,1565,1567,1568,1570],{},"Дополнительные env-переменные для Google не нужны - используются те же ",[215,1566,466],{}," \u002F ",[215,1569,476],{},". Supabase выдаёт собственный JWT независимо от провайдера входа, поэтому PowerSync продолжает проверять его через JWKS без изменений.",[177,1572,1574],{"id":1573},"миграция-в-новый-прод-проект","Миграция в новый прод-проект",[173,1576,1577],{},"Поднятие свежего прод-проекта рядом с существующим окружением (продвижение dev\u002Fлокального набора данных или переход с другого бэкенда) добавляет три шага, которых нет в общей настройке выше: импорт существующих данных, переключение Vercel на новый проект и перемотку deploy-ветки.",[372,1579,1581],{"id":1580},"импорт-существующих-данных-remap-userid","Импорт существующих данных (remap userId)",[173,1583,1584,1585,222,1588,222,1591,222,1594,1596,1597,1599,1600,1603,1604,1606,1607,1610],{},"Четыре таблицы - пер-юзерные: ",[215,1586,1587],{},"categories",[215,1589,1590],{},"wallets",[215,1592,1593],{},"trns",[215,1595,957],{},". ",[215,1598,1173],{}," глобальная - ",[185,1601,1602],{},"не копируем"," (её наполняет ",[215,1605,1166],{},"). Копия идёт Postgres -> Postgres (схема между окружениями идентична), с переписыванием ",[215,1608,1609],{},"userId"," на auth-uid целевого проекта.",[204,1612,1614,1615,1617,1618,1621,1622,1625,1626,1630],{"color":1613,"icon":207},"warning","Целевой ",[215,1616,1609],{}," - это auth-uid пользователя ",[185,1619,1620],{},"в новом проекте",", который появляется только ",[185,1623,1624],{},"после первого входа"," там. Поэтому: завершите ",[190,1627,1629],{"href":1628},"#%D0%B2%D1%85%D0%BE%D0%B4-%D1%87%D0%B5%D1%80%D0%B5%D0%B7-google-%D0%BF%D1%80%D0%BE%D0%B4%D0%B0%D0%BA%D1%88%D0%B5%D0%BD","настройку Google",", сделайте первый вход (подойдёт preview-деплой), считайте uid и тогда импортируйте.",[173,1632,1633,1634,1636,1637,1640,1641,1643,1644,1647],{},"Стримим каждую таблицу с переписанным ",[215,1635,1609],{}," в ",[215,1638,1639],{},"SELECT",". Для ",[215,1642,957],{}," переписываем и ",[215,1645,1646],{},"id"," (он равен uid):",[249,1649,1651],{"className":251,"code":1650,"filename":701,"language":253,"meta":254,"style":254},"SRC_UID=\u003Csource-auth-uid>\nDST_UID=\u003Cnew-prod-auth-uid>\n\n# categories - wallets\u002Ftrns по тому же шаблону (полный список колонок, userId подменён).\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",[215,1652,1653,1665,1677,1683,1688,1707,1738,1767,1787,1797],{"__ignoreMap":254},[258,1654,1655,1658,1660,1663],{"class":260,"line":261},[258,1656,1657],{"class":264},"SRC_UID",[258,1659,1086],{"class":268},[258,1661,1662],{"class":276},"source-auth-uid",[258,1664,767],{"class":268},[258,1666,1667,1670,1672,1675],{"class":260,"line":298},[258,1668,1669],{"class":264},"DST_UID",[258,1671,1086],{"class":268},[258,1673,1674],{"class":276},"new-prod-auth-uid",[258,1676,767],{"class":268},[258,1678,1679],{"class":260,"line":313},[258,1680,1682],{"emptyLinePlaceholder":1681},true,"\n",[258,1684,1685],{"class":260,"line":331},[258,1686,1687],{"class":800},"# categories - wallets\u002Ftrns по тому же шаблону (полный список колонок, userId подменён).\n",[258,1689,1690,1692,1694,1697,1699,1702,1704],{"class":260,"line":345},[258,1691,887],{"class":272},[258,1693,283],{"class":268},[258,1695,1696],{"class":276},"\u003Csource-conn>",[258,1698,292],{"class":268},[258,1700,1701],{"class":276}," -c",[258,1703,283],{"class":268},[258,1705,1706],{"class":276},"\\copy (\n",[258,1708,1709,1712,1715,1718,1721,1723,1725,1728,1730,1733,1735],{"class":260,"line":358},[258,1710,1711],{"class":276},"  select id, '",[258,1713,1714],{"class":264},"$DST_UID",[258,1716,1717],{"class":276},"' as ",[258,1719,1720],{"class":264},"\\\"",[258,1722,1609],{"class":276},[258,1724,1720],{"class":264},[258,1726,1727],{"class":276},", name, color, icon, ",[258,1729,1720],{"class":264},[258,1731,1732],{"class":276},"parentId",[258,1734,1720],{"class":264},[258,1736,1737],{"class":276},",\n",[258,1739,1740,1743,1746,1748,1750,1752,1755,1757,1759,1761,1764],{"class":260,"line":1080},[258,1741,1742],{"class":264},"         \\\"",[258,1744,1745],{"class":276},"showInLastUsed",[258,1747,1720],{"class":264},[258,1749,222],{"class":276},[258,1751,1720],{"class":264},[258,1753,1754],{"class":276},"showInQuickSelector",[258,1756,1720],{"class":264},[258,1758,222],{"class":276},[258,1760,1720],{"class":264},[258,1762,1763],{"class":276},"updatedAt",[258,1765,1766],{"class":264},"\\\"\n",[258,1768,1770,1773,1775,1777,1779,1782,1785],{"class":260,"line":1769},8,[258,1771,1772],{"class":276},"  from public.categories where ",[258,1774,1720],{"class":264},[258,1776,1609],{"class":276},[258,1778,1720],{"class":264},[258,1780,1781],{"class":276}," = '",[258,1783,1784],{"class":264},"$SRC_UID",[258,1786,370],{"class":276},[258,1788,1790,1793,1795],{"class":260,"line":1789},9,[258,1791,1792],{"class":276},") to stdout",[258,1794,292],{"class":268},[258,1796,310],{"class":264},[258,1798,1800,1803,1806,1808,1811,1813,1815,1817,1820],{"class":260,"line":1799},10,[258,1801,1802],{"class":268},"|",[258,1804,1805],{"class":272}," psql",[258,1807,283],{"class":268},[258,1809,1810],{"class":276},"\u003Cprod-pooler-conn>",[258,1812,292],{"class":268},[258,1814,1701],{"class":276},[258,1816,283],{"class":268},[258,1818,1819],{"class":276},"\\copy public.categories from stdin",[258,1821,855],{"class":268},[173,1823,1824,1825,1828,1829,1831,1832,1567,1834,1836,1837,1839],{},"Запись в прод-БД идёт через IPv4 ",[185,1826,1827],{},"session pooler"," (та же IPv6-оговорка, что и ",[215,1830,810],{},") под ",[215,1833,942],{},[215,1835,1201],{},", который обходит RLS. Строки инертны, пока не совпадут с реальным uid - как только ",[215,1838,1609],{}," равен uid вошедшего пользователя, PowerSync стримит их на клиент при следующей синхронизации.",[372,1841,1843],{"id":1842},"переключение-vercel-на-новый-проект","Переключение Vercel на новый проект",[173,1845,1846,1847,1850],{},"Если локальный ",[215,1848,1849],{},".vercel"," привязан к другому проекту (например, dev), перелинкуйте перед установкой prod-env, иначе переменные уйдут не туда:",[249,1852,1854],{"className":251,"code":1853,"filename":701,"language":253,"meta":254,"style":254},"vercel link            # выбрать прод-проект\n",[215,1855,1856],{"__ignoreMap":254},[258,1857,1858,1861,1863],{"class":260,"line":261},[258,1859,1860],{"class":272},"vercel",[258,1862,774],{"class":276},[258,1864,1865],{"class":800},"            # выбрать прод-проект\n",[173,1867,1868,1869,1567,1871,1567,1873,1875,1876,1879],{},"Выставьте ",[215,1870,466],{},[215,1872,476],{},[215,1874,486],{}," на прод-значения и удалите переменные старого бэкенда. ",[185,1877,1878],{},"Не запускайте прод-ребилд",", пока дефолтная ветка отдаёт старый стек - дождитесь перемотки ветки ниже, иначе прод соберёт старый код на новом env.",[372,1881,1883],{"id":1882},"перемотка-ветки-git-connected-прод","Перемотка ветки (git-connected прод)",[173,1885,1886],{},"Когда новый стек живёт на фиче-ветке, а прод авто-деплоится с дефолтной ветки, продвигайте дефолтную ветку, а не делайте force-push:",[683,1888,1889,1947],{},[656,1890,1891,1894,1895],{},[185,1892,1893],{},"Заархивируйте текущую дефолтную ветку",", чтобы старый стек оставался восстановимым:",[249,1896,1898],{"className":251,"code":1897,"filename":701,"language":253,"meta":254,"style":254},"git branch \u003Carchive> \u003Cdefault>      # напр. git branch convex main\ngit push origin \u003Carchive>\n",[215,1899,1900,1930],{"__ignoreMap":254},[258,1901,1902,1904,1907,1909,1912,1915,1917,1919,1922,1925,1927],{"class":260,"line":261},[258,1903,586],{"class":272},[258,1905,1906],{"class":276}," branch",[258,1908,732],{"class":268},[258,1910,1911],{"class":276},"archiv",[258,1913,1914],{"class":264},"e",[258,1916,741],{"class":268},[258,1918,732],{"class":268},[258,1920,1921],{"class":276},"defaul",[258,1923,1924],{"class":264},"t",[258,1926,741],{"class":268},[258,1928,1929],{"class":800},"      # напр. git branch convex main\n",[258,1931,1932,1934,1936,1939,1941,1943,1945],{"class":260,"line":298},[258,1933,586],{"class":272},[258,1935,797],{"class":276},[258,1937,1938],{"class":276}," origin",[258,1940,732],{"class":268},[258,1942,1911],{"class":276},[258,1944,1914],{"class":264},[258,1946,767],{"class":268},[656,1948,1949,1952,1953,1956,1957],{},[185,1950,1951],{},"Перемотайте дефолтную ветку"," на верхушку фичи (без force-push, когда фича - прямой потомок: проверьте ",[215,1954,1955],{},"git rev-list --left-right --count \u003Cdefault>...\u003Cfeature>",", левое число должно быть 0):",[249,1958,1960],{"className":251,"code":1959,"filename":701,"language":253,"meta":254,"style":254},"git checkout \u003Cdefault>\ngit merge --ff-only \u003Cfeature>\ngit push origin \u003Cdefault>\n",[215,1961,1962,1977,1996],{"__ignoreMap":254},[258,1963,1964,1966,1969,1971,1973,1975],{"class":260,"line":261},[258,1965,586],{"class":272},[258,1967,1968],{"class":276}," checkout",[258,1970,732],{"class":268},[258,1972,1921],{"class":276},[258,1974,1924],{"class":264},[258,1976,767],{"class":268},[258,1978,1979,1981,1984,1987,1989,1992,1994],{"class":260,"line":298},[258,1980,586],{"class":272},[258,1982,1983],{"class":276}," merge",[258,1985,1986],{"class":276}," --ff-only",[258,1988,732],{"class":268},[258,1990,1991],{"class":276},"featur",[258,1993,1914],{"class":264},[258,1995,767],{"class":268},[258,1997,1998,2000,2002,2004,2006,2008,2010],{"class":260,"line":313},[258,1999,586],{"class":272},[258,2001,797],{"class":276},[258,2003,1938],{"class":276},[258,2005,732],{"class":268},[258,2007,1921],{"class":276},[258,2009,1924],{"class":264},[258,2011,767],{"class":268},[173,2013,2014,2015,2018,2019,2022],{},"Пуш триггерит прод-билд Vercel на новом стеке. ",[185,2016,2017],{},"Откат"," дешёвый: вернуть дефолтную ветку на архивную (",[215,2020,2021],{},"git push origin \u003Carchive>:\u003Cdefault> --force-with-lease",") или мгновенный rollback к предыдущему деплою в Vercel.",[177,2024,2026],{"id":2025},"premium-finapp-premium","Premium (Finapp Premium)",[173,2028,2029,2030,2035,2036,202],{},"Finapp Premium живёт в отдельном репозитории (",[190,2031,2034],{"href":2032,"rel":2033},"https:\u002F\u002Fgithub.com\u002Filkome\u002Ffinapp-premium",[194],"finapp-premium",") и расширяет базовое приложение как ",[185,2037,2038],{},"Nuxt layer",[372,2040,2042],{"id":2041},"настройки-vercel","Настройки Vercel",[377,2044,2045,2053],{},[380,2046,2047],{},[383,2048,2049,2051],{},[386,2050,388],{},[386,2052,391],{},[393,2054,2055,2063,2069,2078,2086],{},[383,2056,2057,2059],{},[398,2058,400],{},[398,2060,2061],{},[215,2062,202],{},[383,2064,2065,2067],{},[398,2066,410],{},[398,2068,413],{},[383,2070,2071,2073],{},[398,2072,418],{},[398,2074,2075],{},[215,2076,2077],{},"npx nuxt prepare base\u002Fapp && pnpm generate",[383,2079,2080,2082],{},[398,2081,428],{},[398,2083,2084],{},[215,2085,433],{},[383,2087,2088,2090],{},[398,2089,438],{},[398,2091,2092],{},[215,2093,443],{},[173,2095,2096,2097,2100,2101,2104],{},"На Vercel не нужно задавать ",[215,2098,2099],{},"FINAPP_BASE_PATH",". Путь по умолчанию - ",[215,2102,2103],{},".\u002Fbase\u002Fapp",", то есть checked-out базовый субмодуль.",[372,2106,2108],{"id":2107},"как-это-работает","Как это работает",[653,2110,2111,2120,2132,2145,2155],{},[656,2112,2113,2116,2117,2119],{},[215,2114,2115],{},"nuxt.config.ts"," использует переменную ",[215,2118,2099],{}," для определения пути к базовому слою",[656,2121,2122,2123,2125,2126,2128,2129],{},"По умолчанию (CI\u002FVercel): ",[215,2124,2103],{}," - поддиректория ",[215,2127,514],{}," внутри Git-субмодуля ",[215,2130,2131],{},"base",[656,2133,2134,2137,2138,2141,2142],{},[215,2135,2136],{},"base\u002F"," - Git-субмодуль, указывающий на ветку ",[215,2139,2140],{},"main"," репозитория ",[215,2143,2144],{},"ilkome\u002Ffinapp",[656,2146,2147,2150,2151,2154],{},[215,2148,2149],{},"npx nuxt prepare base\u002Fapp"," генерирует ",[215,2152,2153],{},"base\u002Fapp\u002F.nuxt\u002Ftsconfig.json",", который нужен Vite для резолва TypeScript config из submodule layer",[656,2156,2157,2158,1636,2161,2164],{},"Локально: установите ",[215,2159,2160],{},"FINAPP_BASE_PATH=..\u002Fmono\u002Fapp",[215,2162,2163],{},".env"," для работы с локальной монорепой и hot reload",[204,2166,2167],{"color":206,"icon":207},"Vercel инициализирует Git-субмодули при checkout. Репозиторий базового субмодуля должен быть публичным.",[372,2169,2171],{"id":2170},"локальная-разработка","Локальная разработка",[173,2173,2174,2175,1636,2177,2179],{},"Установите ",[215,2176,2099],{},[215,2178,2163],{},", указав на директорию приложения в монорепе:",[249,2181,2183],{"className":251,"code":2182,"filename":2163,"language":253,"meta":254,"style":254},"FINAPP_BASE_PATH=..\u002Fmono\u002Fapp\n",[215,2184,2185],{"__ignoreMap":254},[258,2186,2187,2189,2192],{"class":260,"line":261},[258,2188,2099],{"class":264},[258,2190,2191],{"class":268},"=",[258,2193,2194],{"class":276},"..\u002Fmono\u002Fapp\n",[372,2196,2198],{"id":2197},"git-субмодуль","Git-субмодуль",[173,2200,2201,2202,2141,2204,1245],{},"Субмодуль должен указывать на ветку ",[215,2203,2140],{},[215,2205,2144],{},[249,2207,2209],{"className":251,"code":2208,"filename":701,"language":253,"meta":254,"style":254},"git submodule add -b main https:\u002F\u002Fgithub.com\u002Filkome\u002Ffinapp.git base\n",[215,2210,2211],{"__ignoreMap":254},[258,2212,2213,2215,2218,2221,2224,2227,2230],{"class":260,"line":261},[258,2214,586],{"class":272},[258,2216,2217],{"class":276}," submodule",[258,2219,2220],{"class":276}," add",[258,2222,2223],{"class":276}," -b",[258,2225,2226],{"class":276}," main",[258,2228,2229],{"class":276}," https:\u002F\u002Fgithub.com\u002Filkome\u002Ffinapp.git",[258,2231,2232],{"class":276}," base\n",[173,2234,2235],{},"После свежего клонирования:",[249,2237,2239],{"className":251,"code":2238,"filename":701,"language":253,"meta":254,"style":254},"git submodule update --init\n",[215,2240,2241],{"__ignoreMap":254},[258,2242,2243,2245,2247,2250],{"class":260,"line":261},[258,2244,586],{"class":272},[258,2246,2217],{"class":276},[258,2248,2249],{"class":276}," update",[258,2251,2252],{"class":276}," --init\n",[2254,2255,2256],"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":254,"searchDepth":298,"depth":298,"links":2258},[2259,2263,2264,2270,2271,2276],{"id":179,"depth":298,"text":180,"children":2260},[2261,2262],{"id":374,"depth":313,"text":375},{"id":518,"depth":313,"text":519},{"id":565,"depth":298,"text":566},{"id":640,"depth":298,"text":641,"children":2265},[2266,2267,2268],{"id":680,"depth":313,"text":681},{"id":978,"depth":313,"text":979},{"id":1162,"depth":313,"text":2269},"Курсы валют (fetch-rates)",{"id":1402,"depth":298,"text":1403},{"id":1573,"depth":298,"text":1574,"children":2272},[2273,2274,2275],{"id":1580,"depth":313,"text":1581},{"id":1842,"depth":313,"text":1843},{"id":1882,"depth":313,"text":1883},{"id":2025,"depth":298,"text":2026,"children":2277},[2278,2279,2280,2281],{"id":2041,"depth":313,"text":2042},{"id":2107,"depth":313,"text":2108},{"id":2170,"depth":313,"text":2171},{"id":2197,"depth":313,"text":2198},"Деплой Finapp и документации на Vercel из монорепы с Supabase и PowerSync в качестве бэкенда.","md",null,{},{"icon":90},{"title":87,"description":2288},"Как задеплоить приложение и документацию Finapp на Vercel с Supabase Postgres и PowerSync.","B2lDFs17VSyWBdxOya5wsf-Ue15QnfedQ2UG5UEKuzs",[2291,2293],{"title":82,"path":83,"stem":84,"description":2292,"icon":85,"children":-1},"Исторический обзор миграции бэкенда Финапки - с Firebase на Supabase + PowerSync.",{"title":92,"path":93,"stem":94,"description":2294,"icon":95,"children":-1},"Скрипты для unit- и E2E-тестов.",1782114340077]