Большинство наших «проектов мечты» умирают не потому, что идея плохая, а потому что мы останавливаемся на уровне «ну вот, фронт есть, бэк вроде тоже, как-нибудь допилю оплаты и выложу». Не допиливаем. Потому что платежи, вебхуки, витрина, SEO, публикации — это уже не «интересный код», а «организационная скука».
Проект как раз про то, чтобы скучное сделать готовым и многоразовым. Мы один раз собираем связку: AI → Django/DRF → ЮKassa → деплой → Web Stories → SEO, а дальше в неё можно подставлять вашу идею — не только Mermaid. Mermaid здесь как манекен: на нём удобно показывать, куда вешать оплату, куда прикручивать экспорт, где пускать трафик.
Если у вас в голове крутится мысль «я бы запустил свою фичу, если бы была готовая дорожка к деньгам» — это она.
1. Философия проекта: почему микро-SaaS и при чём здесь ваша идея
Мы не делаем “проект про Mermaid”. Мы делаем каркас для монетизации вашей идеи. Mermaid выбран, потому что он наглядный: написал текст → получил диаграмму → экспортнул SVG → взял деньги. На его месте может быть «генератор учебных планов», «сборщик контент-постов», «AI-подсказки для разработчиков» — механизм тот же.
Что ломаем:
путь «сначала идеальный UI, потом когда-нибудь монетизация»;
привычку откладывать платежи «на следующую версию»;
бесконечный выбор стека.
Что даём вместо:
Короткий цикл: идея → минимальный интерфейс → платёж → доработка по данным.
Единый вход: Next.js, который можно показать и на GH Pages, и в проде.
Нормальный бэкенд: Django/DRF, который не стыдно развивать.
Реальные платежи: ЮKassa с вебхуками и подписками.
Трафик: Google Web Stories + SEO, чтобы не жить в пустоте.
Формула простая: рулит идея, а не деньги. Деньги всего лишь подтверждают, что идея кому-то нужна. Поэтому вся дальнейшая техника в статье — это обслуживание одной задачи: сделать так, чтобы вашу идею можно было не только показать, но и купить.
2. Архитектура: как всё устроено под капотом
Технический стек и почему именно он:
Frontend: Next.js (SSR, статика, Web Stories)
Backend: Django/DRF (быстрое прототипирование API)
Платежи: ЮKassa (вебхуки, подписки, работа с р/ф)
Хостинг: Render (бэкенд/фронтенд) + GitHub Pages (SEO-витрина)
Вот как выглядит общая архитектура (диаграмма сгенерирована нашим же проектом):
graph TD
A[Пользователь] --> B[Web Stories Витрина]
B --> C[Next.js Фронтенд]
C --> D[Django API]
D --> E[LLM Модели]
D --> F[ЮKassa]
F --> G[Вебхуки]
G --> D
D --> H[База данных]
C --> I[Статика GitHub Pages]

3. Аутентификация: NextAuth + Django JWT
Одна из сложных задач — сделать бесшовную аутентификацию между Next.js и Django. Решение: NextAuth для фронтенда и кастомные JWT-токены для бэкенда.
Ключевые компоненты:
# Django - кастомный вход по email
class CustomLoginView(APIView):
def post(self, request):
email = request.data.get('email')
password = request.data.get('password')
user = authenticate(request, email=email, password=password)
if user:
refresh = RefreshToken.for_user(user)
return Response({
'access': str(refresh.access_token),
'refresh': str(refresh),
'user': UserSerializer(user).data
})
// Next.js - перехватчик запросов с авто-обновлением токена
apiClient.interceptors.request.use(async config => {
const session = await getSession();
if (session?.accessToken && isTokenExpired(session.accessToken)) {
try {
const { data } = await axios.post(
`${baseURL}/api/auth/refresh/`,
{ refresh: session.refreshToken }
);
session.accessToken = data.accessToken;
session.refreshToken = data.refreshToken;
} catch {
handleSignOut();
throw new axios.Cancel('Сессия истекла');
}
}
if (session?.accessToken) {
config.headers["Authorization"] = `Bearer ${session.accessToken}`;
}
return config;
});
4. LLM-агрегатор: умный пул моделей
Вместо привязки к одному провайдеру AI, я сделал агрегатор, который работает с десятками моделей через OpenRouter. Это даёт гибкость и отказоустойчивость.
sequenceDiagram
participant User
participant Frontend
participant Django
participant OpenRouter
participant Cache
User->>Frontend: Отправляет запрос
Frontend->>Django: POST /api/chat/questions/
Django->>Cache: Проверяет кэш
alt В кэше есть ответ
Cache-->>Django: Возвращает кэшированный результат
else Нет в кэше
Django->>OpenRouter: Запрос к AI-модели
OpenRouter-->>Django: Ответ
Django->>Cache: Сохраняет в кэш (1 час)
end
Django-->>Frontend: Ответ
Frontend-->>User: Показывает результат

Код селектора моделей:
def get_top_models() -> dict:
"""Берём последние free-модели по каждому бренду"""
models = fetch_models()
free_models = [m for m in models if is_model_free(m)]
brand_groups = defaultdict(list)
for m in free_models:
mid = m["id"]
brand = mid.split("/")[0].lower()
brand_groups[brand].append(mid)
# Берём ТОП-10 брендов по числу моделей
top = sorted(brand_groups.items(), key=lambda x: len(x[1]), reverse=True)[:10]
# Для каждого бренда выбираем ПОСЛЕДНЮЮ free-модель
def pick_latest(ids: list[str]) -> str:
for mid in reversed(ids):
low = mid.lower()
if not any(h in low for h in BAD_HINTS):
return mid
return ids[-1]
split_idx = max(1, len(top) // 2)
code_brands = top[:split_idx]
text_brands = top[split_idx:]
return {
"code_models": [{"brand": b, "model_id": pick_latest(ids)} for b, ids in code_brands],
"text_models": [{"brand": b, "model_id": pick_latest(ids)} for b, ids in text_brands],
}
5. Генератор Mermaid: текст → диаграмма → SVG
Сердце проекта — превращение текстового описания в красивые диаграммы с помощью AI.

Код генерации:
@api_view(["POST"])
@permission_classes([IsAuthenticated])
def generate_mermaid(request):
text = (request.data.get("text") or "").strip()
prefer_type = (request.data.get("type") or "").strip()
lang = request.data.get("language") or "ru"
model_id = request.data.get("model_id")
if not text:
return Response({"error": "empty text"}, status=400)
try:
t, code, warnings, used_model = generate(text, prefer_type, lang, model_id)
# Валидация, что AI вернул валидный Mermaid-код
if not code.strip().lower().startswith(MERMAID_HEADS):
return Response(
{"error": "OpenRouter сейчас недоступен. Попробуйте позже."},
status=503
)
return Response({
"type": t,
"code": code,
"warnings": warnings,
"used_model": used_model
}, status=200)
except RuntimeError:
return Response(
{"error": "OpenRouter сейчас недоступен. Попробуйте позже."},
status=503
)
Корректировка диаграмм AI:
@api_view(["POST"])
@permission_classes([IsAuthenticated])
def adjust_mermaid(request):
"""
Итерируем код по инструкции пользователя
"""
code = (request.data.get("code") or "").strip()
t = request.data.get("type") or "flowchart"
instr = (request.data.get("instruction") or "").strip()
lang = "ru"
model = request.data.get("model_id")
system = f"""
Ты модифицируешь Mermaid {t} код.
Верни ТОЛЬКО код Mermaid в тройных кавычках, без пояснений.
Комментарии внутри кода — только через '%%'
""".strip()
user = f"Инструкция:\n{instr}\n\nТекущий код:\n```mermaid\n{code}\n```"
out, used = query_openrouter(
prompt=user,
model_id=model,
language=lang,
system_prompt=system,
temperature=0.2
)
fixed = extract_fenced(out)
fixed = sanitize_mermaid(out)
fixed = normalize_brand_names(fixed)
if looks_like_mermaid(fixed):
return Response({
"type": t,
"code": fixed,
"used_model": used,
"warnings": []
})
6. Платежи ЮKassa: от первой оплаты до подписок
Самая ответственная часть — бесперебойная работа платежей с защитой от дублей и корректной обработкой вебхуков.
Интерфейс оплаты:

Защита от дублей:
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def process_kassa(request):
subscription_type = (request.data.get('subscription_type') or '').lower()
coupon_code = (request.data.get('coupon_code') or '').strip()
# 1) Блок повторной покупки
if subscription_type in ('monthly', 'yearly'):
existing = Subscription.objects.filter(
user=request.user,
plan=subscription_type,
status='active',
next_charge_at__gt=timezone.now(),
).first()
if existing:
return Response({
"error": "У вас уже есть активная подписка этого типа.",
"plan": existing.plan,
"next_charge_at": existing.next_charge_at.isoformat(),
}, status=status.HTTP_409_CONFLICT)
# 2) Анти-даблклик (недавний запуск того же типа)
if recent_inflight_payment_exists(request.user, subscription_type, window_seconds=90):
return Response(
{"error": "Платёж уже создаётся, подождите пару секунд"},
status=status.HTTP_429_TOO_MANY_REQUESTS
)
Обработка вебхуков:
@csrf_exempt
@api_view(['POST'])
@permission_classes([AllowAny])
def kassa_webhook(request):
# IP-проверка Яндекс-серверов
if not is_valid_webhook_signature(request):
return Response(status=403)
data = json.loads(request.body.decode('utf-8'))
event = (data.get('event') or '').strip()
obj = data.get('object', {}) or {}
# Логируем все события
log = PaymentEventLog.objects.create(
event_id=obj.get('id'),
event_type=event,
payload=data,
applied=False,
)
# Маршрутизация событий
if event == 'payment.waiting_for_capture':
resp = webhook_waiting_for_capture(data)
elif event == 'payment.succeeded':
resp = webhook_succeeded(data)
elif event == 'payment.canceled':
resp = webhook_canceled(data)
elif event == 'refund.succeeded':
resp = webhook_refund(data)
else:
return Response(status=200) # Неизвестные события подтверждаем
# Отмечаем успешно обработанные события
if resp.status_code == 200:
log.applied = True
log.save()
return resp
Автосписания через GitHub Actions:
name: charge-subscriptions (daily)
on:
workflow_dispatch: {}
schedule:
- cron: "0 15 * * *" # Ежедневно в 18:00 Мск
jobs:
run-charges:
runs-on: ubuntu-latest
env:
BACKEND_URL: ${{ secrets.BACKEND_URL }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
steps:
- name: Charge subscriptions
run: |
curl -sS \
-H "X-CRON-SECRET: $CRON_SECRET" \
-d "{\"limit\": 100}" \
"$BACKEND_URL/api/payment/charge-subscriptions/"
7. Web Stories: SEO-витрина как точка входа
Чтобы проект не остался в вакууме, сделал AMP Web Stories витрину, которая отлично индексируется и даёт мобильный трафик.
Витрина проектов:

Структура AMP Story:
<amp-story standalone
title="A · Chat & Aggregator"
publisher="lemon1964"
poster-portrait-src="./assets/c3-chat-hero-1080x1920.webp">
<amp-story-page id="cover">
<amp-story-grid-layer template="fill">
<amp-img src="./assets/c3-chat-hero-1080x1920.webp"
width="1080" height="1920" layout="fill" alt="Chat hero">
</amp-img>
</amp-story-grid-layer>
<amp-story-grid-layer template="vertical" class="layer">
<h1 class="title"><span class="pill">LLM-чат как агрегатор</span></h1>
<p class="text"><span class="pill">Все модели в одном окне. Текст и голос.</span></p>
</amp-story-grid-layer>
</amp-story-page>
<amp-story-cta-layer>
<a href="https://lemon1964.github.io/ai-chat-pages/?utm_source=webstories"
target="_top">Открыть чат</a>
</amp-story-cta-layer>
</amp-story>
8. Деплой и инфраструктура
Frontend/Backend: Render.com
Витрина: GitHub Pages
CI/CD: GitHub Actions
База: PostgreSQL на Render
Roadmap разработки:

9. Что получилось в итоге
Живые ссылки:
Технические итоги:
? 7 модулей готового микро-SaaS
? Работающие платежи с подписками
? AI-агрегатор с 20+ моделями
? Mermaid-генератор как пример монетизации
? SEO-витрина с Web Stories
☁️ Полностью развёрнутый продакшен
10. Заключение
Mermaid в этом проекте — лишь пример. Рулит идея, а не деньги, вторые лишь ресурс для жизни первого'. Этот каркас — ваш конструктор. Замените генератор диаграмм на свою AI-логику: персональный ассистент, аналитика текстов, генератор документов...
Ваш первый платеж не должен быть целью — он должен стать следствием реализации стоящей идеи. Проект даёт вам инструмент, чтобы проверить это на практике."
Что дальше:
Можете взять идеи из статьи и повторить архитектуру
Сходить на Stepik и получить все паттерны «под ключ»
Главное — подставить свою логику вместо Mermaid и запустить
P.S. Если есть вопросы по реализации — спрашивайте в комментариях. Расскажу про подводные камни, которые встретил на пути.