Деплой
Finapp - это монорепа на pnpm workspaces. Приложение и документация деплоятся из одного репозитория как два отдельных проекта в Vercel.
Настройка Vercel
Создайте два проекта в Vercel, оба указывают на один GitHub-репозиторий. Nuxt не требует особой настройки Vercel помимо настроек проектов ниже - базовая интеграция описана в Nuxt on Vercel и гайде Vercel по Nuxt.
vercel.json каждого проекта (framework, buildCommand, installCommand, outputDirectory, ignoreCommand, env, rewrites). Root Directory нельзя положить в vercel.json, потому что Vercel должен знать его заранее, чтобы понять где искать файл. Ставится один раз в dashboard (Settings -> General -> Root Directory) или через API:TOKEN=$(jq -r .token "$HOME/Library/Application Support/com.vercel.cli/auth.json")
curl -X PATCH \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://api.vercel.com/v9/projects/<project-name>?teamId=<team-id>" \
-d '{"rootDirectory":"docs"}'
Проект приложения
| Настройка | Значение |
|---|---|
| Root Directory | app |
| Framework Preset | Nuxt.js |
| Build Command | pnpm build |
| Output Directory | .output/public |
| Install Command | pnpm install |
Добавьте переменные окружения, указывающие на ваш хостируемый проект Supabase и сервис PowerSync:
| Переменная | Значение |
|---|---|
VITE_SUPABASE_URL | URL вашего проекта Supabase |
VITE_SUPABASE_ANON_KEY | Публичный (anon) ключ Supabase |
VITE_POWERSYNC_URL | URL вашего сервиса PowerSync |
finapp-dev на feature-ветке) можно обойтись без git-привязки и задеплоить рабочее дерево прямо из корня репозитория: создайте/слинкуйте проект (vercel link) с Root Directory = app, затем vercel deploy --prod. Вся монорепа грузится из корня, а Vercel собирает app/ по настройке Root Directory.Проект документации
| Настройка | Значение |
|---|---|
| Root Directory | docs |
| Framework Preset | Nuxt.js |
| Build Command | pnpm build |
| Output Directory | .output/public |
Ignored Build Step
Чтобы избежать лишних билдов, настройте Ignored Build Step в каждом проекте Vercel (Settings -> Git -> Ignored Build Step):
git diff --quiet HEAD^ HEAD -- . ':!docs'
git diff --quiet HEAD^ HEAD -- docs
Так приложение не будет передеплоиваться при изменениях только в документации, и наоборот.
Бэкенд: Supabase + PowerSync
Nuxt SPA хостится статически (выше описан Vercel, но подойдёт любой статический хостинг, отдающий .output/public после pnpm generate). Бэкенд-сервисы отдельные:
- Supabase - хостируемый проект Supabase (облачный или self-hosted) предоставляет Postgres, Auth и путь записи через PostgREST/supabase-js.
- PowerSync - сервис PowerSync (облачный или self-hosted) обеспечивает offline-first синхронизацию, реплицируя данные из Supabase Postgres через логическую репликацию.
Установите три VITE_* переменные в Vercel, указывающие на продакшен-проект Supabase и экземпляр PowerSync. Описание переменных - в разделе Установка.
Supabase (продакшен-проект)
- Создайте проект Supabase (через дашборд или
supabase projects create), привяжите репозиторий и примените схему (доки Supabase по миграциям):Terminalcd app supabase projects create finapp --org-id <org-id> --region <region> --db-password <pw> supabase link --project-ref <project-ref> supabase db push # applies supabase/migrations/ (tables, RLS, signup trigger)
Нет IPv6?db pushупадёт. Прямое подключениеdb.<ref>.supabase.co:5432- только IPv6, поэтому в сети без IPv6db pushпадает сsocket is not connected. Пушьте через IPv4 session-пулер - строку возьмите во вкладке Connect дашборда (режим Session):Terminalsupabase db push --db-url "postgresql://postgres.<ref>:<pw>@aws-1-<region>.pooler.supabase.com:5432/postgres"
На этой странице используются два разных подключения: пулер (IPv4) - дляdb pushи запросов с вашей машины, и прямое подключение - для логической репликации PowerSync ниже (у инфраструктуры PowerSync IPv6 есть). - Убедитесь, что публикация
powersyncсуществует для синхронизируемых таблиц -supabase db queryвыполняет setup-SQL на привязанном проекте,psqlне нужен:Terminalsupabase db query --linked -f supabase/powersync_setup.sql # через пулер мультистейтмент-файл может упереться в prepared-statement limit; # тогда выполните DROP и CREATE публикации отдельными вызовами `supabase db query "..."`
Роль: cloud vs self-hosted.powersync_role(REPLICATION + BYPASSRLS) из скрипта нужен для self-hosted PowerSync. В Supabase Cloud у ролиpostgresуже есть REPLICATION + BYPASSRLS (а не-суперюзерpostgresвсё равно не может создать REPLICATION-роль), поэтому PowerSync Cloud подключается какpostgres- там важна только публикацияpowersync. - RLS-политики и триггер
user_settingsпри регистрации приходят вместе с миграциями - вручную ничего настраивать не нужно.
Поднимаете self-hosted Supabase вместо облака? Шаги, специфичные для Finapp, идентичны (применить миграции, выполнить powersync_setup.sql) - сам стек разворачивается по гайду self-hosting с Docker, после чего подставьте вместо *.supabase.co URL и JWKS-эндпоинт адреса своего инстанса.
PowerSync (продакшен-сервис)
Два варианта; оба реплицируют из Supabase Postgres и проверяют JWT Supabase через JWKS:
- PowerSync Cloud (управляемый): укажите в качестве исходной базы прямую строку подключения Supabase (не pooler - логической репликации нужно прямое подключение), включите Supabase auth и используйте правила синхронизации из
app/powersync/config/sync-config.yaml. Сделать это можно в дашборде (гайд Supabase + PowerSync) или официальным PowerSync CLI (npm i -g powersync), который держит конфиг инстанса как код:Terminalpowersync login powersync init cloud # создаёт powersync/service.yaml + sync-config.yaml # service.yaml: replication -> прямое подключение Supabase (username postgres); # client_auth -> supabase: true + jwks_uri https://<ref>.supabase.co/auth/v1/.well-known/jwks.json # в качестве sync rules используйте app/powersync/config/sync-config.yaml powersync link cloud --instance-id <id> # или --create --org-id <org> --project-id <project> POWERSYNC_DATABASE_PASSWORD=<pw> powersync deploy # деплоит service-config + sync-config
Новый проект в PowerSync Cloud сразу содержит инстансы Production и Development - деплойте в один из них, а не создавайте третий. Сами проекты создаются в дашборде; CLI управляет инстансами и конфигом, но не проектами. - Self-hosted:
app/powersync/(docker-compose +config/service.yaml) - это шаблон, но он предназначен только для локальной разработки - его учётные данные закоммичены для localhost-стека. Для продакшена: вынесите URI репликации, Postgres URI bucket-хранилища и API-токен в секреты; укажите вclient_auth.jwks_uriадресhttps://<project-ref>.supabase.co/auth/v1/.well-known/jwks.jsonи уберитеallow_local_jwks; выделите сервису собственный Postgres для bucket-хранилища (свою схему он мигрирует сам); терминируйте TLS перед портом 8080. Полная конфигурация - в гайде по self-hosting и референсе конфигурации инстанса.
VITE_POWERSYNC_URL - публичный URL этого сервиса.
Курсы валют (fetch-rates)
Для конвертации валют нужна наполненная таблица rates. Этим занимается edge-функция Supabase (app/supabase/functions/fetch-rates/): она тянет два USD-базовых источника - Coinbase (без API-ключа, ~весь фиат + крипта, базовый слой) и Open Exchange Rates (точнее по фиату, накладывается сверху), - сливает их в одну карту и пишет одну строку source = 'merged' в день. Клиент читает только последнюю строку (SELECT * FROM rates ORDER BY date DESC LIMIT 1), поэтому мёрдж происходит при записи. Запись идёт под авто-инжектируемым service_role - у rates есть глобальная политика чтения, но нет клиентской политики INSERT.
- Задеплоить функцию:Terminal
cd app supabase functions deploy fetch-rates - Ключ OER (опционально). Задайте его секретом функции, чтобы добавить точный фиат; без него функция работает только на Coinbase (всё равно ~630 валют, включая крипту). Бесплатный App ID - на openexchangerates.org:Terminal
supabase secrets set OPEN_EXCHANGE_RATES_KEY=<app_id> - Ежедневное расписание. Миграция
supabase/migrations/*_schedule_fetch_rates.sqlвключаетpg_cron+pg_netи ставитfetch-rates-dailyна06:00 UTC; применяется черезsupabase db push(та же оговорка про IPv6/pooler, что и выше). Крон читает URL функции и anon-ключ из секретов Vault - они вынесены из миграции ради переносимости, - поэтому задайте их один раз на проект (пока их нет, запуск по расписанию безвредно ничего не делает):Terminalsupabase db query --linked "select vault.create_secret('https://<ref>.supabase.co', 'project_url')" supabase db query --linked "select vault.create_secret('<anon-key>', 'anon_key')"
supabase functions нет подкоманды invoke - запустите функцию обычным POST, чтобы rates наполнилась до первого запуска по расписанию:curl -X POST "https://<ref>.supabase.co/functions/v1/fetch-rates" \
-H "Authorization: Bearer <anon-key>"
Вход через Google (продакшен)
В продакшене провайдер Google настраивается в дашборде хостируемого Supabase, а не в config.toml (этот файл управляет только локальным стеком).
- В Google Cloud Console создайте (или используйте существующий) OAuth 2.0 Client ID и добавьте callback вашего проекта как разрешённый redirect URI:
https://<ваш-проект-supabase>.supabase.co/auth/v1/callback - В дашборде Supabase: Authentication -> Providers -> Google, включите провайдер и вставьте client id/secret.
- В Authentication -> URL Configuration задайте Site URL = origin вашего приложения (например,
https://finapp.ilko.me) и добавьте его (с шаблоном/**) в Redirect URLs, чтобы возврат после OAuth на/loginбыл разрешён.
curl -X PATCH "https://api.supabase.com/v1/projects/<ref>/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"external_google_enabled":true,"external_google_client_id":"<id>","external_google_secret":"<secret>","site_url":"https://<your-app>","uri_allow_list":"https://<your-app>/**"}'
supabase config push - он отправляет весь config.toml, где site_url = локальный http://localhost:3050, и перезатрёт URL-конфиг на remote. Management API меняет только переданные поля.Дополнительные env-переменные для Google не нужны - используются те же VITE_SUPABASE_URL / VITE_SUPABASE_ANON_KEY. Supabase выдаёт собственный JWT независимо от провайдера входа, поэтому PowerSync продолжает проверять его через JWKS без изменений.
Миграция в новый прод-проект
Поднятие свежего прод-проекта рядом с существующим окружением (продвижение dev/локального набора данных или переход с другого бэкенда) добавляет три шага, которых нет в общей настройке выше: импорт существующих данных, переключение Vercel на новый проект и перемотку deploy-ветки.
Импорт существующих данных (remap userId)
Четыре таблицы - пер-юзерные: categories, wallets, trns, user_settings. rates глобальная - не копируем (её наполняет fetch-rates). Копия идёт Postgres -> Postgres (схема между окружениями идентична), с переписыванием userId на auth-uid целевого проекта.
userId - это auth-uid пользователя в новом проекте, который появляется только после первого входа там. Поэтому: завершите , сделайте первый вход (подойдёт preview-деплой), считайте uid и тогда импортируйте.Стримим каждую таблицу с переписанным userId в SELECT. Для user_settings переписываем и id (он равен uid):
SRC_UID=<source-auth-uid>
DST_UID=<new-prod-auth-uid>
# categories - wallets/trns по тому же шаблону (полный список колонок, userId подменён).
psql "<source-conn>" -c "\copy (
select id, '$DST_UID' as \"userId\", name, color, icon, \"parentId\",
\"showInLastUsed\", \"showInQuickSelector\", \"updatedAt\"
from public.categories where \"userId\" = '$SRC_UID'
) to stdout" \
| psql "<prod-pooler-conn>" -c "\copy public.categories from stdin"
Запись в прод-БД идёт через IPv4 session pooler (та же IPv6-оговорка, что и db push) под postgres / service_role, который обходит RLS. Строки инертны, пока не совпадут с реальным uid - как только userId равен uid вошедшего пользователя, PowerSync стримит их на клиент при следующей синхронизации.
Переключение Vercel на новый проект
Если локальный .vercel привязан к другому проекту (например, dev), перелинкуйте перед установкой prod-env, иначе переменные уйдут не туда:
vercel link # выбрать прод-проект
Выставьте VITE_SUPABASE_URL / VITE_SUPABASE_ANON_KEY / VITE_POWERSYNC_URL на прод-значения и удалите переменные старого бэкенда. Не запускайте прод-ребилд, пока дефолтная ветка отдаёт старый стек - дождитесь перемотки ветки ниже, иначе прод соберёт старый код на новом env.
Перемотка ветки (git-connected прод)
Когда новый стек живёт на фиче-ветке, а прод авто-деплоится с дефолтной ветки, продвигайте дефолтную ветку, а не делайте force-push:
- Заархивируйте текущую дефолтную ветку, чтобы старый стек оставался восстановимым:Terminal
git branch <archive> <default> # напр. git branch convex main git push origin <archive> - Перемотайте дефолтную ветку на верхушку фичи (без force-push, когда фича - прямой потомок: проверьте
git rev-list --left-right --count <default>...<feature>, левое число должно быть 0):Terminalgit checkout <default> git merge --ff-only <feature> git push origin <default>
Пуш триггерит прод-билд Vercel на новом стеке. Откат дешёвый: вернуть дефолтную ветку на архивную (git push origin <archive>:<default> --force-with-lease) или мгновенный rollback к предыдущему деплою в Vercel.
Premium (Finapp Premium)
Finapp Premium живёт в отдельном репозитории (finapp-premium) и расширяет базовое приложение как Nuxt layer.
Настройки Vercel
| Настройка | Значение |
|---|---|
| Root Directory | . |
| Framework Preset | Nuxt.js |
| Build Command | npx nuxt prepare base/app && pnpm generate |
| Output Directory | .output/public |
| Install Command | pnpm install |
На Vercel не нужно задавать FINAPP_BASE_PATH. Путь по умолчанию - ./base/app, то есть checked-out базовый субмодуль.
Как это работает
nuxt.config.tsиспользует переменнуюFINAPP_BASE_PATHдля определения пути к базовому слою- По умолчанию (CI/Vercel):
./base/app- поддиректорияapp/внутри Git-субмодуляbase base/- Git-субмодуль, указывающий на веткуmainрепозиторияilkome/finappnpx nuxt prepare base/appгенерируетbase/app/.nuxt/tsconfig.json, который нужен Vite для резолва TypeScript config из submodule layer- Локально: установите
FINAPP_BASE_PATH=../mono/appв.envдля работы с локальной монорепой и hot reload
Локальная разработка
Установите FINAPP_BASE_PATH в .env, указав на директорию приложения в монорепе:
FINAPP_BASE_PATH=../mono/app
Git-субмодуль
Субмодуль должен указывать на ветку main репозитория ilkome/finapp:
git submodule add -b main https://github.com/ilkome/finapp.git base
После свежего клонирования:
git submodule update --init