← к блогу

// бэкенд

Prisma + Turso: адаптеры — это легко, а миграции — боль

Обложка статьи: «Prisma + Turso»

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

Тезис простой: driver adapters в Prisma 6 уже взрослые и подключаются легко. А вот миграции — это боль, и `migrate deploy`, на который все молятся, в проде с Turso просто не запускается.

Driver adapters: GA, про флаг можно забыть

Год назад подключить Turso к Prisma означало воткнуть preview-флаг `driverAdapters` в генератор и собирать всё на честном слове. На Prisma 6.x это уже GA. В схеме генератор — просто `prisma-client-js`, никакого preview-списка. Адаптер ты подключаешь в рантайме, когда создаёшь клиент, — в схеме про него ни слова.

Сам адаптер — пакет `@prisma/adapter-libsql`. Идея в лоб: вместо встроенного движка Prisma запросы исполняет libSQL-клиент, которому ты отдаёшь URL и authToken. Один объект — `new PrismaLibSQL({ url, authToken })` — и Prisma говорит с Turso на его родном протоколе.

const { PrismaLibSQL } = await import('@prisma/adapter-libsql');
const adapter = new PrismaLibSQL({
  url: config.databaseUrl,
  authToken: config.tursoAuthToken,
});
return new PrismaClient({ adapter });

Один switch по env — и весь остальной код про него не знает

Мой любимый трюк, который снял половину боли: адаптер живёт ровно в одном месте — `db.ts` — и включается по окружению. В dev и в тестах база — это обычный файл `file:./prisma/dev.db`, и клиент создаётся как `new PrismaClient()` без всякого адаптера. В проде URL начинается с `libsql:` или есть authToken — тогда подключается Turso. Решает всё один флаг.

export const isTurso =
  /^libsql:/i.test(config.databaseUrl) ||
  /^(https?|wss?):/i.test(config.databaseUrl) ||
  Boolean(config.tursoAuthToken);

Из этого вытекает важная вещь: вся локальная разработка и весь CI идут вообще без аккаунта Turso. Никто не заводит токен, тесты гоняются на изолированном файле, а libSQL-биндинги не грузятся в память. Адаптер я подключаю ленивым `await import()` внутри ветки `isTurso` — пока ты в dev, нативные модули libSQL никто не трогает. Прод-зависимость, которую можно потрогать только в проде, — это не зависимость, а ловушка.

А теперь миграции — тут Prisma делает вид, что глухая

Сценарий, который все держат в голове: `prisma migrate dev` локально пишет миграцию, `prisma migrate deploy` накатывает её в прод. С Turso второй шаг отваливается. `migrate deploy` хочет напрямую открыть соединение с базой по строке подключения, а `libsql://` он напрямую не умеет — это не тот SQLite, который Prisma ждёт увидеть на конце провода. И упираешься ты в это ровно в момент первого деплоя. Неприятно.

Решение, которое реально работает в проде: не давать Prisma самой ходить в Turso за миграциями. Локально я по-прежнему делаю `prisma migrate dev` на файловой базе — там Prisma в своей стихии и честно ведёт историю. А накат на Turso превращаю в чистый SQL и заливаю его родным клиентом Turso.

  • `prisma migrate dev` на локальном `file:`-SQLite — пишет миграцию и держит историю.
  • `prisma migrate diff --from-url "$DATABASE_URL" --to-schema-datamodel prisma/schema.prisma --script > patch.sql` — генерит чистый SQL-патч между базой и схемой.
  • Глазами читаю `patch.sql` — это последняя точка, где можно поймать случайный DROP до прода.
  • `turso db shell ishbnv-blog < patch.sql` — заливаю SQL родным клиентом Turso, минуя `migrate deploy`.

Да, это руками, и history-таблицу миграций на стороне Turso автоматически ты так не ведёшь. Но датасет крошечный, схема меняется редко, а контроль над тем, какой именно SQL уходит в прод, дороже автоматики, которая всё равно спотыкается о `libsql://`. `migrate diff` тут — рабочая лошадь: сравнивает живую базу со схемой и отдаёт ровно дельту.

Грабли, на которые я наступил по дороге

Первое — версии. Адаптер `@prisma/adapter-libsql` и `@prisma/client` обязаны идти одной мажорной линией. Если client на 6.x, а адаптер случайно подтянулся другой — ты ловишь странности в рантайме вместо внятной ошибки: клиент создаётся, но запросы ведут себя не так. Я держу обе зависимости на одной версии и обновляю их парой, никогда по отдельности.

Второе — Json в SQLite. У меня в модели Post тело статьи, теги и audiences — это `Json`. В Postgres это нативный jsonb, а в SQLite, а значит и в Turso, Json физически лежит как TEXT. Prisma сериализует и десериализует его прозрачно, и это удобно — пока не вспомнишь, что на уровне базы это строка. Никаких запросов внутрь JSON, никакой индексации по ключу. Поэтому структуру я валидирую Zod-схемой контент-контракта до записи — база её не проверит, для неё это просто текст. Плюс один и тот же `provider = "sqlite"` обслуживает и файл, и Turso — значит никаких Postgres-only фишек, даже если очень хочется.

Вывод скучный и предсказуемый, как и должно быть в проде. Driver adapters в Prisma 6 уже взрослые — спрячь их за один env-флаг, и весь dev останется на файле без единого токена. А миграции в Turso не доверяй `migrate deploy`: гоняй `migrate diff` в SQL, читай его глазами и заливай через `turso db shell`. Ни разу не уронило прод.

P.S. Готовлю разбор контент-контракта на Zod: почему та самая схема, которой я валидирую Json до записи, заодно решает, что вообще считается валидным постом во всём сервисе.

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

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

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

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

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

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

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

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

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

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

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

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