Open Finapp
Разработка

Деплой

Деплой Finapp и документации на Vercel из монорепы с Supabase и PowerSync в качестве бэкенда.

Finapp - это монорепа на pnpm workspaces. Приложение и документация деплоятся из одного репозитория как два отдельных проекта в Vercel.

Настройка Vercel

Создайте два проекта в Vercel, оба указывают на один GitHub-репозиторий. Nuxt не требует особой настройки Vercel помимо настроек проектов ниже - базовая интеграция описана в Nuxt on Vercel и гайде Vercel по Nuxt.

Где что настраивать: файл vs проект. Большинство build-настроек живут в 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 Directoryapp
Framework PresetNuxt.js
Build Commandpnpm build
Output Directory.output/public
Install Commandpnpm install

Добавьте переменные окружения, указывающие на ваш хостируемый проект Supabase и сервис PowerSync:

ПеременнаяЗначение
VITE_SUPABASE_URLURL вашего проекта Supabase
VITE_SUPABASE_ANON_KEYПубличный (anon) ключ Supabase
VITE_POWERSYNC_URLURL вашего сервиса PowerSync
Git-connected vs прямой CLI-деплой. Схема из двух проектов выше - git-connected (пуши деплоятся автоматически). Для отдельного или временного окружения (например, проект finapp-dev на feature-ветке) можно обойтись без git-привязки и задеплоить рабочее дерево прямо из корня репозитория: создайте/слинкуйте проект (vercel link) с Root Directory = app, затем vercel deploy --prod. Вся монорепа грузится из корня, а Vercel собирает app/ по настройке Root Directory.

Проект документации

НастройкаЗначение
Root Directorydocs
Framework PresetNuxt.js
Build Commandpnpm build
Output Directory.output/public

Ignored Build Step

Чтобы избежать лишних билдов, настройте Ignored Build Step в каждом проекте Vercel (Settings -> Git -> Ignored Build Step):

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 (продакшен-проект)

  1. Создайте проект Supabase (через дашборд или supabase projects create), привяжите репозиторий и примените схему (доки Supabase по миграциям):
    Terminal
    cd 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, поэтому в сети без IPv6 db push падает с socket is not connected. Пушьте через IPv4 session-пулер - строку возьмите во вкладке Connect дашборда (режим Session):
    Terminal
    supabase db push --db-url "postgresql://postgres.<ref>:<pw>@aws-1-<region>.pooler.supabase.com:5432/postgres"
    

    На этой странице используются два разных подключения: пулер (IPv4) - для db push и запросов с вашей машины, и прямое подключение - для логической репликации PowerSync ниже (у инфраструктуры PowerSync IPv6 есть).
  2. Убедитесь, что публикация powersync существует для синхронизируемых таблиц - supabase db query выполняет setup-SQL на привязанном проекте, psql не нужен:
    Terminal
    supabase 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.
  3. 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), который держит конфиг инстанса как код:
    Terminal
    powersync 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.

  1. Задеплоить функцию:
    Terminal
    cd app
    supabase functions deploy fetch-rates
    
  2. Ключ OER (опционально). Задайте его секретом функции, чтобы добавить точный фиат; без него функция работает только на Coinbase (всё равно ~630 валют, включая крипту). Бесплатный App ID - на openexchangerates.org:
    Terminal
    supabase secrets set OPEN_EXCHANGE_RATES_KEY=<app_id>
    
  3. Ежедневное расписание. Миграция supabase/migrations/*_schedule_fetch_rates.sql включает pg_cron + pg_net и ставит fetch-rates-daily на 06:00 UTC; применяется через supabase db push (та же оговорка про IPv6/pooler, что и выше). Крон читает URL функции и anon-ключ из секретов Vault - они вынесены из миграции ради переносимости, - поэтому задайте их один раз на проект (пока их нет, запуск по расписанию безвредно ничего не делает):
    Terminal
    supabase 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 наполнилась до первого запуска по расписанию:
Terminal
curl -X POST "https://<ref>.supabase.co/functions/v1/fetch-rates" \
  -H "Authorization: Bearer <anon-key>"

Вход через Google (продакшен)

В продакшене провайдер Google настраивается в дашборде хостируемого Supabase, а не в config.toml (этот файл управляет только локальным стеком).

  1. В Google Cloud Console создайте (или используйте существующий) OAuth 2.0 Client ID и добавьте callback вашего проекта как разрешённый redirect URI:
    https://<ваш-проект-supabase>.supabase.co/auth/v1/callback
    
  2. В дашборде Supabase: Authentication -> Providers -> Google, включите провайдер и вставьте client id/secret.
  3. В Authentication -> URL Configuration задайте Site URL = origin вашего приложения (например, https://finapp.ilko.me) и добавьте его (с шаблоном /**) в Redirect URLs, чтобы возврат после OAuth на /login был разрешён.
Без дашборда. Шаги 2-3 можно выполнить одним вызовом Management API (нужен access-токен Supabase):
Terminal
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):

Terminal
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, иначе переменные уйдут не туда:

Terminal
vercel link            # выбрать прод-проект

Выставьте VITE_SUPABASE_URL / VITE_SUPABASE_ANON_KEY / VITE_POWERSYNC_URL на прод-значения и удалите переменные старого бэкенда. Не запускайте прод-ребилд, пока дефолтная ветка отдаёт старый стек - дождитесь перемотки ветки ниже, иначе прод соберёт старый код на новом env.

Перемотка ветки (git-connected прод)

Когда новый стек живёт на фиче-ветке, а прод авто-деплоится с дефолтной ветки, продвигайте дефолтную ветку, а не делайте force-push:

  1. Заархивируйте текущую дефолтную ветку, чтобы старый стек оставался восстановимым:
    Terminal
    git branch <archive> <default>      # напр. git branch convex main
    git push origin <archive>
    
  2. Перемотайте дефолтную ветку на верхушку фичи (без force-push, когда фича - прямой потомок: проверьте git rev-list --left-right --count <default>...<feature>, левое число должно быть 0):
    Terminal
    git 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 PresetNuxt.js
Build Commandnpx nuxt prepare base/app && pnpm generate
Output Directory.output/public
Install Commandpnpm 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/finapp
  • npx nuxt prepare base/app генерирует base/app/.nuxt/tsconfig.json, который нужен Vite для резолва TypeScript config из submodule layer
  • Локально: установите FINAPP_BASE_PATH=../mono/app в .env для работы с локальной монорепой и hot reload
Vercel инициализирует Git-субмодули при checkout. Репозиторий базового субмодуля должен быть публичным.

Локальная разработка

Установите FINAPP_BASE_PATH в .env, указав на директорию приложения в монорепе:

.env
FINAPP_BASE_PATH=../mono/app

Git-субмодуль

Субмодуль должен указывать на ветку main репозитория ilkome/finapp:

Terminal
git submodule add -b main https://github.com/ilkome/finapp.git base

После свежего клонирования:

Terminal
git submodule update --init