Всем привет. Хочу поделиться тем, как я, совершенно без навыков 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 = 8080API_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/projectsreadonly 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).
Разобравшись в мини-проекте, как работают эти два брата — 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:
# ----------------------------------------------------# ДЕПЛОЙ 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 для отслеживания метрик нод и отображения их профиле пользователей, как бизнес фича.
ссылка на оригинал статьи https://habr.com/ru/articles/1037922/