Как я без навыков fullstack-разработчика сделал свой SaaS

Страницы:  1

Ответить
 

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-страничку.
Тут так же стоит упомянуть одну делаль - ИИшки тупые. В плане они не могут удерживать весь контекст в памяти. Поэтому мною было принято решение сделать собственную дизайн систему, адаптированную для ИИ.
Когда я прошу ИИ накатать мне новую страницу или компонент - она может расставить рандомные классы для стилизации текста и компонентов. Поэтому у меня есть правило-промпт к которому обращаются ИИ, чтобы ВЕСЬ текст - шрифты, отступы, цвета, а так же компоненты - имели ОДИН И ТОТ ЖЕ стиль.
pic
тут можно увидеть как устроена типографика
По сути - это дизайн система всего проекта. В папке 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 и скидывать архив в чат. Если фича не требовала бОльшего контекста, только нужную часть, чтобы не сжигать токены впустую.
Но каждый раз руками прописывать исключения — .gitvenvnode_modulesdist и прочий мусор — такое себе. Поэтому написал 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).
pic
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:
pic
скрин 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 человек.
pic
как выглядит список юзеров для админа
У каждого пользователя в профиле отображается зверушка, которая назначается хешированием юзернейма по алгоритму djb2. Их 12 штук, все SVG.
pic
лк пользователей-И вот я тут 🙂— 21 мая 26 года — хочу прикрутить платёжный шлюз. API уже написано, но никто пока не одобрил подключение. Кто знает какие-нибудь платежные шлюзы для high risk без оформления сз/ип и прочих ненужностей? Желательно без конских комиссий, докой API и наличием Sandbox.
В будущем планирую добавить Prometheus с Grafana, Node Exporter для отслеживания метрик нод и отображения их профиле пользователей, как бизнес фича.-Источник
 
Loading...
Error