Я делаю планировщик задач Clarify. Один, в свободное время, для себя. Разработчик из меня любительский, так что профессионалы в комментариях наверняка найдут, что мне сказать. Десктоп на Flutter под Windows, на телефоне та же сборка как PWA. База и авторизация в Supabase, а для того, что нельзя делать на клиенте, был маленький FastAPI на бесплатном Render.
Про блокировки хостингов на Хабре писали много. Я хочу рассказать не про сам переезд, он как раз скучный. Хочу про то, сколько всего продолжало стучаться на старый адрес после того, как я решил, что всё перенёс. И как я этого не замечал.
Сначала я думал, что Render просто спит
История началась с другого. 5 сентября я чинил дыру во входе через Яндекс. У меня там такая схема: бэкенд меняет код Яндекса на данные пользователя и заводит в Supabase «теневой» аккаунт. Пароль у этого аккаунта вычислялся из айди Яндекса. Да, я знаю. Переделал на случайный, прогнал проверки, всё зелёное.
На следующий день я попробовал войти сам. Не вышло. Приложение написало «Ошибка сети. Проверьте интернет‑соединение».
Бесплатный Render засыпает при простое, это все знают. Поэтому первым делом я решил, что запрос не дождался холодного старта, и побежал делать повторы и новую заставку загрузки. Потом подумал на кэш PWA. Оба раза мимо.
На самом деле сервер отвечал. Он выдавал одноразовый токен для входа и следующей же строкой менял пароль теневого аккаунта. А смена пароля в Supabase Auth гасит все токены, которыми ещё не воспользовались. То есть моя же правка безопасности сломала вход наглухо. А проверки прошли, потому что проверяли куски по отдельности: пароль случайный, токен выдаётся. Войти целиком никто не пробовал.
И ещё одно. «Ошибка сети» показывалась на любой сбой после ответа сервера. С таким текстом ты идёшь искать проблему в сети. Туда я и пошёл, дважды. Когда развёл причины по разным сообщениям, баг нашёлся с первой попытки.
А потом Render просто пропал
Вход я починил 6 сентября. А 8-го выяснилось, что бэкенд не отвечает совсем. После двух дней про «сервер спит» это выглядело как издевательство.
Тут уже было не до гипотез про холодный старт. Из России перестали открываться адреса Render из 216.24.57.0/24. И режут именно IP: голый https://216.24.57.7 висит ровно так же, как *.onrender.com. Первая идея была повесить свой домен, но он привёл бы на те же самые адреса.
А вход у меня, напомню, идёт через бэкенд. Нет бэкенда, нет входа. Те, кто уже был залогинен, продолжали работать: клиент ходит в Supabase напрямую. Новый человек войти не мог. Переезжать надо было в тот же вечер.
Куда переехал
На другой зарубежный PaaS не хотелось: в следующий раз заблокируют его, и всё по новой. Полез туда, где и так лежит база, в Supabase Edge Functions. Логика простая: если закроют Supabase, у меня ляжет всё целиком, и отдельный хостинг для бэкенда тут не поможет. Зачем тогда держать две точки отказа вместо одной.
Переносить было немного. Вход через Яндекс, разбор задачи из фразы нейросетью, расшифровка голосового, вебхук Telegram‑бота и несколько служебных маршрутов: напоминания, утренняя сводка, чистка корзины. Я сделал одну функцию api с роутингом по пути, и клиент теперь знает один адрес. Он даже не хранится отдельно, а собирается из SUPABASE_URL. Одна копия адреса меньше, чем две. Дальше станет понятно, почему меня это так волнует.
Старый Python‑код я не стёр, он лежит рядом как справочник. В комментариях там записано, почему сделано так, а не иначе, и при переписывании на TypeScript это сэкономило мне кучу времени.
И заодно расписание
Напоминания о сроках до этого дёргал GitHub Actions по крону раз в пять минут. По крайней мере, я так думал. Когда утренняя сводка не пришла вообще, я посмотрел на реальные запуски. Один в 01:18, следующий в 05:52, между ними ничего. На бесплатном тарифе расписание Actions это скорее пожелание. Для напоминания «за час до» такое не годится.
Расписание уехало в саму базу, в pg_cron. Запрос в функцию делает pg_net:
select cron.schedule( 'clarify-check-due-tasks', '*/5 * * * *', $$ select net.http_post( url := '<SUPABASE_URL>/functions/v1/api/internal/check-due-tasks', headers := jsonb_build_object('X-Internal-Token', '<токен>'), body := '{}'::jsonb ); $$);
С тех пор пять минут длятся пять минут. На этом месте я решил, что переезд закончен. Зря.
Полтора месяца спустя
В конце октября я добавлял аналитику и полез смотреть схему базы. На таблицах tasks, messages и workspace_messages висело по триггеру. Вот такому:
CREATE TRIGGER "Tasks" AFTER UPDATE ON public.tasks FOR EACH ROWEXECUTE FUNCTION supabase_functions.http_request( 'https://clarify-backend-****.onrender.com/internal/notify-event', 'POST', ...);
Это Database Webhooks, я накликал их когда‑то в панели Supabase и благополучно забыл. В репозитории их нет, они живут только в базе. Поэтому поиск по onrender.com в коде их не нашёл. Ошибок тоже нигде не было: запрос уходит асинхронно, отваливается по таймауту и никому ничего не говорит.
Из‑за них с 8 сентября не работали уведомления «вам назначили задачу» и пуши о сообщениях. Обработчик на новом месте стоял и ждал, а база звонила на старый номер. Сам я этими функциями пользуюсь редко, потому и не заметил.
Приятный бонус: в определении триггера открытым текстом лежал внутренний токен. А определение триггера попадает в выгрузку схемы, которую я держу в git. Токен, понятно, теперь под замену.
Теперь все триггеры вызывают одну функцию, а та берёт токен из таблицы в закрытой схеме. Заодно триггер на задачах стал срабатывать только при смене исполнителя. Раньше он стрелял HTTP‑запросом на каждую галочку.
Если у вас тоже Supabase, вот запрос, который их найдёт:
select tgrelid::regclass as table_name, tgnamefrom pg_triggerwhere not tgisinternal and pg_get_triggerdef(oid) ilike '%http%';
И кнопка на собственном сайте
Вторая находка неловкая. Веб‑версия давно живёт на GitHub Pages. А кнопка «Clarify в браузере» на лендинге всё это время вела на старый адрес на Render. Человек из России нажимал, смотрел, как ничего не грузится, и закрывал вкладку. Сколько таких было, я не знаю: аналитики тогда ещё не было.
Почему я не заметил? Потому что приложение у меня давно установлено, и на кнопку на своём же сайте я не нажимал ни разу. Стыдно, но так. Тот же адрес ещё стоял в политике конфиденциальности, там он записан просто текстом, и никакой линтер его не ловит.
Где ещё прячется старый адрес
Если вы тоже съезжаете с Render, вот места, где старый домен нашёлся у меня. Поиск по репозиторию приложения покрывает далеко не всё:
-
триггеры и вебхуки в базе (запрос выше);
-
задания по расписанию: pg_cron, Actions, внешние пингеры;
-
вебхуки сторонних сервисов и адреса возврата OAuth;
-
сайт, лендинг и политика конфиденциальности, там адрес часто записан просто текстом.
И самое дурацкое и самое полезное: пройдите путь нового пользователя с чистого телефона без VPN. Открыть сайт, нажать кнопку, войти, завести задачу. Две из трёх моих проблем нашлись бы за пять минут.
Сам Clarify бесплатный, без рекламы и подписок. Если интересно потыкать: vantra‑labs.github.io/clarify. А если вы переезжали с заблокированного хостинга и у вас потом тоже что‑то всплыло, расскажите в комментариях. У меня подозрение, что я нашёл не всё.
ссылка на оригинал статьи https://habr.com/ru/articles/1103922/