ntanduy/cloudflare-d1-database
Use Cloudflare D1 as a native Laravel database driver with full Eloquent ORM, Query Builder, and migration support. Choose REST API or Worker connections, batch queries, bulk inserts, read replication, import and schema dump commands, time travel, retries, and circuit breaking. Requires PHP 8.2+ and...
Bottom line: this is a credible, well-engineered adapter that makes Cloudflare D1 usable from Laravel, but it is not a drop-in replacement for MySQL/Postgres. Adopt it only for bounded, read-heavy, low-write domains where you can tolerate the loss of transactions, and only after you have accepted long-term fork ownership.
Architecture fit. The package integrates at the correct Laravel seam: D1ServiceProvider merges config/d1-database.php into database.connections.d1, so Eloquent, the Query Builder, the migration runner, and seeders all work through Laravel's normal connection manager. D1Connection extends SQLiteConnection and D1SchemaGrammar extends the SQLite grammar, which is the right call for dialect fidelity — D1 is SQLite. Architecturally, though, it inverts a core Laravel assumption: every query is an HTTP round-trip to Cloudflare, not a stateful socket to a database. Anything that depends on connection-scoped state or multi-statement session semantics is either reimplemented or silently absent.
Integration feasibility. Installation is genuinely trivial — composer require, publish config, three env vars, php artisan d1:health. Two drivers: REST (zero infrastructure, extra HTTP hop) and Worker (deploy the included Wrangler project, get lower latency, D1 Sessions, and automatic read/write splitting). A competent Laravel team can have a REST proof-of-concept running in an afternoon. The Worker path is a half-day of additional platform work (wrangler secrets, D1 binding, HMAC configuration, deploy pipeline).
Technical risk. Ranked:
beginTransaction(), commit(), rollBack(), Laravel savepoints, and rollback SQL all silently do nothing. This is the dominant risk. DB::transaction(), lockForUpdate(), and any implicitly transactional ORM path will appear to work while providing zero atomicity or isolation. Silent correctness failures under concurrency are far more expensive than loud ones. Atomicity is only available via D1Connection::batch() (D1's native batch, capped at 100 statements).SELECT/WITH. That is the right discipline — blind write retries would duplicate data — but it means write failures surface as hard errors and your application must own idempotency. Any middleware or queue retry layer that assumes the DB driver handles this is wrong.D1Pdo extends PDO and opens an unused sqlite::memory: handle purely to satisfy type constraints. Anything that type-hints PDO, calls lastInsertId(), or introspects statements may behave unexpectedly, and future PHP PDO changes are a latent break.D1SchemaGrammar uses reflection to match Laravel 12+ schema method signatures. That is pragmatic but brittle; minor Laravel releases can break migrations in ways your tests may not catch.Key questions to answer before commitment:
d1:schema-dump, d1:time-travel, and full d1:info metadata always need REST credentials, so Worker deployments still carry a second secret set.SELECT 1)?Stack fit. Requires PHP ≥ 8.2 and Laravel 10–13. Their CI matrix runs PHP 8.2/8.3/8.4 against Laravel 10/11/12/13 on both prefer-lowest and prefer-stable (Laravel 13 excluded on PHP 8.2) — that is a serious, above-average compatibility posture for a 30-star package. If you are on PHP 8.1 or Laravel ≤9, this is a non-starter without a fork.
Migration path. Do not plan a DB_CONNECTION=d1 cutover for an existing application. Instead:
$connection = 'd1', and use DB::connection('d1') for targeted read paths.DB_CONNECTION=d1 for greenfield services whose data model you are designing around D1 from day one.Compatibility caveats to budget for. You inherit the SQLite dialect, not your current one. Expect rework around: ENUM/SET columns (no native support), ALTER TABLE limitations (SQLite's constrained column-drop/alter semantics), native JSON functions, case-insensitive collation defaults, full-text search, stored procedures, and MySQL-specific raw SQL. Foreign-key enforcement in SQLite is a per-connection PRAGMA — over a stateless HTTP transport this does not persist between requests, so FK enforcement is a specific thing to verify with a failing-insert test rather than assume. Auto-increment, DEFAULT expressions, and index syntax differences will surface during migration replays.
Sequencing. Five gates, kill-able at each:
d1:health, replay a meaningful subset of migrations, capture p50/p95 latency for your ten most representative queries — including an Eloquent collection with eager loading.DB::transaction, lockForUpdate, sharedLock, savepoint, raw SQL, and any package that assumes transactional writes. Every hit is a blocker for that domain.N+1 discipline is mandatory, not optional. With a REST driver, an N+1 becomes N HTTP round-trips plus N billed row-reads. Enforce Model::preventLazyLoading() in non-production and audit every list endpoint before pilot.
Maintenance. Assume you will fork. Pin an exact version, commit the Worker/ directory into your own repo, and treat upstream as a source of patches rather than a dependency you trust to stay current. The AGENTS.md files are unusually good — they document the no-op transactions, the retry-safety boundary, the batch-limit chunking, and the circuit-breaker cache constraint — which raises confidence in the maintainer's judgment even where it lowers confidence in the platform fit. Budget roughly 10–15% of one engineer's time for tracking upstream, re-running the PHP and Worker test suites, and re-validating on each Laravel minor.
Support. No SLA, no vendor escalation, GitHub issues only. You sit between two support boundaries: Cloudflare supports D1 and Workers but not this driver; the maintainer supports the driver but not your deployment. Anything that breaks in the seam — an auth change in the D1 REST API, a Wrangler breaking change, a Laravel schema-grammar shift — is yours to diagnose. Document a runbook with d1:health as the first triage step.
Scaling. Reads are the story: with the Worker driver, Sessions plus automatic read/write splitting route SELECTs to nearby replicas and track bookmarks for sequential consistency. Writes are single-primary and D1 is not a high-write-throughput system. Hard ceilings to design around: 100 statements per batch (so bulkInsert() chunks, and earlier chunks stay committed if a later chunk fails — that is a partial-write failure mode requiring reconciliation logic), per-query size limits, and per-database storage limits. Each query is an HTTP request subject to Cloudflare concurrency limits; the retry layer absorbs 429s but converts them into latency. Cost is per rows-read/rows-written — an N+1 regression is a billing incident as much as a latency one, so wire the query logger into your metrics and alert on rows-read per request.
Failure modes to instrument:
bulkInsert above 100 rows — needs a reconciliation job, not just an alert.database cache driver (documented dependency loop), and if your cache tier is down the breaker degrades with it. The breaker is also per-application-instance, not global — it will not coordinate across your fleet.Ramp-up. Non-trivial. Engineers need to internalize three things that violate Laravel instincts: transactions do not exist (use batch()), atomicity is capped at 100 statements and drops to best-effort above that, and which driver you chose silently determines whether Sessions and read splitting exist. Plan one to two sprints of ramp for a team already fluent in Laravel, plus a short Worker/wrangler primer for whoever owns the platform side. The failure mode of skipping this is predictable: an engineer writes DB::transaction() in month two, it passes code review and functional tests, and it corrupts data under load in month six.
How can I help you explore Laravel packages today?