← к блогу

// бэкенд

SQLite на проде — взрослое решение

Обложка статьи: «SQLite на проде»

Когда я собирал контент-сервис для блога, который кормит сразу два моих сайта, вопрос «какую базу» занял у меня секунд тридцать. И я почти на автомате потянулся за Postgres. Потому что так делают все. Потому что это безопасный ответ, за который тебя никто не упрекнёт на ревью.

А потом я остановился и честно посмотрел на нагрузку. И понял, что чуть не решил проблему, которой у меня нет. Тезис простой: бери самый скучный инструмент, который закрывает твою реальную форму нагрузки. У меня это SQLite через Turso — Postgres сюда не подошёл. Со стороны похоже на лень, на деле это зрелость.

Что у меня за нагрузка на самом деле

Десятки постов. Записей — пара в неделю, когда я что-то публикую. Чтений — на порядки больше. На «приложение с базой» это не тянет — это книжка, которую изредка дописывают.

Read-heavy — это когда чтений несоизмеримо больше, чем записей. Tiny dataset — когда вся таблица влезает в один экран, а весь контент по объёму меньше, чем одна аватарка пользователя в нормальном продукте. У меня сходятся оба свойства, причём в крайней точке.

Больше того: сайты вообще не читают базу на каждый запрос пользователя. Они забирают контент один раз, на этапе сборки. Prebuild-скрипт делает один fetch, пишет снапшот в JSON, и дальше статика живёт сама по себе. Это нужно для SSG и превью-ботов: Телеграм и соцсети не исполняют JS, им надо отдать готовый HTML с правильными OG-тегами — пустая SPA-оболочка для них бесполезна. Следствие для базы одно: на проде к ней ходят только два билда, и то изредка — тысячи посетителей до неё просто не доходят. Проектировать такую систему под конкурентную нагрузку — решать несуществующую проблему.

Сколько стоит ответ «по умолчанию беру Postgres»

Postgres прекрасен. Но он не бесплатен — платишь вниманием. Это отдельный процесс, который надо где-то держать, обновлять, бэкапить, мониторить. Это connection pooling, который начинает кусаться, как только появляется serverless или несколько инстансов. Это роли, права, vacuum, версии. Каждый пункт — крошечный. Но сумма — это постоянный налог на внимание, который ты платишь до конца жизни проекта. Для сервиса, где данных меньше мегабайта, этот налог не окупается ничем.

Postgres — это не цена за мощь. Это абонентская плата за мощь, которой ты не пользуешься.

Чем SQLite и Turso проще

SQLite зря считают урезанным Postgres. У него другая модель: база живёт внутри процесса, в одном файле, без отдельного сервера и без сетевого хопа на чтение. Для read-heavy это идеально — чтение становится по сути обращением к локальным данным, без похода по сети к чужому демону. Turso берёт этот файл и делает из него managed-сервис на libSQL: тот же SQLite, но с репликами и удалённым доступом, чтобы к нему можно было ходить из контейнера на VPS. Подключение в Prisma — это буквально драйвер-адаптер с url и токеном, без отдельной инфраструктуры базы рядом.

const adapter = new PrismaLibSQL({
  url: process.env.TURSO_DATABASE_URL,
  authToken: process.env.TURSO_AUTH_TOKEN,
});
const prisma = new PrismaClient({ adapter });
  • Ноль ops: нет процесса базы, который надо администрировать. Меньше движущихся частей — меньше того, что ломается в три ночи.
  • Embedded-модель: чтение идёт мимо сетевого сервера, а edge-реплики позволяют держать данные ближе к месту, где их читают.
  • Бэкап — это файл. Восстановление катастрофы — скопировать его обратно. Никакого pg_dump, никакого ритуала.
  • Локальная разработка — тот же движок: на машине это просто SQLite-файл, без отдельного сервиса в docker-compose только ради того, чтобы поднять проект.

Где Postgres всё-таки нужен — честно

Я не продаю SQLite как серебряную пулю — это ровно та же ошибка, от которой я ушёл, только зеркальная. Есть три места, где я бы развернулся и взял Postgres не задумываясь: реляционная сложность с тяжёлыми джойнами и аналитикой по живым данным; конкурентная запись, где SQLite сериализует писателей и поток одновременных транзакций становится бутылочным горлышком; расширения и типы вроде PostGIS, промышленного полнотекста, JSONB с индексами и pgvector.

Ни одно из этих условий у меня не выполняется. Один писатель — это я сам, нажимающий «опубликовать». Связей — минимум. Запросы — тривиальный срез по сайту и сортировка по дате. Это профиль, под который SQLite спроектирован буквально идеально.

Скучная база — это зрелость

Лень — это не думать и взять то, что под рукой. Иногда под рукой как раз Postgres, потому что «все так делают». Зрелость — это посмотреть на реальную форму нагрузки и взять самый скучный инструмент, который её закрывает, осознанно отказавшись от мощи, которая тебе не нужна.

Я беру фичу целиком — от интерфейса до схемы данных и прода. И именно потому, что отвечаю за весь стек, я не хочу тащить в него лишний демон ради нагрузки в две записи в неделю. Самая дешёвая в эксплуатации часть системы — та, которой нет. Лучший выбор базы — тот, о котором ты больше ни разу не вспомнишь. Мой файл просто лежит и отдаёт контент. Это и есть результат.

P.S. Дальше — про доставку: как контент вообще попадает на сайт без рантайма, почему выбран билд вместо SSR, и при чём тут превью-карточки для ботов.

Есть похожая задача?

Расскажите — вернусь с оценкой в тот же день.

Обсудить проект

// читать дальше

Кадр из клипа про то, почему ИИ-агенты рисуют серые интерфейсы
0:36
// видео

Почему ИИ-агенты рисуют одинаково серые интерфейсы

Обложка статьи: «Claude Design в проде»
// дизайн6 мин

Как использовать Claude Design в проде

Я разработчик, дизайнер из меня никакой. Но сайт, который ты читаешь, выглядит как надо — дизайн я собрал с Claude, руками не рисовал. Показываю весь путь и артефакты, которые реально пошли в работу.

Обложка статьи: «1 блог, 2 сайта, 1 сервис»
// архитектура8 мин

Два сайта — один блог: как я не уронил SEO, сливая контент в один сервис

Слил блоги двух сайтов в один сервис на Hono и Turso. База оказалась простой частью, вся коварность — в доставке: контент должен приезжать на билде, потому что при фетче на клиенте превью-боты ловят пустую страницу.