|
Professor Seleznov
|
Всем привет. Хочу поделиться тем, как я, совершенно без навыков fullstack-разработчика, сделал, как мне кажется, прикольный SaaS. Весь текст - мой поток мыслей и воспоминаний. Моё устройство: MacBook Air M2, 16 ГБ. У меня нет опыта работы в бигтехе, в принципе в каком либо техе, хехе 🙂-В декабре 2025 года, когда поехал в гости к родителям праздновать Новый год, столкнулся с небывалой для меня критической проблемой — отсутствием доступа к интернету. ChatGPT, Grok, Claude, YouTube, Telegram — всё. Мой родной регион был одним из первых, на ком РКН тестировал белые списки. До этого я пользовался для обхода блокировок OpenVPN — энтузиасты выкладывали ключи в открытый доступ. Да, с моей стороны это было пренебрежением безопасностью. Но меня устраивало, пока в конце 2025 года OpenVPN не забанили. Мой университет подключал студентов к своей сети через него, а теперь сосет лапу. Я не готов мириться с принудительными блокировками. Поэтому, находясь всё ещё в регионе, в поисках решения наткнулся на него — протокол Hysteria2. Быстренько развернул VPS, настроил и был рад возвращению в современный мир. Но во время чтения доки меня зацепили возможности этого протокола. По сути — протокол, удобно упакованный в Docker-контейнер, который давал отличный и удобный API.-В конце февраля 2026 года мне попалось видео на YouTube — «Как устроена оплата картой». Это видео показало мне, как работает REST API, и дало осознание: из маленьких блоков можно построить что-то большее. Мне стало настолько интересно, что я развернул контейнер на своём MacBook и теперь был готов подёргать его за ручки. Сначала курлами, но потом пришла мысль автоматизировать это через Python. Первое, с чем столкнулся — библиотека requests. С иишкой быстренько накатали код, в котором мне предстояло очень подробно разобраться. Вот кусочек:
API_HOST = "127.0.0.1" API_PORT = 8080 API_SECRET = "sixseven" USERS = { "s" : "ss", "user": "password", "test": "12345", } ALLOWED = { "s" : False, "user": True, "test": False, } def get_online_users(): url = f"http://{API_HOST}:{API_PORT}/online" headers = {"Authorization": API_SECRET} try: r = requests.get(url, headers=headers, timeout=5) r.raise_for_status() # выбросит ошибку, если не 200 return r.json() # {"username": connection_count, ...} except requests.exceptions.HTTPError as e: print(f"API вернул ошибку {r.status_code}: {r.text}") return {} except requests.exceptions.RequestException as e: print(f"Ошибка подключения к API: {e}") return {}
Не буду врать, на новичка такое производит большое впечатление. Поэтому, восхищённый этими возможностями, я решил сделать простой VPN-сервис. Исключительно чтобы разобраться и удовлетворить любопытство.-Началась разработка. Разделил проект на три части: backend, frontend, Hysteria2 — три независимые сущности, зоны ответственности которых не пересекаются. Так я познакомился с Docker Compose. Контейнер для Hysteria2 уже тянулся с Docker Hub, а через Compose весь стек поднимался за секунды. Потом важно было правильно написать Dockerfile — инструкцию по сборке образа. Причём так, чтобы часто пересобираемые слои были ниже в файле: тогда Docker переиспользует кэш для неизменившихся слоёв и не пересобирает всё с нуля при каждом изменении кода.-Дальше я попросил ИИшку выплюнуть мне README проекта со всеми эндпоинтами, чтобы другая ИИшка сделала чистый фронт. Я давно работал с Manus — это агент, который может запилить веб-приложение с нуля. Однако я три раза с нуля просил его переписать фронт. На тот момент у меня не было никакого понимания, как работает фронт, поэтому даже не мог толком объяснить, что не так. Отбросив весь говнокод от Manus (много мусора, потому что ИИ-агенты щедро наваливают рядом кучу... отладочных скриптов и вспомогательных файлов для себя), я начал писать фронт с Claude с нуля — причём с полным пониманием каждого шага. Так познакомился с Vite — бандлером, который собирает проект из TS, TSX и прочего и в процессе разработки выступает сервером: по запросу браузера отдаёт собранную HTML-страничку. Тут так же стоит упомянуть одну делаль - ИИшки тупые. В плане они не могут удерживать весь контекст в памяти. Поэтому мною было принято решение сделать собственную дизайн систему, адаптированную для ИИ. Когда я прошу ИИ накатать мне новую страницу или компонент - она может расставить рандомные классы для стилизации текста и компонентов. Поэтому у меня есть правило-промпт к которому обращаются ИИ, чтобы ВЕСЬ текст - шрифты, отступы, цвета, а так же компоненты - имели ОДИН И ТОТ ЖЕ стиль.

тут можно увидеть как устроена типографика По сути - это дизайн система всего проекта. В папке cStyles лежат *Stls.ts файлы, которые описывают стили для каждого раздела компонентов в проекте:
src/styles/ ├── tokens.ts — атомарные токены (typography, surface, radius, …) ├── animations.ts — transition, hover, press, enter, loading ├── variants.ts — colorScheme + тип ColorScheme ├── index.ts — единая точка входа, реэкспортирует всё └── cStyles/ — стили компонентов, разбитые по папкам src/components/ ├── uiStls.ts компоненты из components/ui/ ├── commonStls.ts компоненты из components/common/ ├── layoutStls.ts компоненты из components/layout/ ├── dashboardStls.ts → компоненты из components/dashboard/ ├── usersStls.ts → компоненты из components/users/ ├── serversStls.ts → компоненты из components/servers/ └── pagesStls.ts → компоненты из pages/
Сначала никакой дизайн системы не было. Страницы выглядели хаотичными. Где то отличался размер шрифтов, где-то цвета. Только потом появился промпт для перевода компонентов и страниц на дизайн систему — это md файл на 250+ строк.-Что касается ИИ — использовал много разных: Claude, Grok, Gemini, ChatGPT, Codex. Не заплатил ни цента. Создал примерно 15 Google-аккаунтов, и лимитов на весь рабочий день хватало с запасом. Штука в том, что ИИ важен контекст — как устроены эндпоинты, что лежит в .env, как связаны части проекта. Без этого каждый раз объяснять всё с нуля — боль. Поэтому я начал архивировать проект через tar и скидывать архив в чат. Если фича не требовала бОльшего контекста, только нужную часть, чтобы не сжигать токены впустую. Но каждый раз руками прописывать исключения — .git, venv, node_modules, dist и прочий мусор — такое себе. Поэтому написал bash-скрипт pack.sh. Вот как примерно он выглядел:
#!/usr/bin/env bash # # Скрипт pack — упаковка частей проекта htrBox в .tar архивы # Все архивы сохраняются на уровень выше проекта (/Users/stas/projects/) # # Использование: # pack -> весь проект (кроме исключений) # pack back -> backend + docker-compose.yaml + .env # pack front -> frontend + docker-compose.yaml + .env # pack --dry-run [all|back|front] -> только показать команду tar, не выполнять # set -euo pipefail # ------------------------------------------------ # ЖЁСТКО ЗАХАРДКОЖЕННЫЕ ПУТИ # ------------------------------------------------ readonly PROJECT_ROOT="/Users/stas/projects/htrBox" readonly OUTPUT_DIR="$(dirname "$PROJECT_ROOT")" # -> /Users/stas/projects readonly PROJECT_NAME="htrBox" # ------------------------------------------------ # Список исключений (при pack без аргументов) # ------------------------------------------------ declare -a EXCLUDES=( ".git" ".env" "backend/venv" "backend/.DS_Store" "frontend/node_modules" "frontend/.DS_Store" "frontend/package-lock.json" ".DS_Store" "other" "exclude.txt" ) и тд ...
-Ещё один трюк, который реально изменил процесс. Раньше я сразу говорил ИИ: «давай реализуем фичу». Теперь — нет. Сначала говорю: не трогай код. Опиши план внедрения фичи с TODO-маркерами и подробным описанием каждого шага — так, чтобы человек, который вообще не в теме проекта, понял что делать. Мы обкашливали план, и только потом я давал команду: вперёд, с первого пункта. Важный момент: просил ИИ отдавать только изменённые файлы — включая обновлённый TODO с текущим прогрессом после выполнения каждого шага. Копировал файлы, смотрел через git dif в VSCode, что поменялось, если все ок — двигались дальше. Кстати, GitKraken — топ. Раньше сидел на терминальном tig, но GitKraken удобнее, особенно для визуализации веток. Для одного разработчика — бесплатный 🙂 Так в чём кайф всего этого подхода? Когда ИИ начинает галлюцинировать или заканчиваются токены на аккаунте — не страшно. Можно сделать ./pack.sh, перекинуть архив с актульным контекстом на новый аккаунт вместе с актуальной историей TODO, и продолжить работу с того места где остановился. Я считаю, что такой поход к разработке относительно замедляет работу. Но это не критично, потому что дает полный контроль и понимание над процессом. Данный подход использовался исключительно для Claude, потому что он умеет работать с tar архивами (как и Manus).-Написав какую-то часть фронта и запустив всю эту шарманку, возникла куча других проблем. Бэкенд есть, данные там обновляются — но как их доставить до фронта? Причём не один раз, а постоянно. Как сделать так, чтобы после перезагрузки страницы не требовался повторный вход? Как не долбить сервер лишними запросами и кэшировать данные? Вопросов было много. Насоветовавшись с ИИ, познакомился с концепцией DOM-дерева и отобрал для себя стек:
- TanStack Query — для автоматического обновления данных на странице
- Zustand + localStorage — для кэширования состояния между сессиями
- wouter — лёгкий роутер для навигации, значительно проще React Router
- httpOnly cookie — для хранения refresh token; проставляется через заголовок в ответе бэкенда
Чтобы не тонуть в сложности большого проекта, сделал отдельный мини-проект — максимально упрощённую версию, чтобы понять концепцию поллинга через TanStack Query и кэширование через Zustand + cookie. Ну и заодно разобрался, как работает JWT-авторизация. Ещё открыл для себя Postman. Через него тестировать API — кайф. Особенно классная фича — наследование авторизации всей коллекции сверху вниз: не нужно прописывать pre- и post-scripts для каждого запроса (правда бился 3 часа почему postman не видит cookie - надо выставить тумблер Cookie Jar для каждого эндпоинта в OFF).

postman moooood-Разобравшись в мини-проекте, как работают эти два брата — frontend и backend, — пришло время следующей главы: базы данных. На тот момент у меня не было вообще никакого опыта. Что такое foreign key или primary key — загадка. Поэтому скачал DataGrip, сделал дамп базы с прода и начал разбираться. PostgreSQL уже крутился на проде, но что происходило внутри — было непонятно. Для понимания баз данных сделал локальную среду для лабораторных работ по SQL на базе PostgreSQL 17 в Docker-контейнере. Нашёл классный курс от Postgres Pro и их же книгу по SQL, где объясняются ключевые концепции: как проектировать схему, что такое нормализация, как писать запросы, как делать DDL. После этого база данных стала для меня интересным объектом исследования. К тому же пришло понимание, что ИИшка не все связи между таблицами выстроила, поэтому могли быть висячие данные (которые должны удаляться на ON DELETE CASCADE).-Разработка идет полным ходом. Конечно же ВСТАЛ вопрос об автоматизации деплоя на сервера. Такое тут тоже есть. Bash скрипт. Скрипт накатывает docker container Hysteria2 на ноды в других странах (которые принимают соединение от пользователей с последующим проксированием) и на backend в Yandex Cloud. Вот пример функции деплоя на YC с автоматическим продлением сертификатов через certbot:

скрин admin dashboard
# ---------------------------------------------------- # ДЕПЛОЙ Yandex Cloud (frontend + backend + postgres) # ---------------------------------------------------- deploy_yc() { echo "" echo "-------------------------------------------" echo " 🚀 Деплой -> Yandex Cloud ($YC_HOST)" echo "-------------------------------------------" [ ! -f "$SCRIPT_DIR/certificates/yandex-cloud.ini" ] && fail "certificates/yandex-cloud.ini не найден" [ ! -f "$SCRIPT_DIR/infra/.env" ] && fail "infra/.env не найден" [ ! -f "$SCRIPT_DIR/infra/nginx.conf" ] && fail "infra/nginx.conf не найден" log "Создаём директории на YC..." ssh -i "$YC_KEY" "$YC_USER@$YC_HOST" "mkdir -p $YC_DIR $YC_DATA_DIR/pgdata" log "Копируем backend/ на YC..." tar -czf /tmp/backend.tar.gz \ --exclude="venv" \ --exclude="__pycache__" \ --exclude="*.pyc" \ --exclude="pytest.ini" \ --exclude=".pytest_cache" \ --exclude="requirements-dev.txt" \ --exclude="TODO" \ -C "$PROJECT_ROOT" backend scp -i "$YC_KEY" /tmp/backend.tar.gz "$YC_USER@$YC_HOST:/tmp/" ssh -i "$YC_KEY" "$YC_USER@$YC_HOST" "tar --warning=no-unknown-keyword -xzf /tmp/backend.tar.gz -C $YC_DIR && rm /tmp/backend.tar.gz" rm /tmp/backend.tar.gz log "Копируем frontend/ на YC..." tar -czf /tmp/frontend.tar.gz \ --exclude="node_modules" \ --exclude="dist" \ --exclude="TODO" \ --exclude="nginx.conf" \ -C "$PROJECT_ROOT" frontend scp -i "$YC_KEY" /tmp/frontend.tar.gz "$YC_USER@$YC_HOST:/tmp/" ssh -i "$YC_KEY" "$YC_USER@$YC_HOST" "tar --warning=no-unknown-keyword -xzf /tmp/frontend.tar.gz -C $YC_DIR && rm /tmp/frontend.tar.gz" rm /tmp/frontend.tar.gz # nginx.conf для prod передаётся отдельно, поверх распакованного frontend/ # dev-файл (frontend/nginx.conf в репозитории) не затрагивается log "Копируем prod nginx.conf на YC (infra/nginx.conf -> frontend/nginx.conf)..." scp -i "$YC_KEY" "$SCRIPT_DIR/infra/nginx.conf" "$YC_USER@$YC_HOST:$YC_DIR/frontend/nginx.conf" log "Копируем docker-compose и .env..." scp -i "$YC_KEY" "$SCRIPT_DIR/infra/docker-compose.yaml" "$YC_USER@$YC_HOST:$YC_DIR/docker-compose.yaml" scp -i "$YC_KEY" "$SCRIPT_DIR/infra/.env" "$YC_USER@$YC_HOST:$YC_DIR/.env" log "Копируем Cloudflare токен..." ssh -i "$YC_KEY" "$YC_USER@$YC_HOST" "mkdir -p ~/.secrets && chmod 700 ~/.secrets" scp -i "$YC_KEY" "$SCRIPT_DIR/certificates/yandex-cloud.ini" "$YC_USER@$YC_HOST:~/.secrets/cloudflare-yc.ini" ssh -i "$YC_KEY" "$YC_USER@$YC_HOST" "chmod 600 ~/.secrets/cloudflare-yc.ini" log "Проверяем сертификат для stdoq.ru..." ssh -i "$YC_KEY" "$YC_USER@$YC_HOST" ' CERT_PATH="/etc/letsencrypt/live/stdoq.ru/fullchain.pem" RENEW_NEEDED=false if [ ! -f "$CERT_PATH" ]; then echo " -> Сертификат не найден, получаем новый..." RENEW_NEEDED=true else EXPIRY=$(openssl x509 -enddate -noout -in "$CERT_PATH" | cut -d= -f2) EXPIRY_TS=$(date -d "$EXPIRY" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$EXPIRY" +%s 2>/dev/null) NOW_TS=$(date +%s) DAYS_LEFT=$(( (EXPIRY_TS - NOW_TS) / 86400 )) echo " -> Сертификат найден, осталось дней: $DAYS_LEFT" [ "$DAYS_LEFT" -lt 30 ] && RENEW_NEEDED=true fi if [ "$RENEW_NEEDED" = true ]; then if ! command -v certbot &>/dev/null; then echo " -> Устанавливаем certbot..." sudo apt-get update -qq && sudo apt-get install -y -qq certbot python3-certbot-dns-cloudflare fi sudo certbot certonly \ --dns-cloudflare \ --dns-cloudflare-credentials ~/.secrets/cloudflare-yc.ini \ -d stdoq.ru -d www.stdoq.ru \ --email ТУТ БЫЛ МОЙ EMAIL@yandex.ru \ --agree-tos --no-eff-email \ --non-interactive echo " -> Сертификат получен ✓" else echo " -> Сертификат актуален, пропускаем ✓" fi ' log "Запускаем контейнеры (--build)..." ssh -i "$YC_KEY" "$YC_USER@$YC_HOST" " cd $YC_DIR docker compose -f docker-compose.yaml up -d --build --force-recreate frontend backend postgres docker image prune -f " echo "" echo "---- Статус контейнеров ----" ssh -i "$YC_KEY" "$YC_USER@$YC_HOST" "docker ps --filter name=frontend --filter name=backend --filter name=postgres --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'" echo "" echo "---- Логи backend (последние 20 строк) ----" ssh -i "$YC_KEY" "$YC_USER@$YC_HOST" "docker logs backend --tail=20" log "Yandex Cloud задеплоен ✓" }
Где Ansible? Где Terraform? Для трех или пяти серверов не критично купить и настроить их ручками. Пытался переписать bash скрипты для deploy и cleanup (он тоже есть) на Ansible PlayBooks, но повяз в этом. К тому же не хотелось лишать пользователей (в случае неудачно написанного playbook'a) их законного доступа к интернету. На данный момент bash отлично справляется - решил я.-Немного скринов, как это выглядит: Сейчас backend стреляет всем пулом пользователей. Если пользователей станет больше 100 (что я не планирую), придется добавлять пагинацию. Тогда данные будут лететь, например, по 50 человек.

как выглядит список юзеров для админа У каждого пользователя в профиле отображается зверушка, которая назначается хешированием юзернейма по алгоритму djb2. Их 12 штук, все SVG.

лк пользователей-И вот я тут 🙂— 21 мая 26 года — хочу прикрутить платёжный шлюз. API уже написано, но никто пока не одобрил подключение. Кто знает какие-нибудь платежные шлюзы для high risk без оформления сз/ип и прочих ненужностей? Желательно без конских комиссий, докой API и наличием Sandbox. В будущем планирую добавить Prometheus с Grafana, Node Exporter для отслеживания метрик нод и отображения их профиле пользователей, как бизнес фича.-Источник
|