Большинство наших «проектов мечты» умирают не потому, что идея плохая, а потому что мы останавливаемся на уровне «ну вот, фронт есть, бэк вроде тоже, как-нибудь допилю оплаты и выложу». Не допиливаем. Потому что платежи, вебхуки, витрина, 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. Если есть вопросы по реализации — спрашивайте в комментариях. Расскажу про подводные камни, которые встретил на пути.
