Немного предыстории
Месяц назад я баловался с сервисом Lovable — мне показался прикольным инструментом, а главное для нашей истории это то, что он предложил мне настроить БД на Supabase с помощью пары кликов: база Postgres уже крутится, таблицы создаются, в админке всё прозрачно и, главное, что все это бесплатно в пределах небольших квот.
Пока щёлкал UI заметил раздел Functions. «Что‑то похожее на AWS Lambda, только прямо рядом с базой» - подумал я. И так в "долгом ящике" появилась идея потыкать в эти функции.
И тут несколько дней назад релиз OpenAI Images API. Мы как-то обсуждали со знакомым разработчиком, что прикольно было бы сделать бота в телеге для ру пользователей, так как их доступ до ChatGPT-plus ограничен. Вспомнил про Supabase → идеальный шанс пощупать edge‑функции подняв простого бота.
Как обычно для себя, накидал мысли по тому, как должен работать бот и на чем, и попросил ChatGPT написать первые черновики ТЗ. Несколько правок и плюс-минус полное описание задачи было на руках. Затем пошёл в Claude через OpenRouter — он выплюнул скелет кода под Supabase edge functions. Имея на руках написанный код я пошел его править локально и запускать постепенно погружаясь в написание функций.
Бот можно потыкать здесь, а если вам интересно, чем занимается аналитик данных в свободное время, то можно еще и на мой канал подписаться
Архитектура решения

Почему именно Supabase
Edge Functions на Deno — супер легкий деплой
supabase functions deploy bot
и готово.Знакомый мне Postgres — из коробки policies + row‑level security.
Есть встроенный Storage S3— если решу кешировать картинки.
Бесплатный план тянет 500 тыс. Edge‑инвокаций / 10 ГБ‑трафика — на хобби‑бот за глаза хватает.
Логическая модель

Таблица | Назначение |
---|---|
| учёт TG‑пользователей |
| подписка на промо‑канал |
| FSM (finite state machine — конечный автомат)‑состояние бота |
| лог картинок |
Таблички создавал через их удобную SQL студию, там же накинул функций для списывания кол-ва генерации
Edge Function bot
URL
https://xyz.supabase.co/functions/v1/bot/<WEBHOOK_SECRET>
— Именно туда Telegram «постучится» методом POST, передавая JSON-объект TelegramUpdate с сообщениями, фото, callback-кнопками и т.д.Проверяем
req.method === 'POST'
и секрет в path.Парсим
TelegramUpdate
, роутим вstates.ts
.
Состояния бота
states.ts
WAITING_PHOTO ─▶ WAITING_PROMPT ─▶ GENERATING ─▶ WAITING_PHOTO
└────────── SUBSCRIPTION_OFFER
FSM (finite state machine) хранится в sessions
— одна строка на пользователя.
Глубже в код
1. database.ts
Создаём Supabase‑клиент:
const supabase = createClient(supabaseUrl, supabaseKey);
и дальше поехали обрабатывать запросы
getUser / createUser / updateUserInteraction
.RPC (Remote Procedure Call) через
supabase.rpc('decrease_generation_count')
— дёргает за функцию DB, чтобы декрементировать счётчик.Проверка подписки: сначала смотрим локальную таблицу, если же пользователя там нет, то стоит еще посмотреть подписку пользователя прямо в телеге через метод
getChatMember
. Если пользователь подписчик моего замечательного канала, то он на этом шаге автоматически прорастет в таблицу с подписками.
2. states.ts — бизнес‑логика
/start
приветствует и создаёт пользователя.handlePhoto
— сохраняетphoto_url
и переводит в WAITING_PROMPT.handleCallbackQuery
— меню стилей + проверка подписки.handlePromptWithPhoto
— главное блюдо: скачиваем фото, вызываемgenerateImageBytes
, шлёмsendPhoto
, минусуем фрипасс.
3. openai.ts - Интеграция с OpenAI
// 1. скачиваем картинку
const res = await fetch(srcUrl);
if (!res.ok) throw new Error("Failed to fetch source image");
const blob = await res.blob();
// 2. определяем расширение и MIME
const ext = srcUrl.match(/\.(png|jpe?g|webp)$/i)?.[1]?.toLowerCase() ?? "png";
const mime = ext.startsWith("jp") ? "image/jpeg" :
ext === "webp" ? "image/webp" : "image/png";
// 3. превращаем Blob в File (openai ждёт File | Buffer)
const file = new File([blob], `source.${ext}`, { type: mime });
// 4. вызываем Images API
const { data } = await openai.images.edit({
model : 'gpt-image-1',
image : file,
prompt,
n : 1,
size : '1024x1024',
quality: 'low',
});
// 5. декодируем base64
const bytes = Uint8Array.from(atob(data[0].b64_json!), c => c.charCodeAt(0));
Что происходит выше
fetch → blob — превращаем URL в бинарные данные, чтобы можно было переслать файл целиком (Images API не принимает внешние URL, ну или я слепой и не нашел).
ext / mime — по имени файла решаем, какой
Content‑Type
передать OpenAI. Изображения допускаются только четырёх форматов: PNG, JPEG/JPG, WEBP.File() — OpenAI SDK в Deno/Node ждёт
File
илиBuffer
; обычныйBlob
не подойдёт.openai.images.edit() — ключевые параметры:
model
: сейчас единственная модель —gpt-image-1
.image
: исходныйFile
.prompt
: текст‑инструкция.n
: количество вариантов (1‑10).size
:'256x256' | '512x512' | '1024x1024'
.quality
:'low' | 'standard' | 'hd'
— влияет на цену и скорость.
base64 → Uint8Array — Telegram API принимает файл как
FormData
, поэтому нужно превратить строку в сырые байты.
На выходе получаем b64_json
, декодируем в Uint8Array
, кидаем обратно в Telegram.
4. Деплой
Тут все просто
supabase functions serve bot --env-file .env --no-verify-jwt # локально (отключаем JWT)
supabase functions deploy bot --no-verify-jwt # прод (бот авторизуется по секрету в URL)
И не забываем зарегистрировать hook:
curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook?url=https://xyz.supabase.co/functions/v1/bot/<WEBHOOK_SECRET>"
Подводные камни
JWT‑проверка
По умолчанию Edge Functions требуют валидный JWT в заголовке Authorization
и отвечают 401 Unauthorized, если его нет. Для веб‑хуков (Stripe, Telegram, Viber) Supabase предлагает опцию отключить проверку:
# локально
supabase functions serve bot --no-verify-jwt
# деплой
supabase functions deploy bot --no-verify-jwt
Используйте эту «дырку» только вместе с собственным секретом в URL (как /<WEBHOOK_SECRET>
), иначе функцию сможет дернуть кто угодно.
Итоги
За пару часов на Supabase я собрал полностью безсерверный Telegram‑бот на Supabase function, и всё заработало без танцев с VPS и Docker.
Качество генерации установил на "low", так как с medium особой разницы не заметил, а вот жрет токенов в 4 раза больше, по этой причине качество генерации очень ОЧЕНЬ страдает
Вот примеры



Можно конечно еще и платные генерации сделать за деньги, но нужны ли они кому нибудь будут я не уверен.
Если кому нужен код, то там ничего интересного, но посмотреть можно здесь