Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Cloudflare D1 Database Laravel Package

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...

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a drop-in Laravel connection driver, so the setup is intentionally boring — which is good. Budget about ten minutes to first working query.

Minimal steps

composer require ntanduy/cloudflare-d1-database
php artisan vendor:publish --tag="d1-config"

Then pick a driver. This is the one real architectural decision, and it drives everything else:

  • REST (default) — you already have an API token; zero infrastructure. Every query is an HTTP round-trip to Cloudflare's API, so it's the right choice for admin panels, scheduled jobs, migrations, and low-traffic apps.
  • Worker — you deploy the bundled Worker/ template with Wrangler and get a co-located proxy. Necessary if you want read replication, sessions, or automatic read/write splitting.

REST env:

CF_D1_API_TOKEN=...
CF_D1_ACCOUNT_ID=...
CF_D1_DATABASE_ID=...
CF_D1_DRIVER=rest
DB_CONNECTION=d1

Worker env:

CF_D1_DRIVER=worker
CF_D1_WORKER_URL=https://your-d1-worker.your-subdomain.workers.dev
CF_D1_WORKER_SECRET=a-strong-shared-secret

The API token comes from the "Edit Cloudflare D1" template in the dashboard; the account ID is in the dashboard sidebar; the database ID is on the D1 database page.

Verify before writing any code. d1:health is the single most valuable command in the package — it checks the URL/secret are set, runs SELECT 1, and reports end-to-end latency. Run it before php artisan migrate --database=d1, not after, so you're not debugging a migration failure that's actually a bad token.

php artisan d1:health
php artisan migrate --database=d1

Where to look first

  1. config/d1-database.php — every knob (driver, retries, circuit breaker, sessions, HMAC) lives here.
  2. src/D1/D1Connection.php — the class your DB::connection('d1') actually returns; batch(), bulkInsert(), withSession(), setQueryLogger() all hang off it.
  3. agents/AGENTS.md — genuinely the best "why is it like this" document. Read the "Driver Gotchas" list before your first PR.

First use case that fits well: a read-heavy content or catalog site where writes are infrequent (admin CRUD, a webhook or two) and you want edge-adjacent data without managing a database server. Define a model, run migrations, and you're done:

$posts = Post::where('published', true)->orderBy('created_at', 'desc')->get();

Models are ordinary Eloquent models — no trait, no base class, no D1Model. That's the point of the package.

Implementation Patterns

Use the named connection, not the default, during rollout. Keeping DB_CONNECTION on your existing database and writing DB::connection('d1') lets you migrate table-by-table and compare results side-by-side. Only flip DB_CONNECTION=d1 once you've exercised the real query shapes your app issues (not just migrations).

Replace transactions with batch(). This is the single biggest code-shape change. Anywhere you'd write DB::transaction(...) around multiple statements, write:

$results = DB::connection('d1')->batch([
    ['sql' => 'UPDATE accounts SET balance = balance - ? WHERE id = ?', 'params' => [100, 1]],
    ['sql' => 'UPDATE accounts SET balance = balance + ? WHERE id = ?', 'params' => [100, 2]],
]);

On the Worker driver this maps to D1's native atomic batch(). Catch D1BatchException — the message includes the failing statement index, which is enough to write a meaningful error log without a debugger.

Use bulkInsert() for seeds and imports. It parameterizes each row (no injection risk) and pushes work through D1 batch instead of one HTTP call per row:

$connection->bulkInsert('users', $rows); // chunks at 100

This is where the package pays for itself on REST: a 5,000-row seed goes from 5,000 round-trips to 50.

Wire the query logger in a service provider, gated by environment. It's the closest thing to a profiler you get with a remote database, and it's per-connection so it costs nothing in production if you don't attach it:

if (! app()->isProduction()) {
    DB::connection('d1')->d1()->setQueryLogger(function ($sql, $params, $ms, $ok, $err) {
        Log::channel('d1')->debug($ok ? 'ok' : 'FAIL', compact('sql', 'params', 'ms', 'err'));
    });
}

The $ms field is the whole point — with the REST driver you're measuring full HTTP round-trip time, so this is your latency budget telemetry.

Sessions are a request-scoped tool, not a global setting. For Worker deployments needing read-your-writes, open a session per logical operation and carry the bookmark forward:

$connection->withSession('first-primary');
$order = Order::create([...]);
$bookmark = $connection->getBookmark(); // persist in session/queue payload
$connection->endSession();

The bookmark is only useful when you pass it to the next request — stash it in the Laravel session, a queue job property, or an HTTP header. Enabling CF_D1_SESSION_ENABLED=true globally without threading bookmarks gives you the latency benefit but not the consistency guarantee you probably wanted.

Treat Artisan commands as your ops toolbox. d1:import for restoring a SQL dump into a fresh database, d1:schema-dump before risky migrations, d1:time-travel for point-in-time recovery, d1:info for metadata and a quick connectivity check. A sane pre-deploy habit: d1:time-travel bookmark → migrate → d1:info.

Test against the mock connector, not the network. The package's own suite uses a MockCloudflareD1Connector backed by in-memory SQLite that intercepts Saloon requests. Mirror that in your app's test suite: bind a fake connector in TestCase::setUp(). tests/worker/ (a separate mock Worker, not the production template) is the pattern to copy for integration tests that need HTTP semantics.

Detect the driver at runtime when behavior differs. $connection->getDriver() / isWorkerDriver() lets you branch in code that needs sessions on Worker but a degraded path on REST, rather than scattering env() calls.

Gotchas and Tips

There are no transactions. beginTransaction()/commit()/rollBack() are no-ops. This is the one that will bite you. Laravel code that looks transactional will silently not roll back. Audit for DB::transaction(), ->transaction(), lockForUpdate(), and savepoints before migrating, and rewrite each one as a batch(). Note that the package's own tests use SQLite which does simulate transactions — so passing tests do not prove your production rollback works.

bulkInsert() is atomic per chunk, not overall. Datasets over 100 rows become multiple D1 batches; if chunk 3 fails, chunks 1–2 are already committed. Design for re-runs: unique constraints, INSERT OR IGNORE semantics, or an idempotency key per row. Don't wrap a bulk insert in a transaction and assume safety.

Automatic retries only cover SELECT and WITH. shouldRetryFor() is deliberately narrow, and the maintainers say not to broaden it without an idempotency design. Practical consequence: a 502 on an UPDATE surfaces as an exception. Handle write failures yourself with an application-level idempotency key rather than expecting the driver to paper over them.

Do not use the database cache store for the circuit breaker. Circuit state lives in Laravel Cache; putting it in D1 creates a dependency loop — D1 goes down, so the circuit breaker can't read its own state, so it can't open. Redis, file, or array only.

Worker users still need REST credentials. d1:schema-dump, d1:time-travel, and full d1:info metadata all go through the REST API. If you set only CF_D1_WORKER_URL/SECRET in production you'll find your backup command broken exactly when you need it. Keep CF_D1_API_TOKEN, CF_D1_ACCOUNT_ID, and CF_D1_DATABASE_ID in the env regardless of driver.

Sessions and read/write splitting are Worker-only, full stop. This is a Cloudflare platform limitation, not a package gap. The REST driver silently ignores session, read, and write hints — so code that "works" in staging on Worker may lose consistency guarantees in a REST environment without erroring.

Latency per query is the real cost model. Every query is an HTTP hop. N+1 relationships that were "fine-ish" on a local Postgres become 50 sequential HTTP calls. Eager-load aggressively, add indexes as if every extra query costs 20–50ms (it does), and consider the Worker driver specifically when your page renders >10 queries.

HMAC replay protection is per-isolate. Nonces are tracked in memory, so they reset on cold starts and don't span Cloudflare colos. HMAC_REQUIRED=true gives you body-tamper protection and best-effort replay defense — not a global guarantee. If that distinction matters, the README points at Durable Objects.

Config precedence runs user-first. D1ServiceProvider merges config/d1-database.php into database.connections.d1; your config/database.php entry wins. If a setting appears to have no effect, check there before opening an issue — and remember php artisan config:cache after env changes.

D1Pdo opens an unused sqlite::memory: connection to satisfy PDO inheritance. Harmless, but don't be alarmed when you see it in a stack trace or getPdo() debugging session. D1Connection similarly extends SQLiteConnection, which is why SQLite-flavored things behave the way they do.

Custom schema features go through D1SchemaGrammar. It extends the SQLite grammar and uses reflection to handle Laravel 12+ schema method signatures. If you're writing a custom blueprint macro or a column type, that's the extension point — and preserve cross-version compatibility, since the CI matrix spans Laravel 10–13 on PHP 8.2–8.4 (Laravel 13 excluded on 8.2).

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
marcelgwerder/laravel-api-handler
firebrikdotcom/conveyor-laravel-broadcaster
kanata-php/conveyor-laravel-broadcaster
alish/short-message
sonrac/lumen-league-oauth2
pushery/sanctum-hardening-for-laravel
evanschleret/laravel-uuidable
yigit-serin/laravel-openvidu
adachsoft/command-executor-lib
tagixo/document-builder
tagixo/page-builder
tagixo/mail-builder
tagixo/form-builder
primix/primix
tagixo/core
alesima/laravel-azure-service-bus
tomshaw/shopcart
likyly/laravel
darthsoup/shoppingcart
tdt/core