// бэкенд
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 до записи, заодно решает, что вообще считается валидным постом во всём сервисе.
Есть похожая задача?
Расскажите — вернусь с оценкой в тот же день.


