Проблема

У аквариумистов есть общий ритуал. Проснулся, проверил рыб, потестил воду, загуглил “почему у неона побледнел бок”, открыл три форума, нашёл противоречивые советы, закрыл в отчаянии.

Информация размазана по десяткам источников. Половина устарела. Русскоязычных ресурсов, которые объединяли бы энциклопедию, торговлю и сообщество, почти нет. Есть форумы на phpBB из 2010-х, есть группы в VK, есть Avito. Единой штуки нет.

Я решил написать инструмент для себя. Потом подумал, что раз уж пишу, можно сразу для всех.

Что умеет платформа

Best Corals (best-corals.ru) закрывает несколько задач, которые раньше решались тремя приложениями, блокнотом и группой в Telegram.

Дневник аквариума. Добавляешь свой аквариум, указываешь тип, объём и оборудование. Записываешь параметры воды. Платформа строит графики, напоминает о подменах и ругается, когда pH уехал. Есть быстрый ввод: одна строка, Tab для перехода, Enter для сохранения. Когда стоишь у аквариума с мокрыми руками, это удобнее, чем заполнять формы.

Справочник видов. 117 видов: рыбы, кораллы, беспозвоночные. Добавляешь обитателей в виртуальный танк, система показывает, кто с кем уживётся. Если вида нет, можно отправить заявку, и AI заполнит характеристики. Работает не идеально, но быстрее, чем вручную.

AI-ветеринар. Загружаешь фото больной рыбы, описываешь симптомы. Mistral Pixtral Large смотрит на картинку и предлагает возможные диагнозы с рекомендациями. Это не замена ветеринару. Но когда в два часа ночи ты видишь белые точки на плавниках, хочется хотя бы понять, насколько это срочно.

Маркетплейс и аукционы. Продаёшь кораллы, рыб, оборудование. AI генерирует описания. На аукционах есть анти-снайп: ставка в последние 5 минут продлевает время. Об этом подробнее в разделе про безопасность, потому что именно тут я провёл больше всего времени на RLS-политики.

Форум. Обычный форум с категориями. Необычная часть: AI-бот отвечает на вопросы в профильных разделах и умеет суммаризировать длинные обсуждения. Полезно, когда тема на 40 постов скатилась в холивар, а тебе нужен только ответ.

Энциклопедия болезней и автостопщиков. 28 заболеваний, 28 “автостопщиков” (это существа, которые приезжают на живых камнях). Некоторые безобидные, некоторые жрут кораллы. AI может определить автостопщика по фото.

Социальная лента и геймификация. Лента событий сообщества с фильтрами. XP-система за активность, 10 уровней от “Новичка” до “Титана”, лидерборд. Об этом отдельно ниже.

IoT. Через MQTT-брокер контроллер аквариума может отправлять данные прямо в дневник. Пока только Noopsyche, но протокол открытый.

Стек

Когда пишешь pet-project, хочется попробовать что-то новое. Когда он начинает расти, хочется, чтобы оно просто работало.

Слой

Технология

Зачем

Frontend

React 19, Vite 6

HMR за 50мс. SSR для нишевого проекта избыточен

Стили

Tailwind CSS 4

Dark mode из коробки

Анимации

Motion (ex Framer Motion)

Пружинные анимации для UI

Backend

Express

Один файл, потом модули. 5 AI-хендлеров, уведомления, Telegram-бот

БД

Self-hosted Supabase, PostgreSQL 15

RLS, Auth, Storage, REST API. 93 таблицы, 122 RPC

AI

Mistral API

В 3-5 раз дешевле OpenAI. 8 фич на бесплатном тарифе

IoT

Mosquitto MQTT

Стандартный протокол, лёгкий

Мониторинг

Prometheus, Grafana

16 алертов

Деплой

VDS, Docker Compose

~$15/мес за всё

Self-hosted Supabase вместо Firebase

Начинал на Firebase. Удобно, быстро, но:

Firestore не PostgreSQL. Нет JOIN-ов, нет нормальных транзакций. Для маркетплейса с заказами это больно. Стоимость растёт непредсказуемо. И данные в облаке Google для нишевого русского проекта казались лишним риском.

Supabase дал мне PostgreSQL с RLS, нормальный Auth, Storage для картинок. Всё self-hosted на моём VDS в Docker.

Переезд стоил мне две недели и dbMapper.ts на 200 строк, который конвертирует snake_case в camelCase и обратно. Каждый день я писал маппинг для очередной таблицы. Каждый день ненавидел оба формата. Но альтернатива хуже: инлайн-маппинг в каждом хуке.

AI на Mistral

8 фич, все на Mistral API:

Диагностика по фото (Vision). Идентификация автостопщиков (тоже Vision). Анализ аквариума: смотрит на оборудование, параметры, обитателей, даёт оценку. SEO-описания для маркетплейса, за пару секунд. Модерация всего UGC на токсичность и спам, 11 типов контента. Суммаризация форума. Обогащение справочника: по названию рыбы заполняет все характеристики. Персональные рекомендации по улучшению системы.

Одна архитектурная деталь, которая оказалась полезной: пул API-ключей с round-robin. У Mistral щедрые бесплатные лимиты, но на один ключ их легко выжать. У меня 4 пула (chat, moderation, features, forum) с автоматическим fallback при 429. За два месяца я заплатил $0.

Зачем XP на платформе для рыбок

Аквариумистика, это хобби с длинным циклом. Запустил аквариум, месяц ждёшь азотный цикл, полгода растишь кораллы. Без причины заходить каждый день пользователь забывает о платформе через неделю.

XP-система решает это напрямую. Каждое полезное действие даёт опыт. Записал параметры воды, плюс 10 XP. Опубликовал статью, плюс 50. Ответил на форуме, плюс 5. Уровень считается по формуле floor(sqrt(xp/100)) + 1. Десять рангов. При повышении уровня вылезает тост с анимацией.

Вся логика в одной PostgreSQL-функции award_xp() с security definer. Клиент вызывает её fire-and-forget после успешного действия. Если RPC упал, ничего страшного, XP не критичен.

CREATE FUNCTION award_xp(p_user_id UUID, p_amount INT, p_reason TEXT)
RETURNS JSONB AS $$
DECLARE v_new_xp INT; v_new_level INT; v_old_level INT;
BEGIN
  SELECT xp, level INTO v_new_xp, v_old_level FROM users WHERE id = p_user_id;
  v_new_xp := COALESCE(v_new_xp, 0) + p_amount;
  v_new_level := GREATEST(1, FLOOR(SQRT(v_new_xp::FLOAT / 100.0))::INT + 1);
  UPDATE users SET xp = v_new_xp, level = v_new_level WHERE id = p_user_id;
  RETURN jsonb_build_object(
    'xp', v_new_xp, 'level', v_new_level,
    'leveled_up', v_new_level > v_old_level, 'xp_added', p_amount);
END; $$ LANGUAGE plpgsql SECURITY DEFINER;

Честно, я не знаю, работает ли это на удержание. Данных пока мало. Но мне нравится, как лидерборд выглядит.

Безопасность: 12 атак, которые пришлось закрыть

Аукционная площадка с деньгами, это магнит для злоупотреблений. Я составил список из 12 векторов (назвал “Dirty Dozen”) и прошёлся по каждому.

Пользователь пытается изменить свою роль на admin через REST API. Подставляет чужой user_id в ставку. Сокращает время аукциона, чтобы выиграть. Ставит ставку ниже минимального шага. Пытается увидеть чужие заказы. Накручивает себе репутацию.

Всё закрывается через RLS и RPC. Ни один UPDATE на аукционные данные не идёт напрямую через REST. Только через хранимую процедуру, которая проверяет все инварианты. Это параноидально, но аукционы без этого не работают.

Архитектура

Internet → Nginx (SSL) → Docker Network
                          ├── React + Express (frontend и API)
                          ├── Supabase (13 сервисов)
                          ├── Mosquitto MQTT
                          ├── Prometheus + Grafana
                          └── Portainer

93 таблицы. 122 RPC-функции. Два Docker Compose стека: Supabase и приложение.

Уведомления работают через PG LISTEN/NOTIFY, не через Supabase Realtime (об этом дальше). Триггер на INSERT в notifications кидает pg_notify, Express слушает канал через pg.Client и рассылает email через nodemailer и Telegram. Пользователь может настроить, какие типы уведомлений хочет получать в Telegram.

Приложение работает как PWA. Устанавливается на телефон, статика кэшируется. API и Supabase всегда идут через сеть, никогда через Service Worker.

Где я облажался и чему научился

Supabase Realtime на self-hosted это ловушка. Красивая идея: подписался на изменения таблицы, получаешь обновления. Реальность: на VDS с 4 GB RAM контейнер Realtime начинает падать и молча отключается. Я два дня дебажил, почему у пользователей не обновляются ставки на аукционе. Оказалось, Realtime просто умер и никому не сказал.

Убрал его полностью. Заменил на три паттерна: refetch после мутации, polling с setInterval для аукционов и чата (с обязательной паузой через usePageVisibility() в фоновых вкладках), и однократная загрузка для статики.

usePageVisibility() обязателен. Один забытый setInterval на 5 секунд в компоненте, который открыт у 50 пользователей в фоновых вкладках, это 600 запросов в минуту. Я нашёл это, когда Grafana показала подозрительную нагрузку в нерабочее время. Добавил паузу при скрытой вкладке, нагрузка упала вдвое.

RLS дебажить больно. Row Level Security в PostgreSQL отлично работает для безопасности. Но когда запрос возвращает пустой массив, ты не знаешь, данных нет или RLS отрезал. Приходится лезть в логи и перебирать политики одну за другой.

Один конкретный совет: всегда пишите (SELECT auth.uid()) вместо голого auth.uid() в политиках. Без скобок движок пересчитывает uid на каждую строку. С таблицей на 10 000 строк это заметно.

AI-модерация должна быть fail-open. Если AI-сервис упал, контент должен публиковаться, а не блокироваться. Модерация работает асинхронно: контент публикуется сразу, AI проверяет в фоне, при проблемах ставит флаг. Один раз Mistral API лежал 4 часа, и я был рад, что модерация не блокирует публикацию.

Один разработчик, это удобно до первого бага на проде в 3 ночи. Вся архитектура в голове, решения принимаются за секунды, нет созвонов. Но когда что-то падает, чинить тоже тебе. И тестировать. И деплоить. И отвечать на вопросы пользователей.

Цифры

Метрика

Значение

Пользователей

52

Видов в справочнике

117

Болезней и автостопщиков

56

Таблиц в БД

93

RPC-функций

122

AI-фич

8

Время от первого коммита

~8 недель

Инфраструктура

~$15/мес (VDS)

AI

$0 (Mistral free tier)

Проект молодой. Первый пользователь зарегистрировался 8 апреля 2026. Платформа уже работает: торговля идёт, аукционы проводятся, AI ставит диагнозы. Но 52 пользователя это не метрика успеха, это стартовая точка.

Что дальше

Мобильное приложение (PWA хорош, но нативные пуши работают лучше). Расширение IoT: больше контроллеров, автоматические алерты в Telegram. Совместные закупки живности. Может быть, англоязычная версия, если хватит сил.

Если у вас есть аквариум или вы думаете о нём, заходите на best-corals.ru. Если вы разработчик, у которого хобби превратилось в проект, расскажите в комментариях. Интересно, у кого что получилось.

React 19, Vite 6, Tailwind CSS 4, Motion, self-hosted Supabase (PostgreSQL 15), Express, Mistral AI, Mosquitto MQTT, Docker, Prometheus + Grafana. Один разработчик.