Drizzle vs Prisma con D1 e SQLite: cosa cambia quando il runtime è un Worker

Su un Worker con D1 l'ORM non è una scelta di gusto: chi scrive e applica le migrazioni, cosa succede alle transazioni, cosa pesa sullo startup. Drizzle e Prisma 7 a confronto, con le docs ufficiali.

Francesco Pio Nocerino8 minCloudflare
Drizzle vs Prisma con D1 e SQLite: cosa cambia quando il runtime è un Worker
Indice

In breve: Su un Worker con D1 la scelta tra Drizzle e Prisma non è una questione di gusto. Decidono tre cose: chi scrive e chi applica le migrazioni, cosa succede quando ti serve atomicità e quanto pesa il client sullo startup del Worker. Entrambi parlano con D1 dal binding env.DB. Drizzle espone il batch() di D1, che è una transazione. Prisma su D1 ignora le transazioni e genera le migrazioni con prisma migrate diff. Per un Worker nuovo solo su D1 sceglierei Drizzle. Per un team che ha già Prisma altrove, Prisma 7 con l'adapter D1 regge, se conosci i suoi limiti.

Nessun benchmark mio in questo pezzo: niente dimensioni di bundle o tempi di avvio misurati da me. Uso solo documentazione ufficiale di Drizzle, Prisma e Cloudflare, letta il 16 ottobre 2026, e le versioni pubblicate su npm quel giorno. Se D1 è nuovo per te, il contesto è in Workers + D1 + R2 e in SQLite vs Postgres per un prodotto indie.

Due definizioni, per allinearci. Drizzle ORM è un query builder TypeScript: lo schema è codice, e le query diventano SQL senza un client generato. Prisma ORM è un ORM con uno schema dichiarativo in un file .prisma, da cui prisma generate produce un client tipizzato.

La domanda sbagliata e quella giusta

Non "quale ORM ha l'API più comoda?". È: chi è il proprietario dello schema quando il database è D1, e cosa ti dà l'ORM quando due scritture devono andare insieme o non andare affatto.

D1 non è un Postgres con un altro nome. Ha un suo sistema di migrazioni gestito da Wrangler. Lavora in auto-commit. Il suo strumento per l'atomicità è batch(). L'ORM che scegli o si adatta a questi tre fatti, o te li nasconde. Il secondo caso è quello che costa.

Tabella rapida

Criterio

Drizzle ORM

Prisma ORM 7

Connessione a D1

drizzle(env.DB) sul binding

new PrismaD1(env.DB) con @prisma/adapter-d1

Schema

File TypeScript

Prisma Schema Language + prisma generate

Migrazioni

drizzle-kit generate scrive l'SQL; applichi con Wrangler o drizzle-kit migrate

prisma migrate diff scrive l'SQL; applichi con Wrangler

Atomicità su D1

db.batch() sopra il batch() di D1

Transazioni ignorate: query singole

Stato su Workers

D1 supportato, docs con v1 in RC

Workers in Preview nella tabella edge di Prisma

Cosa finisce nel Worker

Query builder in TypeScript, nessun motore separato

Client generato, Rust-free dalla v7

Verdict di riga: non vince l'ORM più famoso. Vince quello che non ti fa scoprire in produzione che una "transazione" non lo era.

Il binding: entrambi parlano con env.DB

Qui le due strade si somigliano. Nessuna stringa di connessione, nessun segreto: il Worker riceve il database come binding, dichiarato in wrangler.jsonc.

Con Drizzle crei l'istanza dentro l'handler e interroghi (docs Drizzle D1):

import { drizzle } from "drizzle-orm/d1";
import { users } from "./schema";

export default {
  async fetch(request: Request, env: Env) {
    const db = drizzle(env.DB);
    const rows = await db.select().from(users).all();
    return Response.json(rows);
  },
};

Con Prisma 7 serve il generator con runtime = "cloudflare", un output esplicito e l'adapter (guida Prisma D1):

import { PrismaClient } from "./generated/prisma/client";
import { PrismaD1 } from "@prisma/adapter-d1";

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const prisma = new PrismaClient({ adapter: new PrismaD1(env.DB) });
    const users = await prisma.user.findMany();
    ctx.waitUntil(prisma.$disconnect());
    return Response.json(users);
  },
};

La guida Prisma chiama $disconnect() in modo esplicito per liberare le risorse, altrimenti il Worker può finire la memoria. È una riga, ma va ricordata. Per i tipi di Env puoi generare il file con wrangler types in tutti e due i casi.

Una nota che vale per entrambi: l'API D1 converte i booleani in INTEGER e oggi non supporta i BigInt. Se il tuo schema usa BigInt, verifica cosa arriva davvero al binding prima di andare in produzione.

Migrazioni: chi scrive l'SQL e chi lo applica

D1 ha un sistema suo. I file .sql stanno in migrations/, e Wrangler registra quelli applicati nella tabella d1_migrations (docs D1 migrations). Cartella e nome della tabella si cambiano nel binding.

Drizzle genera l'SQL dallo schema TypeScript con drizzle-kit generate (fondamenti delle migrazioni Drizzle). Poi hai due strade:

  • applicarle con wrangler d1 migrations apply, puntando migrations_dir alla cartella di Drizzle;
  • applicarle con drizzle-kit migrate via HTTP API di D1, con il driver d1-http e un token con permessi di modifica su D1 (guida d1-http).

Il layout recente di Drizzle mette ogni migrazione in una sottocartella con il suo migration.sql. Wrangler di default cerca solo file .sql al primo livello. Dal 29 maggio 2026 c'è migrations_pattern, pensato proprio per questi layout annidati (changelog Cloudflare):

{
  "d1_databases": [{
    "binding": "DB",
    "database_name": "my-database",
    "database_id": "<UUID>",
    "migrations_dir": "migrations",
    "migrations_pattern": "migrations/*/migration.sql"
  }]
}

Attenzione a un dettaglio: drizzle-kit migrate tiene il suo registro in __drizzle_migrations, Wrangler in d1_migrations. Sono due registri diversi. Sceglierne uno e non mescolarli sullo stesso database è la cosa più sana.

Prisma su D1 non usa prisma migrate dev. Il flusso documentato per la v7 ha tre passi (pagina Prisma su D1). Crei il file con wrangler d1 migrations create. Lo riempi con prisma migrate diff. Lo applichi con wrangler d1 migrations apply:

npx wrangler d1 migrations create my-database add_post
npx prisma migrate diff \
  --from-local-d1 \
  --to-schema ./prisma/schema.prisma \
  --script \
  --output migrations/0002_add_post.sql
npx wrangler d1 migrations apply my-database --local
npx wrangler d1 migrations apply my-database --remote

Il diff parte dal D1 locale. Se il locale non riflette il remoto, l'SQL generato descrive il salto sbagliato. Applica sempre prima in locale, e rileggi il file prima del --remote.

La guida di deploy Prisma applica l'SQL con wrangler d1 execute --file. Funziona, ma non registra nulla in d1_migrations. In produzione conviene migrations apply: sai cosa è stato applicato e cosa no.

In entrambi i casi la regola è la stessa: Wrangler resta l'unico che tocca lo schema remoto. Prima di una migrazione su dati veri, segna il punto di ripristino: come funziona Time Travel l'ho scritto in Backup di D1.

Transazioni: la differenza che morde

D1 lavora in auto-commit. Il suo batch() invia più statement in una sola chiamata, eseguiti in sequenza. La documentazione Cloudflare è esplicita: i batch sono transazioni SQL. Se uno statement fallisce, la sequenza viene annullata.

Drizzle espone proprio questo. Il suo db.batch() funziona con D1 e accetta insert, update, delete e select tipizzati (Batch API per SQLite):

const [insertedOrders] = await db.batch([
  db.insert(orders).values({ userId, total }).returning(),
  db.update(credits)
    .set({ balance: sql`${credits.balance} - ${total}` })
    .where(eq(credits.userId, userId)),
]);

Prisma su D1 ha un limite documentato e serio. Le transazioni implicite ed esplicite vengono ignorate ed eseguite come query singole. La pagina Prisma lo dice chiaramente: si perdono le garanzie ACID. Un $transaction che su Postgres protegge un ordine, su D1 non protegge niente.

Non è un motivo per scartare Prisma. È un motivo per cercare ogni $transaction nel codice che gira su D1. Per quei percorsi puoi scendere al binding e usare env.DB.batch() con SQL preparato. Oppure progettare le scritture perché siano idempotenti.

Bundle e startup: il limite si è spostato

Il timore classico, su Workers, è la dimensione del bundle. La pagina deploy di Prisma cita ancora 3 MB sul piano Free. La pagina dei limiti di Workers, aggiornata il 12 ottobre 2026, dice altro: 64 MiB non compressi su Free e Paid, nessun limite sul compresso.

Il vincolo che conta adesso è lo startup: il Worker deve eseguire lo scope globale entro 1 secondo. Bundle grandi e inizializzazioni costose fuori dall'handler lo allungano. Cloudflare indica come misurarlo: wrangler deploy --outdir bundled/ --dry-run per la dimensione, startup_time_ms nell'output di deploy per l'avvio.

Lato Prisma, la v7 ha reso predefinito il client senza motore Rust. Il changelog Prisma dichiara bundle "~90% più piccoli": è un numero del vendor, non mio. Lato Drizzle, l'architettura è più sottile per costruzione: costruisce SQL in TypeScript e lo passa al binding, senza un client generato.

Non ti do una cifra per nessuno dei due. Dipende dal tuo schema e dalle tue dipendenze. Misura il tuo Worker con e senza ORM: è un comando.

SQLite fuori da D1

Il titolo dice anche SQLite, e conta. Spesso lo stesso schema deve girare in test locali, in un Node, o in un Durable Object.

  • Drizzle ha connettori per SQLite su Node e Bun, per Turso e anche per lo SQLite dei Durable Objects. Stesso dialetto, driver diverso.
  • Prisma 7 usa il provider sqlite con adapter diversi: better-sqlite3 in locale, libSQL per Turso, D1 sui Workers (connettore SQLite di Prisma). Su un file SQLite locale prisma migrate dev funziona; su D1 no.

Se la tua app vive tra un Worker e un servizio Node sullo stesso schema SQLite, entrambi reggono: cambia l'adapter con Prisma, il driver con Drizzle. Se vive tutta su Cloudflare, Drizzle copre D1 e Durable Objects con lo stesso modello mentale.

Versioni: fissale

Al 16 ottobre 2026 il registro npm dice questo. drizzle-orm ha come latest la 0.45.4, mentre la pagina Drizzle su D1 fa installare @rc, cioè la 1.0 in release candidate. Per Prisma, il tag latest della CLI prisma punta a una 8.0 RC, mentre @prisma/client e @prisma/adapter-d1 sono alla 7.10.1. Infatti la guida D1 usa prisma@prev.

Tradotto: non scrivere npm i prisma o npm i drizzle-orm@rc alla cieca. Fissa le versioni esatte, tieni CLI e client allineati, e leggi le release (Drizzle, Prisma) prima di aggiornare. Se i comandi Wrangler ti arrivano da un agente, ricorda che anche Wrangler è in transizione verso cf.

Cosa sceglierei e perché, per caso d'uso

Non è un racconto di progetto: è il ragionamento che seguirei oggi, con le docs sopra.

  • Worker nuovo, solo D1, scritture che devono essere atomiche. Drizzle. Hai db.batch() tipizzato, l'SQL generato in vista e lo schema in TypeScript accanto al codice.
  • Team che ha già Prisma su Postgres e porta una parte su Workers e D1. Prisma 7 con @prisma/adapter-d1. Accetti tre cose: migrazioni via migrate diff, niente $transaction affidabile, supporto edge ancora in Preview.
  • Prototipo generato da un agente che arriva con Prisma. Succede spesso: è lo stack di default che l'AI propone. Non riscriverlo per principio. Cerca prima le transazioni e decidi chi applica le migrazioni.
  • App che vive tutta su Cloudflare, con Durable Objects. Drizzle, per lo stesso modello su D1 e sullo SQLite dei Durable Objects.

Se invece D1 ti sta stretto per concorrenza in scrittura o per i dati, il problema non è l'ORM. Torna a SQLite vs Postgres e al costo di uscita in Vercel vs Cloudflare vs VPS.

Cosa conviene fare adesso

  • Cerca $transaction e db.transaction nel codice che gira su D1. Ogni occorrenza va verificata o sostituita con un batch.
  • Scegli un solo strumento che applica le migrazioni in remoto. Di solito Wrangler, con migrations_pattern se usi il layout di Drizzle.
  • Misura dimensione e startup del Worker con il dry-run, prima e dopo aver aggiunto l'ORM.
  • Fissa le versioni esatte di ORM, CLI e adapter nel package.json.
  • Prima di una migrazione remota, segna il punto di ripristino di Time Travel.
  • Se hai appena spostato il sito su Workers, ricontrolla i binding in wrangler.jsonc: ne ho parlato nella migrazione da Pages a Workers. Altri pezzi sullo stack sono nell'hub Vibe coding.
[ DOMANDE FREQUENTI ]

Domande frequenti

01Prisma funziona su Cloudflare Workers con D1?

Sì. Usi il provider sqlite, il generator con runtime = "cloudflare" e @prisma/adapter-d1, passando env.DB a PrismaD1. Nella tabella edge di Prisma il supporto a Workers è ancora indicato come Preview.

02Posso usare prisma migrate dev con D1?

No. Per D1 Prisma documenta un altro flusso: generi l'SQL con prisma migrate diff e lo applichi con wrangler d1 migrations apply, prima in locale e poi in remoto.

03Come faccio una scrittura atomica con Drizzle su D1?

Con db.batch(). Drizzle lo appoggia sul batch() di D1, che Cloudflare documenta come transazione SQL: se uno statement fallisce, la sequenza viene annullata.

04Il limite di 3 MB per i Worker vale ancora?

Secondo la pagina dei limiti di Cloudflare aggiornata al 12 ottobre 2026, no: il limite è 64 MiB non compressi su Free e Paid. Conta però lo startup, che deve restare sotto 1 secondo.

05Drizzle o Prisma per un progetto nuovo su D1?

Per un Worker nuovo che usa solo D1, Drizzle: batch atomici, SQL in vista, nessun client generato. Prisma ha senso se il team lo usa già su altri database e accetta i limiti su transazioni e migrazioni.

Condividi