---
name: ru-deploy
description: "Пошаговый перенос проекта с Lovable на российский хостинг Cloud.ru с минимальными правками. Поднимает self-hosted Supabase (база, авторизация, хранилище, edge-функции) на виртуалке Cloud.ru, подключает домен и бесплатный HTTPS через Caddy, настраивает почту (сброс пароля) и платежи YooKassa. Для пользователей-новичков: агент делает тяжёлую работу сам, пользователь копирует минимум команд. Триггеры: 'перенести проект на cloud.ru', 'задеплоить lovable на российский сервер', 'переехать с lovable на свой хостинг', 'ru-deploy', 'мой проект с lovable не работает в России', 'свой сервер для lovable проекта'. AI-функцию (распознавание/генерацию) пользователь донастраивает сам — скилл только оставляет понятный шов."
---

# ru-deploy — переезд с Lovable на Cloud.ru

## Кому и зачем

Пользователь сделал проект в **Lovable** (Vite + React + Supabase). Lovable хостит фронт и держит Supabase в облаке за рубежом, плюс AI-функции идут через шлюз Lovable. Из России это работает нестабильно, а персональные данные граждан РФ по 152-ФЗ должны лежать в РФ.

Этот скилл проводит пользователя **по шагам** от «проект в Lovable» до «рабочий сайт на своём домене на Cloud.ru»: своя база, авторизация, хранилище, почта и платежи — всё в России. **Правок в коде проекта минимум** — основная работа на сервере.

## Главный принцип работы

**Ты (агент) делаешь сложное, пользователь — простое.** Не сваливай на новичка ssh-команды и правку YAML. На каждом шаге:
1. Сам осмотри/правь файлы, сам собери конфиги, сам по ssh выполни команды, когда у тебя есть доступ.
2. Пользователю давай только то, что может сделать лишь он: зарегистрироваться на Cloud.ru, нажать кнопки в личном кабинете, скопировать секретный ключ, привязать домен.
3. Объясняй **простым языком, без жаргона**. Никогда не показывай и не коммить секреты. В примерах — только плейсхолдеры.

## 🚦 Протокол стадий — ОБЯЗАТЕЛЬНО соблюдай

Это **самое важное правило скилла**. Пользователь — новичок, и для него страшно, когда агент «убежал галопом» и всё сделал сам без объяснений. Поэтому **никогда не выполняй весь переезд за один заход.** Работа разбита на стадии (см. ниже). Для **каждой** стадии соблюдай один и тот же ритм из 4 тактов:

1. **Объясни заранее** — простыми словами скажи, что это за стадия, что конкретно ты сейчас будешь делать и зачем, что для этого понадобится от пользователя (если понадобится). 2-4 предложения, без жаргона.
2. **Получи «да»** — спроси разрешение начать эту стадию и **дождись согласия**. Не начинай, пока пользователь не ответил.
3. **Сделай** — выполни стадию. Если что-то нужно от пользователя (кнопки в Cloud.ru, ключ) — попроси и дождись «готово».
4. **Подтверди результат** — покажи простыми словами, что стадия прошла успешно и **чем именно ты это проверил** (например: «зашёл на сервер, ключи сохранены в `.ru-deploy/`, доступ работает»). Только после этого назови, **какая стадия следующая и что в ней будет**, и снова спроси разрешение (такт 1-2 следующей стадии).

**Правила, которые нельзя нарушать:**
- Не переходи к следующей стадии без явного «да» от пользователя. Даже если технически можешь.
- Не объединяй стадии в один монолог «сделал-всё-сразу». Одна стадия — одна остановка.
- Если стадия не нужна этому проекту (нет платежей/почты/AI) — скажи об этом и пропусти её с согласия пользователя.
- Если на стадии что-то пошло не так — остановись, объясни простыми словами, что и почему, предложи решение. Не беги дальше на сломанном.

### Стадии переезда (крупные ворота)

| Стадия | Что входит | Ворота подтверждения |
|---|---|---|
| **A. Осмотр** | Шаг 0 — понять, что за проект и что ему нужно | «вот что нашёл, вот какие стадии понадобятся — начинаем?» |
| **B. Сервер и доступ** | Шаг 1 — создать ВМ, включить вход, открыть порты, домен | «зашёл на сервер по логину/паролю, доступы сохранены, порты открыты — всё работает. Дальше поднимаю backend, ок?» |
| **C. Backend** | Шаги 2-4 — Supabase, схема базы, edge-функции | «backend поднят, все сервисы здоровы, API отвечает по HTTPS. Дальше переключаю и заливаю сам сайт, ок?» |
| **D. Фронт** | Шаг 5 — переключить фронт на новый адрес, собрать, залить | «сайт открывается на новом домене, вход работает. Дальше — почта/платежи (если нужны), ок?» |
| **E. Почта** | Шаг 7 — SMTP для сброса пароля (если нужно) | «письма сброса пароля приходят. Дальше платежи, ок?» |
| **F. Платежи** | Шаг 8 — YooKassa (если нужно) | «тестовый платёж проходит. Осталась AI-часть (её настраиваешь сам).» |
| **G. AI и финал** | Шаг 9 + итоговая проверка | финальный чек-лист вместе с пользователем |

Внутри стадии могут быть под-шаги — их веди так же спокойно, но крупная остановка-«ворота» обязательна между стадиями. Нумерация шагов (0-9) ниже — это детали реализации внутри стадий.

## Где работает

- **Claude Code** — выполняй шаги через `Bash` (ssh/scp/rsync) и инструменты правки файлов.
- **Codex** — то же самое через свои аналоги `Bash`/правок. Скрипты из `assets/` переиспользуются как есть.

Логика, тексты и скрипты не зависят от конкретного агента.

## Резолв пути скилла

Дальше документы ссылаются на `SKILL_DIR` — это абсолютный путь к папке, где лежит этот `SKILL.md` (обычно `~/.claude/skills/ru-deploy`). Определи его один раз в начале и обращайся к файлам как `"$SKILL_DIR/assets/<имя>"` и `"$SKILL_DIR/references/<имя>.md"`. Никогда не вставляй в команды буквальную строку `<SKILL_DIR>`.

---

## Доступы к серверу (спроси в самом начале)

Чтобы что-то делать на сервере, тебе нужен доступ по SSH. **Логин и пароль придумывает сам пользователь, когда создаёт виртуалку в Cloud.ru** — это НЕ всегда `root`. Поэтому в начале работы спроси у пользователя три вещи и дальше используй именно их (нигде не хардкодь `root`):

- **IP-адрес** виртуалки (например `176.123.160.180`);
- **логин** для входа (тот, что он задал при создании ВМ, часто `admin`);
- **пароль**.

### Сохрани доступы безопасно (и не дай им утечь в git)

Запиши их в локальный файл `.ru-deploy/credentials.env` **в корне проекта пользователя**, чтобы не потерять между шагами и не спрашивать заново:

```bash
mkdir -p .ru-deploy
cat > .ru-deploy/credentials.env <<EOF
RU_HOST=<IP>
RU_USER=<логин>
RU_PASS=<пароль>
RU_DOMAIN=<домен, когда определится>
EOF
chmod 600 .ru-deploy/credentials.env
# КРИТИЧНО: добавь в .gitignore проекта, иначе пароль уедет в репозиторий
grep -qxF '.ru-deploy/' .gitignore 2>/dev/null || echo '.ru-deploy/' >> .gitignore
```

Перед каждой серверной командой подгружай их: `set -a; . .ru-deploy/credentials.env; set +a`. **Никогда не печатай пароль в чат и не коммить файл доступов.** Если показываешь команду пользователю — заменяй пароль на `<пароль>`.

### Вход по паролю требует утилиту `sshpass`

Обычный `ssh`/`scp` не умеют принимать пароль из переменной — нужна обёртка `sshpass`. Проверь, стоит ли она (`command -v sshpass`), и если нет — попроси пользователя поставить (объясни простыми словами: «это маленькая утилита, чтобы я мог заходить на твой сервер по паролю, который ты дал»):

- **macOS:** `brew install hudochenkov/sshpass/sshpass`
- **Windows:** проще всего через WSL (Ubuntu) и там `sudo apt install sshpass`; либо `scoop install sshpass`.
- **Linux:** `sudo apt install sshpass` (или аналог пакетного менеджера).

Скрипты `deploy.sh` и серверные ssh-команды сами используют `sshpass`, если в окружении задан `RU_PASS`. Если пользователь предпочёл вход по ssh-ключу (см. шаг 1, вариант для продвинутых) — оставь `RU_PASS` пустым, тогда `sshpass` не нужен.

### Хелпер для ssh-команд

Дальше в шагах команды пишутся как `ssh root@<IP> "..."` для краткости. На практике подставляй логин пользователя и, если вход по паролю, оборачивай в `sshpass`. Удобно завести функцию:

```bash
set -a; . .ru-deploy/credentials.env; set +a
# Пароль передаём через env (sshpass -e), НЕ через `-p <пароль>` — иначе он виден
# в `ps` другим пользователям сервера. SSHPASS читается sshpass автоматически.
export SSHPASS="$RU_PASS"
# ControlMaster: все RSH/SCP-вызовы переиспользуют ОДНО ssh-соединение (одна
# аутентификация на всю сессию). Это и быстрее, и убирает корневую причину
# самобана fail2ban — нет шквала отдельных авторизаций при частых командах.
mkdir -p .ru-deploy/cm
SSH_OPTS="-o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=.ru-deploy/known_hosts -o ControlMaster=auto -o ControlPath=.ru-deploy/cm/%r@%h:%p -o ControlPersist=300"
RSH() { sshpass -e ssh $SSH_OPTS "$RU_USER@$RU_HOST" "$@"; }
SCP_() { sshpass -e scp $SSH_OPTS "$@"; }   # для копирования файлов
# дальше: RSH "whoami"   вместо   ssh root@<IP> "whoami"
```

Системные действия на сервере (запись в `/opt`, `/etc`, `/var/www`, запуск docker) делай через `sudo` — дефолтный пользователь Cloud.ru обычно имеет `sudo` без пароля. Скрипт `server-setup.sh` уже сам подставляет `sudo`, где надо.

### ⚠️ Первый вход почти наверняка не пройдёт — это нормально, среагируй так

Образ Cloud.ru по умолчанию **запрещает вход по паролю** (`PasswordAuthentication no`). Поэтому твоя самая первая попытка зайдёт в ошибку:

```
Permission denied (publickey)
```

или (если ты явно просил пароль)

```
Permission denied (publickey,password)
```

**Не воспринимай это как «неверный пароль» и не проси пользователя перепроверять пароль.** Причина одна: на сервере ещё не включён парольный вход. Пользователь не может этого знать — он задал логин/пароль при создании ВМ и думает, что их достаточно. Твоя задача — спокойно объяснить и дать одну готовую команду.

Сделай ровно так:

1. Скажи простыми словами: «Сервер пока не пускает по паролю — это настройка по умолчанию. Нужно один раз включить вход, я дам команду, а ты вставишь её в специальное окно».
2. Объясни, **куда** вставить (там работает логин/пароль, в отличие от обычного SSH):
   > Зайди на **cloud.ru** → раздел с твоей виртуальной машиной → кнопка **«Консоль»** (откроется чёрное окно-терминал прямо в браузере). Если попросит — введи логин и пароль, что ты задавал при создании машины.
3. Дай **одну** команду целиком (она включает парольный вход и ставит fail2ban с **мягким порогом** — чтобы агент с его частыми SSH-подключениями НЕ попал под бан, но боты-переборщики отсекались):
   ```bash
   sudo bash -c 'sed -i "s/^#*PasswordAuthentication.*/PasswordAuthentication yes/" /etc/ssh/sshd_config; sed -i "s/^#*PasswordAuthentication.*/PasswordAuthentication yes/" /etc/ssh/sshd_config.d/* 2>/dev/null; systemctl restart ssh || systemctl restart sshd; apt-get update -y >/dev/null 2>&1; DEBIAN_FRONTEND=noninteractive apt-get install -y fail2ban >/dev/null 2>&1; printf "[sshd]\nenabled = true\nmaxretry = 20\nfindtime = 600\nbantime = 600\n" > /etc/fail2ban/jail.local; systemctl enable fail2ban; systemctl restart fail2ban; echo OK-PASSWORD-SSH-READY'
   ```
   Порог умеренный: бан после **20 неудачных** входов за 10 минут. Агент это не наберёт (он ходит через одно ssh-соединение — ControlMaster, см. «Хелпер для ssh-команд»), а бот-переборщик отсекается. Если агент всё же упёрся в бан на старом сервере с жёстким дефолтом (`maxretry=5`) — см. `references/troubleshooting.md` → «fail2ban забанил агента».
4. Попроси дождаться в окне строки `OK-PASSWORD-SSH-READY` и **сказать «готово»**. Предупреди: правило применяется не мгновенно — после «готово» сделай 1-2 повторные попытки входа с паузой ~30-60 сек, не сдавайся после первого отказа.
5. Снова попробуй `RSH "echo OK"`. Теперь должно пустить.

Подробности и альтернатива (вход по ssh-ключу для продвинутых) — в `references/cloudru-setup.md` → «Включить вход по паролю». Если вход не проходит даже после включения — `references/troubleshooting.md` → «Не могу зайти на сервер по SSH».

---

## Карта переезда (шаги внутри стадий)

Ниже — детальные шаги. Они сгруппированы в **стадии** из «Протокола стадий» выше: между стадиями обязательна остановка-«ворота» (объясни → разреши → сделай → подтверди). Шаги внутри одной стадии веди подряд, но спокойно.

| Шаг | Стадия | Что делаем | Подробности |
|----|----|------------|-------------|
| 0 | A. Осмотр | Что за проект и что ему нужно (Supabase, платежи, почта, AI) | ниже |
| 1 | B. Сервер и доступ | Cloud.ru: ВМ, вход по SSH, порты, домен | `references/cloudru-setup.md` |
| 2 | C. Backend | Поднять backend (Supabase) одним скриптом | `references/backend-supabase.md` |
| 3 | C. Backend | Накатить схему базы (миграции) | `references/backend-supabase.md` |
| 4 | C. Backend | Залить edge-функции | `references/backend-supabase.md` |
| 5 | D. Фронт | Перенаправить фронт на новый адрес, собрать и залить | ниже |
| 6 | (в составе C) | Домен + HTTPS — Caddy ставится на шаге 2 | `references/cloudru-setup.md` |
| 7 | E. Почта | Подключить почту (сброс пароля) — если нужно | `references/email-smtp.md` |
| 8 | F. Платежи | Подключить платежи YooKassa — если нужно | `references/payments-yookassa.md` |
| 9 | G. AI и финал | AI-функция (пользователь сам) + итоговая проверка | `references/ai-provider.md` |

После каждого шага — короткая проверка «работает ли». Между стадиями — обязательные ворота подтверждения (см. 🚦 выше).

---

## Шаг 0. Осмотр проекта

Сначала пойми, с чем имеешь дело. Прочитай в репозитории:

- `package.json` — это Vite + React? Есть ли `@supabase/supabase-js`?
- `src/integrations/supabase/client.ts` — как заданы `VITE_SUPABASE_URL` и ключ: читаются из `import.meta.env` или **зашиты прямо в коде** (Lovable иногда инлайнит). Запомни — на шаге 5 это важно.
- `supabase/functions/` — какие edge-функции есть. Открой каждую, отметь:
  - какие секреты читает (`Deno.env.get(...)`) — AI-ключи, `YOOKASSA_*` и т.п.;
  - есть ли платёжные функции (`create-payment`, `*webhook*`);
  - какой AI-провайдер зашит (часто `ai.gateway.lovable.dev`, OpenAI, Gemini — из РФ недоступны).
- `supabase/migrations/` — есть ли миграции (схема базы). Если их нет, базу придётся снять с облака Lovable отдельно (см. `references/backend-supabase.md`).
- Авторизация: ищи `resetPasswordForEmail`, `signUp` — если есть, **нужна почта** (шаг 7).

Кратко перескажи пользователю простыми словами, что нашёл и какие шаги ему понадобятся (не всем нужны платежи/почта). Дальше идите только по нужным шагам.

---

## Шаг 1. Cloud.ru: виртуалка, домен, DNS

Полная инструкция со всеми кнопками — в `references/cloudru-setup.md`. Там же в разделах «0» и «0.1» — **как устроено облако Cloud.ru, что платно (публичный IP ≈146 ₽/мес), и как попробовать бесплатно**. Ключевой факт: сама free-tier ВМ бесплатна, но публичный IP — нет; при привязке карты Cloud.ru даёт **4000 ₽ бонусами на 60 дней**, которых хватает оплатить IP примерно на 2 месяца. Сначала коротко объясни это пользователю, потом веди по шагам:

1. Зарегистрироваться на cloud.ru и **привязать банковскую карту** — за это дают **4000 ₽ бонусами на 60 дней**, которыми оплачивается публичный IP (~146 ₽/мес). Баланс/бонусы должны быть положительными (условие free tier).
2. Создать виртуальную машину: **Ubuntu 22.04+**, минимум **2 vCPU / 4 ГБ RAM / 30 ГБ** (free tier как раз такой). При создании пользователь **сам задаёт логин и пароль** (логин часто `admin`) — пусть запомнит их, они понадобятся тебе (см. «Доступы к серверу»).
3. Записать **публичный IP** виртуалки.
4. **Включить вход по SSH.** По умолчанию образ Cloud.ru пускает по SSH **только по ключу** — пароль, заданный при создании ВМ, снаружи не работает (`Permission denied (publickey)`). Для новичка проще всего разово включить вход по паролю прямо в **браузерной консоли Cloud.ru** (там логин/пароль пускают). Пользователь один раз заходит: панель Cloud.ru → его ВМ → кнопка **«Консоль»** — и вставляет одну команду (подробности и команда — в `references/cloudru-setup.md` → «Включить вход по паролю»). Она же ставит `fail2ban` (защита от перебора пароля ботами). После этого ты заходишь по логину/паролю откуда угодно. Продвинутым пользователям можно вместо этого добавить ssh-ключ — но пароль проще и не привязан к одному компьютеру.
5. **Домен.** Покупать необязательно — предложи пользователю выбор (подробно в `references/cloudru-setup.md` → «Домен»):
   - **По умолчанию рекомендуй бесплатный `<IP>.sslip.io`**: домен = `<публичный-IP>.sslip.io`, он сразу резолвится в этот IP. Регистрировать и настраивать A-запись **не нужно** — можно сразу к шагу 2.
   - Если пользователь хочет «красивое» имя — свой `.ru` (~189 ₽/год у Beget/REG.RU), делегировать на DNS и создать **A-запись** на IP виртуалки. Предупреди про идентификацию через Госуслуги (ЕСИА) с 01.09.2026.
   - Не предлагай DuckDNS (заблокирован в РФ) и туннели (Cloudflare/Serveo) — не вписываются в схему.
6. **Открыть порты 80 и 443 в брандмауэре Cloud.ru (обязательно, делает пользователь в панели).** По умолчанию у ВМ открыт только порт 22 (SSH) — порты 80/443 закрыты, и пока их не открыть, **сайт не виден снаружи, а Caddy не сможет получить HTTPS-сертификат** (Let's Encrypt не достучится до сервера). Это правило **в группе безопасности**, по SSH его не сделать. Проведи пользователя: панель Cloud.ru → **Сеть → Группы безопасности** → группа его ВМ → вкладка **«Правила» → «Добавить правило»**, и добавить два правила входящего трафика: **TCP порт 80** и **TCP порт 443**, источник `0.0.0.0/0`. Порт 8000 **не открывать**. Подробно — `references/cloudru-setup.md` → «Брандмауэр». Правило применяется не мгновенно (до ~минуты).

Проверка: `RSH "echo OK"` (т.е. вход по логину/паролю) проходит; `ping <домен>` показывает нужный IP (для `.ru` DNS может обновляться до часа; для `sslip.io` — мгновенно); порты 80/443 открыты снаружи — проверь с локальной машины: `nc -z -G 6 <IP> 80 && nc -z -G 6 <IP> 443 && echo "порты открыты"`.

**Не иди дальше, пока (а) DNS не указывает на сервер и (б) порты 80/443 не открыты снаружи** — Caddy не выдаст HTTPS-сертификат без обоих условий. Для `sslip.io` пункт (а) выполнен сразу, но (б) — обязательный ручной шаг. Если запустить backend при закрытых портах, Caddy несколько раз не сможет выпустить сертификат и уйдёт в долгую паузу (а то и на недоверенный staging-сертификат) — тогда после открытия портов перезапусти Caddy начисто, см. `references/cloudru-setup.md` → «HTTPS не поднялся».

> 🚦 **Ворота стадии B → C.** Это конец стадии «Сервер и доступ». Прежде чем поднимать backend, подтверди пользователю простыми словами: «Я зашёл на твой сервер по логину и паролю — доступ работает, я сохранил его у себя. Домен указывает на сервер, порты для сайта открыты. Всё готово к следующей стадии. Дальше я устанавливаю «движок» сайта — базу данных, вход, хранилище (это стадия C, займёт несколько минут, от тебя ничего не нужно). **Начинаю?**» Дождись «да».

---

## Шаг 2. Backend на сервере (один скрипт)

Подробно — в `references/backend-supabase.md`. Суть: на сервере поднимается self-hosted Supabase в Docker + Caddy для HTTPS.

1. Залей на сервер установочный скрипт и его помощников (в домашнюю папку пользователя, не в `/root`). Нужны все четыре файла — без `patch-compose.py`/`functions-main.ts` функции не увидят секреты и публичные вебхуки не заработают:
   ```bash
   set -a; . .ru-deploy/credentials.env; set +a
   export SSHPASS="$RU_PASS"   # пароль через env, не в argv (см. «Хелпер для ssh-команд»)
   sshpass -e scp -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=.ru-deploy/known_hosts \
     "$SKILL_DIR/assets/server-setup.sh" "$SKILL_DIR/assets/gen-keys.py" "$SKILL_DIR/assets/Caddyfile" \
     "$SKILL_DIR/assets/patch-compose.py" "$SKILL_DIR/assets/functions-main.ts" \
     "$RU_USER@$RU_HOST:~/"
   ```
2. Запусти установку (подставь домен пользователя):
   ```bash
   RSH "bash ~/server-setup.sh <домен>"
   ```
   (если вход по ключу, а не паролю — `RU_PASS` пуст, и `sshpass` не нужен; используй обычный `scp`/`ssh`.)
   Скрипт сам: включит swap (страховка от нехватки памяти на 4 ГБ), поставит Docker и Caddy, склонирует Supabase, **сгенерирует все ключи** (`POSTGRES_PASSWORD`, `JWT_SECRET`, `ANON_KEY`, `SERVICE_ROLE_KEY`), пропишет домен, поднимет контейнеры и настроит Caddy с авто-HTTPS.
3. В конце скрипт напечатает **`ANON_KEY`** (он же `VITE_SUPABASE_PUBLISHABLE_KEY`) — сохрани, нужен на шаге 5.

Проверка: `https://<домен>/rest/v1/` отвечает (обычно ошибкой авторизации — это нормально, значит API живой и HTTPS есть).

---

## Шаг 3. Схема базы (миграции) — отдельный обязательный шаг

**Это самый пропускаемый шаг, и его пропуск ломает сайт незаметно.** Частый баг прошлых деплоев: backend подняли, фронт залили, а миграции к базе **не применили** → схема `public` пустая → сайт открывается, но падает с `Could not find the table 'public.<...>'`, как только пользователь войдёт или что-то сохранит. Поэтому миграции — **самостоятельный шаг с самопроверкой**, а не «между делом».

Запусти готовый скрипт из корня проекта — он применяет миграции по порядку, **сам проверяет**, что схема накатилась (иначе падает), и делает идемпотентный бэкафилл профилей:
```bash
set -a; . .ru-deploy/credentials.env; set +a
RU_HOST="$RU_HOST" RU_USER="$RU_USER" RU_PASS="$RU_PASS" bash "$SKILL_DIR/assets/apply-migrations.sh"
```

Что делает скрипт и как читать его выход:
- **Есть `supabase/migrations/*.sql`** → применяет их (по порядку имён, с остановкой на первой ошибке), печатает список созданных таблиц, бэкафиллит профили существующих пользователей. Код выхода `0` = схема на месте.
- **Папки миграций НЕТ** → выходит с кодом `3` и просит снять схему из облака Lovable (`pg_dump --schema-only`). **Остановись и скажи об этом пользователю** — без схемы дальше идти нельзя (см. `references/backend-supabase.md` → «Вариант Б: миграций нет»). Сними схему, положи в `supabase/migrations/` и запусти скрипт снова.
- **Ошибка применения** (код `2`) → разберись по выводу psql (часто: повтор без guard'ов, ссылка на отсутствующую роль/расширение), поправь миграцию, запусти заново.

Перенос **существующих данных** из облака Lovable (если они нужны) — отдельная процедура, см. справочник.

---

## Шаг 4. Edge-функции

См. `references/backend-supabase.md` → «Edge-функции». Кратко:

1. Залей функции и добавь их секреты на сервер (AI-ключ, `YOOKASSA_*` и т.д. — то, что нашёл на шаге 0) скриптом (доступы он берёт из окружения):
   ```bash
   set -a; . .ru-deploy/credentials.env; set +a
   RU_HOST="$RU_HOST" RU_USER="$RU_USER" RU_PASS="$RU_PASS" bash "$SKILL_DIR/assets/deploy.sh" functions "$RU_DOMAIN"
   ```
2. Контейнер функций уже видит секреты из `.env` — `server-setup.sh` добавил сервису `functions` строку `env_file` сам (через `patch-compose.py`). Вручную YAML править не нужно. После добавления новых секретов в `.env` перезапусти функции: `RSH "cd /opt/supabase/docker && sudo docker compose up -d functions"`.
3. Если функция — **публичный вебхук** (зовётся без токена, например YooKassa): пометка `// verify_jwt = false` в коде на self-hosted **не работает**; вместо этого добавь имя функции в `PUBLIC_FUNCTIONS` в серверном `.env` (см. `references/backend-supabase.md` → «Публичные функции без JWT»). Это шаг 8 (платежи) — здесь просто помни про механизм.
4. Функции доступны по `https://<домен>/functions/v1/<имя>`.

> 🚦 **Ворота стадии C → D.** Это конец стадии «Backend». Перед воротами **проверь сам**, что (а) все сервисы здоровы (`docker compose ps`), (б) API отвечает по HTTPS, (в) **схема базы накатилась** — это подтверждает шаг 3: `apply-migrations.sh` завершился с кодом `0` и напечатал список таблиц. Если он вышел с кодом `3` (нет миграций) или `2` (ошибка) — **ворота закрыты**, не переходи к фронту: разберись со схемой, иначе сайт упадёт при первом входе. Только когда схема подтверждена, скажи пользователю: «Движок сайта поднят — база (таблицы на месте), вход, хранилище и функции работают, я всё проверил, API отвечает по защищённому HTTPS. Дальше я переключаю сам сайт (фронтенд) на этот новый сервер, собираю и заливаю — после этого сайт откроется на твоём домене (стадия D). **Продолжаю?**» Дождись «да».

---

## Шаг 5. Фронт на новый адрес

Минимальная правка кода в проекте — только адрес и ключ backend. **Оба значения должны точно совпадать с серверными**, иначе сайт не подключится.

1. В `src/integrations/supabase/client.ts`:
   - если URL/ключ читаются из `import.meta.env` — создай `.env` в корне:
     ```
     VITE_SUPABASE_URL=https://<домен>
     VITE_SUPABASE_PUBLISHABLE_KEY=<ANON_KEY с шага 2>
     ```
   - если они **зашиты прямо в коде** — замени значения на новый домен и `ANON_KEY` (или перепиши на чтение из `import.meta.env`).

   **Критично — ключ должен совпадать с серверным `ANON_KEY`.** `VITE_SUPABASE_PUBLISHABLE_KEY` — это и есть серверный `ANON_KEY` (его печатает `server-setup.sh`). Если во фронте останется ключ от другого/старого сервера, Kong ответит `401 Unauthorized`, а в браузере это выглядит как `Failed to fetch`. Сверь явно перед сборкой: `RSH "sudo grep '^ANON_KEY=' /opt/supabase/docker/.env"` → это значение и должно стоять в `VITE_SUPABASE_PUBLISHABLE_KEY`. (Скрипт заливки сам сверяет адрес и ключ с сервером до сборки и остановится с подсказкой, если не совпало.)
2. Собери и залей фронт:
   ```bash
   set -a; . .ru-deploy/credentials.env; set +a
   RU_HOST="$RU_HOST" RU_USER="$RU_USER" RU_PASS="$RU_PASS" bash "$SKILL_DIR/assets/deploy.sh" frontend "$RU_DOMAIN"
   ```
   (скрипт делает `npm run build` и копирует `dist/` в `/var/www/app` на сервере; Caddy уже раздаёт эту папку).

Проверка: открой `https://<домен>` — сайт грузится, существующий пользователь входит. **Новая регистрация заработает только после настройки почты (шаг 7)** — по умолчанию вход требует подтверждения email (безопасно), а письма уходят лишь когда настроен SMTP. Если проекту нужно проверить регистрацию прямо сейчас, до почты — backend можно поднять с временным `EMAIL_AUTOCONFIRM=true` (см. `references/backend-supabase.md` → «Безопасные дефолты»), но в боевом режиме это потом верни в `false`.

> 🚦 **Ворота стадии D → дальше.** Сайт уже живёт на новом домене. Подтверди пользователю: «Готово — твой сайт открывается на `https://<домен>`, вход работает, всё крутится на российском сервере.» Затем назови, что осталось **исходя из осмотра (стадия A)**: почта (если есть сброс пароля), платежи (если есть оплата), AI (настраивает сам). Спроси, с чего продолжить, и веди дальше по одной стадии. Если ничего из этого проекту не нужно — поздравь с завершением и предложи финальную проверку.

---

## Шаг 6. Домен и HTTPS

Caddy ставится и настраивается на шаге 2 и **сам** получает бесплатный сертификат Let's Encrypt, как только DNS указывает на сервер. Отдельных действий обычно не нужно. Если сертификат не выдался — `references/cloudru-setup.md` → «HTTPS не поднялся».

### Смена домена (sslip.io → свой .ru, или любой другой)

Запускается, когда пользователь переходит с временного `<IP>.sslip.io` на настоящий домен (или меняет домен). **Домен зашит в ТРЁХ местах — поменяй все, иначе фронт будет стучаться по старому адресу и упадёт с `Failed to fetch`.** Это частый баг: поменяли в одном месте, забыли в других.

Сделай по порядку:

1. **Убедись, что новый домен указывает на сервер** (A-запись на IP) и порты 80/443 открыты — иначе Caddy не выдаст сертификат на новый домен. Проверка: `ping <новый-домен>` → IP сервера.
2. **Сервер — все серверные URL разом** (готовый скрипт, чтобы не забыть ни одну переменную):
   ```bash
   set -a; . .ru-deploy/credentials.env; set +a
   export SSHPASS="$RU_PASS"
   sshpass -e scp -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=.ru-deploy/known_hosts \
     "$SKILL_DIR/assets/set-domain.sh" "$RU_USER@$RU_HOST:~/"
   RSH "bash ~/set-domain.sh <новый-домен>"
   ```
   Скрипт меняет `SITE_URL`, `API_EXTERNAL_URL`, `SUPABASE_PUBLIC_URL`, `ADDITIONAL_REDIRECT_URLS`, перенастраивает Caddy на новый домен и перезапускает `auth`+`kong`.
3. **Фронт — обнови адрес и ПЕРЕСОБЕРИ** (адрес backend вшивается в сборку, без пересборки в JS останется старый):
   - в локальном `.env` проекта: `VITE_SUPABASE_URL=https://<новый-домен>` (если адрес зашит прямо в `src/integrations/supabase/client.ts` — поправь там);
   - обнови `RU_DOMAIN` в `.ru-deploy/credentials.env`;
   - пересобери и залей: `RU_HOST="$RU_HOST" RU_USER="$RU_USER" RU_PASS="$RU_PASS" bash "$SKILL_DIR/assets/deploy.sh" frontend "<новый-домен>"`.
4. **Проверь** (через ~30 сек после перезапуска): `https://<новый-домен>` открывается, вход работает, в консоли браузера нет `Failed to fetch`. Со стороны сервера: `curl -sS -o /dev/null -w '%{http_code} TLS=%{ssl_verify_result}\n' https://<новый-домен>/auth/v1/health` → `200 TLS=0`.
5. **Старый `sslip.io`-домен** после смены перестаёт обслуживаться Caddy — это нормально, ссылки на него больше не используются.

Подробный чек-лист «где встречается домен» — в `references/troubleshooting.md` → «Failed to fetch после смены домена».

---

## Шаг 7. Почта (сброс пароля)

Нужен, если в проекте есть сброс пароля или подтверждение email. **Полная пошаговая настройка Selectel (порты, логин, ошибка 535, безопасность ключа) — в `references/email-smtp.md`.** Кратко:

1. Пользователь заводит почтовый ресурс **Selectel** (по умолчанию; 1000 писем/мес бесплатно), подтверждает домен через **TXT-запись** в DNS, добавляет SPF/DKIM/DMARC, берёт **числовой логин + API-key** из вкладки «Информация» ресурса.
2. Ты прописываешь `SMTP_*` в `/opt/supabase/docker/.env` (хост `smtp.mail.selcloud.ru`, порт **1126** STARTTLS), пароль берёшь из `.ru-deploy/credentials.env` (не из чата), и пересоздаёшь контейнер `auth` (`up -d --force-recreate auth` — иначе `SMTP_*` не подхватятся).
3. Если API-key был засвечен (скриншот/чат) — **посоветуй перевыпустить** его в Selectel.
4. По желанию — русские шаблоны писем (`assets/email-templates/`, как подключить — в справочнике).

Проверка: регистрация / «Забыли пароль?» → письмо приходит. При ошибке `535 invalid smtp auth key` — дело в логине/ключе, не в DNS (см. справочник → «типовые ошибки»).

---

## Шаг 8. Платежи YooKassa

Нужен, если в проекте есть оплата. Подробно — `references/payments-yookassa.md`. Кратко:

1. Пользователь в личном кабинете YooKassa берёт `shopId` и секретный ключ → ты прописываешь `YOOKASSA_SHOP_ID` и `YOOKASSA_SECRET_KEY` в `/opt/supabase/docker/.env`.
2. В коде платёжной функции поменяй `return_url` со старого `*.lovable.app` на `https://<домен>/...`.
3. Пользователь в кабинете YooKassa регистрирует URL уведомлений (вебхук): `https://<домен>/functions/v1/<имя-webhook-функции>`, событие `payment.succeeded`.
4. Вебхук зовётся YooKassa **без токена**, а backend по умолчанию требует JWT. Пометка `// verify_jwt = false` в коде функции на self-hosted **не работает** (роутер её игнорирует). Добавь имя вебхук-функции в `PUBLIC_FUNCTIONS` в серверном `.env` и перезапусти функции — точечно, не снимая защиту с остальных (см. `references/backend-supabase.md` → «Публичные функции без JWT»).
5. **Важно (безопасность):** типовой Lovable-вебхук не проверяет, что запрос реально от YooKassa — любой может проставить «оплачено». Добавь перепроверку статуса через YooKassa API (готовый сниппет — в справочнике). Для реальных денег это обязательно.

Проверка: тестовый платёж проходит, заказ в базе становится `paid`.

---

## Шаг 9. AI-функция (пользователь сам)

Если в проекте есть AI (распознавание фото, генерация и т.п.), он почти всегда ходит в недоступный из РФ шлюз (`ai.gateway.lovable.dev`, OpenAI, Gemini). Это **пользователь донастраивает сам** — скилл только показывает, где шов. Краткий разбор вариантов (GigaChat, YandexGPT) и что именно менять — в `references/ai-provider.md`. Не углубляйся, если пользователь не просит.

---

## Итоговая проверка

Пройди вместе с пользователем финальный чек-лист из `references/troubleshooting.md` → «Финальная проверка». Если что-то не работает — там же типовые ошибки новичков и как их чинить.

---

## Статус продакшена и экономия места

Когда пользователь спрашивает **«что там с сервером / как продакшен / всё ли работает»** — не собирай команды вручную, запусти готовый скрипт-отчёт. Он показывает всё на одном экране: нагрузку и память ВМ, health каждого контейнера Supabase, внешнюю доступность сайта по HTTPS (с проверкой сертификата) и расход места.

```bash
set -a; . .ru-deploy/credentials.env; set +a
export SSHPASS="$RU_PASS"   # пароль через env, не в argv
# залей скрипт рядом (один раз) и запусти
sshpass -e scp -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=.ru-deploy/known_hosts \
  "$SKILL_DIR/assets/server-status.sh" "$RU_USER@$RU_HOST:~/"
RSH "RU_DOMAIN=$RU_DOMAIN bash ~/server-status.sh"
```

Перескажи пользователю результат простыми словами (не вываливай сырой вывод): «сайт открывается, все сервисы здоровы, диск занят на X%».

### Экономия места (важно для free-tier 30 ГБ)

Образы Supabase сами по себе ~9 ГБ — это норма, их не трогаем. Реальный риск переполнения диска со временем — **две вещи**, и обе уже под контролем:

1. **Логи контейнеров.** `server-setup.sh` ставит лимит в `/etc/docker/daemon.json` (`max-size 10m`, `max-file 3`) — логи ротируются и не растут бесконечно. На сервере, поднятом старой версией скрипта без лимита, добавь этот файл и перезапусти Docker (см. `references/troubleshooting.md` → «Кончается место на диске»).
2. **Мусор после пересборок** (dangling-образы, build cache). Чистится по запросу:
   ```bash
   RSH "bash ~/server-status.sh --clean"
   ```
   Режим `--clean` удаляет **только мусор** (dangling-образы, build cache, обрезает логи >20 МБ, чистит apt-кэш) и **не трогает** рабочие контейнеры, тома и базу данных.

> ⚠️ **Никогда** не предлагай `docker system prune -a` или удаление volumes ради места — это снесёт рабочие образы Supabase и **базу данных пользователя**. Только `--clean` из `server-status.sh` или точечные команды.
