{"id":15715590,"url":"https://github.com/tanduy03/cloudflare-d1-database","last_synced_at":"2026-05-16T11:01:12.261Z","repository":{"id":255505316,"uuid":"845491307","full_name":"TanDuy03/cloudflare-d1-database","owner":"TanDuy03","description":"Laravel database driver for Cloudflare D1 - supports REST \u0026 Worker, Eloquent-ready - TanDuy03","archived":false,"fork":false,"pushed_at":"2026-05-13T09:20:31.000Z","size":442,"stargazers_count":24,"open_issues_count":1,"forks_count":7,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-13T11:14:52.736Z","etag":null,"topics":["cloudflare-d1","cloudflare-workers","d1","d1-database","database","database-driver","eloquent","laravel","laravel-package","php","serverless","sqlite","workers"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TanDuy03.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"TanDuy03","patreon":null}},"created_at":"2024-08-21T11:01:30.000Z","updated_at":"2026-05-13T09:18:53.000Z","dependencies_parsed_at":"2024-09-05T21:22:47.739Z","dependency_job_id":"928cc21d-dc29-412b-aabf-dfe8a4f01d93","html_url":"https://github.com/TanDuy03/cloudflare-d1-database","commit_stats":null,"previous_names":["tanduy03/cloudflare-d1-database"],"tags_count":22,"template":false,"template_full_name":null,"purl":"pkg:github/TanDuy03/cloudflare-d1-database","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TanDuy03%2Fcloudflare-d1-database","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TanDuy03%2Fcloudflare-d1-database/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TanDuy03%2Fcloudflare-d1-database/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TanDuy03%2Fcloudflare-d1-database/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TanDuy03","download_url":"https://codeload.github.com/TanDuy03/cloudflare-d1-database/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TanDuy03%2Fcloudflare-d1-database/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33100319,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-16T04:41:52.686Z","status":"ssl_error","status_checked_at":"2026-05-16T04:41:52.009Z","response_time":115,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloudflare-d1","cloudflare-workers","d1","d1-database","database","database-driver","eloquent","laravel","laravel-package","php","serverless","sqlite","workers"],"created_at":"2024-10-03T21:42:04.611Z","updated_at":"2026-05-16T11:01:12.249Z","avatar_url":"https://github.com/TanDuy03.png","language":"PHP","funding_links":["https://github.com/sponsors/TanDuy03"],"categories":[],"sub_categories":[],"readme":"# Cloudflare D1 Database Driver for Laravel\n\n[![codecov](https://codecov.io/gh/TanDuy03/cloudflare-d1-database/graph/badge.svg?token=9MSJ527ZMX)](https://codecov.io/gh/TanDuy03/cloudflare-d1-database)\n[![Tests](https://github.com/TanDuy03/cloudflare-d1-database/actions/workflows/tests.yml/badge.svg)](https://github.com/TanDuy03/cloudflare-d1-database/actions/workflows/tests.yml)\n![PHP](https://img.shields.io/badge/PHP-8.2+-777BB4?logo=php)\n![Laravel](https://img.shields.io/badge/Laravel-10--13.x-FF2D20?logo=laravel)\n[![Latest Stable Version](https://poser.pugx.org/ntanduy/cloudflare-d1-database/v/stable)](https://packagist.org/packages/ntanduy/cloudflare-d1-database)\n[![Total Downloads](https://poser.pugx.org/ntanduy/cloudflare-d1-database/downloads)](https://packagist.org/packages/ntanduy/cloudflare-d1-database)\n[![Monthly Downloads](https://poser.pugx.org/ntanduy/cloudflare-d1-database/d/monthly)](https://packagist.org/packages/ntanduy/cloudflare-d1-database)\n[![License](https://poser.pugx.org/ntanduy/cloudflare-d1-database/license)](https://packagist.org/packages/ntanduy/cloudflare-d1-database)\n\nUse [Cloudflare D1](https://developers.cloudflare.com/d1) as a native Laravel database driver — full Eloquent ORM, Query Builder, and Migration support.\n\n## 🎯 Requirements\n\n- **PHP**: \u003e= 8.2\n- **Laravel**: 10.x, 11.x, 12.x, or 13.x\n\n## ✨ Features\n\n- **Full Laravel Integration** — Eloquent ORM, Query Builder, Migrations, Seeding\n- **Two Connection Drivers** — REST API (zero infrastructure) or Worker (low latency)\n- **Batch Queries** — Execute multiple statements in a single HTTP round-trip\n- **Bulk Insert** — Insert hundreds of rows in a single atomic batch call\n- **Sessions / Read Replication** — Leverage D1 global read replicas for lower-latency reads (Worker driver)\n- **Auto Read/Write Splitting** — Automatic routing of SELECTs to replicas and writes to primary (Worker driver)\n- **Import** — Import SQL files into D1 via `php artisan d1:import`\n- **Schema Dump** — Export your D1 database via `php artisan d1:schema-dump`\n- **Time Travel** — Point-in-time recovery via `php artisan d1:time-travel`\n- **Database Info** — Inspect your D1 database with `php artisan d1:info`\n- **Circuit Breaker** — Fail fast on sustained outages instead of blocking on retries\n- **Automatic Retries** — Exponential backoff with jitter for 5xx/429 errors\n- **Query Logging** — Optional callback for monitoring and debugging\n- **Health Check** — Built-in `php artisan d1:health` to verify connection and measure latency\n\n## 🚀 Installation\n\n```bash\ncomposer require ntanduy/cloudflare-d1-database\n```\n\n## 👏 Usage\n\n### Step 1: Publish Configuration\n\n```bash\nphp artisan vendor:publish --tag=\"d1-config\"\n```\n\nThis creates `config/d1-database.php` with all available options.\n\n### Step 2: Choose a Driver\n\nThis package supports two drivers to connect Laravel with Cloudflare D1:\n\n| Driver | How it works | Latency | Setup |\n|--------|-------------|---------|-------|\n| **REST** (default) | Calls [Cloudflare D1 REST API](https://developers.cloudflare.com/api/resources/d1/subresources/database/methods/query/) directly | Higher (extra HTTP hop) | API Token only |\n| **Worker** | Routes queries through your own [Cloudflare Worker](https://developers.cloudflare.com/workers/) | Lower (co-located with D1) | Requires deploying a Worker |\n\n---\n\n### Driver 1: REST API (Default)\n\nThe simplest setup — no extra infrastructure needed. Queries are sent to Cloudflare's REST API.\n\n**Add to your `.env`:**\n\n```env\nCF_D1_API_TOKEN=your_api_token\nCF_D1_ACCOUNT_ID=your_account_id\nCF_D1_DATABASE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n```\n\n**How to get these values:**\n\n1. **API Token** — Go to [Cloudflare Dashboard → API Tokens](https://dash.cloudflare.com/profile/api-tokens) → Create Token → use the \"Edit Cloudflare D1\" template\n2. **Account ID** — Found on your Cloudflare Dashboard overview page (right sidebar)\n3. **Database ID** — Go to [Workers \u0026 Pages → D1](https://dash.cloudflare.com/?to=/:account/workers/d1) → click your database → copy the Database ID\n\nThat's it! Your Laravel app can now use D1.\n\n---\n\n### Driver 2: Worker (Low Latency)\n\nFor production apps that need lower latency, deploy a Cloudflare Worker as a proxy between Laravel and D1.\n\n**Add to your `.env`:**\n\n```env\nCF_D1_DRIVER=worker\nCF_D1_WORKER_URL=https://your-d1-worker.your-subdomain.workers.dev\nCF_D1_WORKER_SECRET=a-strong-shared-secret\n```\n\n#### Deploy the Worker\n\nA ready-to-deploy Worker template is included in the [`Worker/`](Worker/) directory. To deploy:\n\n```bash\ncd Worker\nnpm install\nnpx wrangler secret put WORKER_SECRET\nnpm run deploy\n```\n\nBefore deploying, update `wrangler.jsonc` with your D1 database binding:\n\n```jsonc\nname = \"ntanduy-d1-worker\"\nmain = \"src/index.ts\"\ncompatibility_date = \"2026-03-10\"\n\n[[d1_databases]]\nbinding = \"DB\"\ndatabase_name = \"your-database-name\"\ndatabase_id = \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\n```\n\n\u003e **Important:** Set `WORKER_SECRET` using `npx wrangler secret put WORKER_SECRET` — never put secrets in `wrangler.jsonc`. This secret must match the `CF_D1_WORKER_SECRET` in your Laravel `.env`.\n\n#### HMAC Request Signing (Optional)\n\nFor additional security, enable HMAC request signing. Each request gets a unique signature that prevents replay attacks and body tampering.\n\n**Laravel `.env`:**\n\n```env\nCF_D1_HMAC=true\n```\n\n**Worker (optional enforcement):**\n\n```bash\nnpx wrangler secret put HMAC_REQUIRED        # Set to \"true\" to reject unsigned requests\nnpx wrangler secret put HMAC_WINDOW_SECONDS  # Replay window (default: 300 = 5 minutes)\n```\n\nWhen enabled, the PHP driver adds `X-D1-Timestamp` and `X-D1-Signature` headers (HMAC-SHA256 of timestamp + body). The Worker verifies these when present. Without `HMAC_REQUIRED=true`, unsigned requests still work (backward compatible).\n\n#### Worker Endpoints\n\nThe Worker exposes these endpoints:\n\n| Endpoint | Method | Auth | Description |\n|----------|--------|------|-------------|\n| `/health` | GET | ❌ | Health check |\n| `/query` | POST | ✅ Bearer | Execute a single SQL query |\n| `/batch` | POST | ✅ Bearer | Execute multiple statements atomically |\n| `/exec` | POST | ✅ Bearer | Execute raw DDL/migration SQL |\n| `/raw` | POST | ✅ Bearer | Execute a query and return raw array-of-arrays |\n\n---\n\n### Step 3: Set as Default Connection\n\nTo use D1 as the default database, add to your `.env`:\n\n```env\nDB_CONNECTION=d1\n```\n\n### Step 4: Verify Connection\n\nRun the built-in health check to verify your setup:\n\n```bash\nphp artisan d1:health\n```\n\n```\n  D1 Health Check\n  Connection : d1\n  Driver     : worker\n\n+-------------------------+---------+------------------------------------------+\n| Check                   | Status  | Detail                                   |\n+-------------------------+---------+------------------------------------------+\n| worker_url configured   | ✓ OK    | https://d1-proxy.name.workers.dev        |\n| worker_secret configured| ✓ OK    | ******cret                               |\n| Query test passed       | ✓ OK    | SELECT 1 as ok                           |\n| End-to-end latency      | ✓ OK    | 24 ms                                    |\n+-------------------------+---------+------------------------------------------+\n\n  Overall: HEALTHY ✓\n```\n\n### Step 5: Run Migrations\n\n```bash\nphp artisan migrate --database=d1\n```\n\n## 📖 Examples\n\n### Eloquent ORM\n\n```php\nuse App\\Models\\Post;\n\n// Create\n$post = Post::create([\n    'title' =\u003e 'Hello from D1',\n    'body' =\u003e 'This is stored in Cloudflare D1!',\n]);\n\n// Read\n$posts = Post::where('published', true)-\u003eorderBy('created_at', 'desc')-\u003eget();\n\n// Update\n$post-\u003eupdate(['title' =\u003e 'Updated Title']);\n\n// Delete\n$post-\u003edelete();\n```\n\n### Query Builder\n\n```php\nuse Illuminate\\Support\\Facades\\DB;\n\n// Select\n$users = DB::connection('d1')-\u003etable('users')\n    -\u003ewhere('active', true)\n    -\u003elimit(10)\n    -\u003eget();\n\n// Insert\nDB::connection('d1')-\u003etable('users')-\u003einsert([\n    'name' =\u003e 'John Doe',\n    'email' =\u003e 'john@example.com',\n]);\n\n// Raw queries\n$results = DB::connection('d1')-\u003eselect('SELECT * FROM users WHERE id = ?', [1]);\n```\n\n### Query Logger\n\nMonitor queries for debugging or performance analysis:\n\n```php\nuse Ntanduy\\CFD1\\D1\\D1Connection;\n\n/** @var D1Connection $connection */\n$connection = DB::connection('d1');\n\n$connection-\u003ed1()-\u003esetQueryLogger(function (\n    string $query,\n    array $params,\n    float $timeMs,\n    bool $success,\n    ?array $error\n) {\n    if (! $success) {\n        Log::error(\"D1 query failed: {$query}\", [\n            'params' =\u003e $params,\n            'error' =\u003e $error,\n            'time_ms' =\u003e $timeMs,\n        ]);\n    }\n});\n```\n\n### Runtime Driver Detection\n\n```php\nuse Ntanduy\\CFD1\\D1\\D1Connection;\n\n/** @var D1Connection $connection */\n$connection = DB::connection('d1');\n\n$connection-\u003egetDriver();      // 'rest' or 'worker'\n$connection-\u003eisWorkerDriver(); // true or false\n```\n\n### Batch Queries\n\nExecute multiple SQL statements in a single HTTP round-trip. On the Worker driver, this uses D1's native `batch()` for atomic execution.\n\n```php\nuse Ntanduy\\CFD1\\D1\\D1Connection;\n\n/** @var D1Connection $connection */\n$connection = DB::connection('d1');\n\n$results = $connection-\u003ebatch([\n    ['sql' =\u003e 'SELECT * FROM users WHERE id = ?', 'params' =\u003e [1]],\n    ['sql' =\u003e 'UPDATE stats SET views = views + 1 WHERE id = ?', 'params' =\u003e [5]],\n    ['sql' =\u003e 'SELECT COUNT(*) as total FROM posts'],\n]);\n\n$user  = $results[0]; // Result set from first statement\n$stats = $results[1]; // Result set from second statement\n$count = $results[2]; // Result set from third statement\n```\n\nIf any statement fails, a `D1BatchException` is thrown with the index of the failing statement:\n\n```php\nuse Ntanduy\\CFD1\\D1\\Exceptions\\D1BatchException;\n\ntry {\n    $results = $connection-\u003ebatch($statements);\n} catch (D1BatchException $e) {\n    // $e-\u003egetMessage() includes the failing statement index\n}\n```\n\n### Driver Feature Matrix\n\n| Feature | REST | Worker |\n|---------|------|--------|\n| Bulk Insert | ✅ | ✅ |\n| Sessions / Read Replication | ❌ Not supported | ✅ Full support |\n| Auto Read/Write Splitting | ❌ Not supported | ✅ Full support |\n| Import (`d1:import`) | ✅ | ✅ (via REST credentials) |\n| Time Travel (`d1:time-travel`) | ✅ | ✅ (via REST credentials) |\n| Schema Dump | ✅ | ✅ (via REST credentials) |\n| Database Info (`d1:info`) | ✅ Full metadata | ✅ Query test + REST metadata |\n| Batch Queries | ✅ | ✅ |\n| Circuit Breaker | ✅ | ✅ |\n| Automatic Retries | ✅ | ✅ |\n\n### Bulk Insert\n\nInsert multiple rows efficiently using D1 batch execution — one HTTP round-trip, atomic:\n\n```php\nuse Ntanduy\\CFD1\\D1\\D1Connection;\n\n/** @var D1Connection $connection */\n$connection = DB::connection('d1');\n\n$connection-\u003ebulkInsert('users', [\n    ['name' =\u003e 'Alice', 'email' =\u003e 'alice@example.com'],\n    ['name' =\u003e 'Bob', 'email' =\u003e 'bob@example.com'],\n    ['name' =\u003e 'Charlie', 'email' =\u003e 'charlie@example.com'],\n]);\n```\n\n- Each row becomes a parameterized INSERT (SQL injection safe)\n- All rows are sent as a D1 batch (atomic — if any fails, none are applied)\n- Rows exceeding D1's 100-statement batch limit are automatically chunked\n- Works with both REST and Worker drivers\n\n### Sessions / Read Replication (Worker Driver Only)\n\nD1 supports [global read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/) — read queries can be served by nearby replicas for lower latency. The Sessions API ensures sequential consistency across queries.\n\n\u003e **Important:** Sessions are only available with the **Worker driver**. The REST API does not support D1 Sessions — this is a [Cloudflare platform limitation](https://developers.cloudflare.com/d1/best-practices/read-replication/).\n\n#### Enable via Config\n\nAdd to your `.env`:\n\n```env\nCF_D1_SESSION_ENABLED=true\nCF_D1_SESSION_MODE=first-unconstrained   # or 'first-primary'\n```\n\nThis automatically enables sessions for all queries on the Worker driver.\n\n#### Enable Programmatically\n\n```php\nuse Ntanduy\\CFD1\\D1\\D1Connection;\n\n/** @var D1Connection $connection */\n$connection = DB::connection('d1');\n\n// Start a session — first query goes to any instance (fastest)\n$connection-\u003ewithSession('first-unconstrained');\n\n// Or start with the latest data from primary\n$connection-\u003ewithSession('first-primary');\n\n// Execute queries — bookmarks are tracked automatically\n$users = DB::table('users')-\u003eget();\n$posts = DB::table('posts')-\u003eget();\n\n// Get the current bookmark (for passing to another request/session)\n$bookmark = $connection-\u003egetBookmark();\n\n// Start a new session from a previous bookmark\n$connection-\u003ewithSession($bookmark);\n\n// End the session when done\n$connection-\u003eendSession();\n```\n\n#### Session Modes\n\n| Mode | First Query | Use When |\n|------|------------|----------|\n| `first-unconstrained` | Any instance (primary or replica) | Lowest latency, eventual consistency OK |\n| `first-primary` | Primary database | Need the latest data for first query |\n| `\u003cbookmark\u003e` | At least as fresh as the bookmark | Continuing from a previous session |\n\n#### How It Works\n\n1. PHP sends a `session` parameter with each query to the Worker\n2. Worker calls `env.DB.withSession(param)` to create a D1 session\n3. Worker returns a `bookmark` in the response\n4. PHP stores the bookmark and uses it for the next query\n5. This ensures **sequential consistency** across HTTP calls\n\n#### Worker Template\n\nThe Worker template in [`Worker/`](Worker/) already includes session support. If you're upgrading from a previous version, redeploy the Worker:\n\n```bash\ncd Worker \u0026\u0026 npm run deploy\n```\n\n### Auto Read/Write Splitting (Worker Driver Only)\n\nAutomatically route `SELECT` queries to D1 replicas and `INSERT`/`UPDATE`/`DELETE` to the primary — zero code changes required.\n\n```php\n// config/database.php\n'd1' =\u003e [\n    'driver' =\u003e 'd1',\n    'd1_driver' =\u003e 'worker',\n    'worker_url' =\u003e env('CF_D1_WORKER_URL'),\n    'worker_secret' =\u003e env('CF_D1_WORKER_SECRET'),\n\n    'read' =\u003e [\n        'session' =\u003e ['mode' =\u003e 'first-unconstrained'],\n    ],\n    'write' =\u003e [\n        'session' =\u003e ['mode' =\u003e 'first-primary'],\n    ],\n    'sticky' =\u003e true,  // After write, reads use primary for consistency\n],\n```\n\nOnce configured, Laravel handles everything:\n\n```php\n// Automatically goes to replica (fast, nearby)\n$users = User::all();\n\n// Automatically goes to primary\nUser::create(['name' =\u003e 'Alice', 'email' =\u003e 'alice@example.com']);\n\n// With sticky=true, this read goes to primary (sees the new user)\n$user = User::where('email', 'alice@example.com')-\u003efirst();\n```\n\n- **`sticky` (default: `true`)** — after a write, subsequent reads in the same request use the write connector's bookmark for sequential consistency\n- Works alongside manual `withSession()` — R/W splitting handles the base routing, you can still use sessions for fine-grained control\n- **REST driver ignores** `read`/`write` config — no sessions support, all queries go to primary\n\n### Database Info\n\nInspect your D1 database metadata and connection status:\n\n```bash\nphp artisan d1:info\n```\n\nDisplays database name, UUID, size, table count, read replication mode, R/W splitting status, circuit breaker state, and runs a query test.\n\n```bash\n# Specify a connection\nphp artisan d1:info --connection=d1\n```\n\n\u003e Uses the [D1 REST API](https://developers.cloudflare.com/api/resources/d1/subresources/database/methods/get/) for metadata. Worker-only users see table count and query test but need REST credentials for full metadata.\n\n### Import\n\nImport a SQL file into your D1 database:\n\n```bash\nphp artisan d1:import path/to/file.sql\n```\n\nThe command handles the full import flow automatically:\n1. Computes MD5 hash and sends `init` request to get a presigned upload URL\n2. Uploads the SQL file to R2\n3. Triggers ingestion\n4. Polls until import is complete\n\n```bash\n# Specify connection\nphp artisan d1:import database/seeds/data.sql --connection=d1\n```\n\n\u003e **Note:** Like `d1:schema-dump`, the import command always uses the REST API. Worker-only users must also set `CF_D1_API_TOKEN`, `CF_D1_ACCOUNT_ID`, and `CF_D1_DATABASE_ID` in their `.env`.\n\n### Time Travel\n\nD1 automatically creates restore points (bookmarks) for up to 30 days. Use `d1:time-travel` to get the current bookmark or restore your database to any point in time:\n\n```bash\n# Get the current bookmark\nphp artisan d1:time-travel\n\n# Get the bookmark at a specific timestamp\nphp artisan d1:time-travel --timestamp=\"2024-01-15T10:30:00+00:00\"\n\n# Unix timestamps also work\nphp artisan d1:time-travel --timestamp=1705312200\n```\n\nTo restore the database to a previous state:\n\n```bash\n# Restore to a specific bookmark\nphp artisan d1:time-travel --restore --bookmark=\"00000085-0000024c-00004c6d-abc123\"\n\n# Restore to a timestamp\nphp artisan d1:time-travel --restore --timestamp=\"2024-01-15T10:30:00+00:00\"\n```\n\n\u003e **Warning:** Restore is a destructive operation — it overwrites the database in place. In-flight queries will be cancelled. The command will prompt for confirmation before proceeding. The previous bookmark is shown after restore so you can undo if needed.\n\n### Schema Dump\n\nExport your D1 database schema (and optionally data) as a SQL file:\n\n```bash\nphp artisan d1:schema-dump\n```\n\nThis uses the [D1 export REST API](https://developers.cloudflare.com/api/resources/d1/subresources/database/methods/export/) with polling mode. The dump is saved to `database/schema/{connection}-schema.sql`.\n\n#### Options\n\n```bash\n# Schema only (no data)\nphp artisan d1:schema-dump --no-data\n\n# Custom output path\nphp artisan d1:schema-dump --path=./backup.sql\n\n# Delete migration files after dumping (same as native schema:dump --prune)\nphp artisan d1:schema-dump --prune\n\n# Specify connection name\nphp artisan d1:schema-dump --connection=d1\n```\n\n\u003e **Note:** `d1:schema-dump` always uses the REST API for export, even when the Worker driver is your primary connection. Worker-only users must also set `CF_D1_API_TOKEN`, `CF_D1_ACCOUNT_ID`, and `CF_D1_DATABASE_ID` in their `.env` for the dump command to work.\n\n### Circuit Breaker\n\nPrevents cascading failures when Cloudflare Workers experience cold starts or sustained outages. Instead of blocking for 30s+ on retries, the circuit breaker fails fast after consecutive failures.\n\n**States:**\n\n```\nCLOSED → requests pass through normally\n  ↓ (threshold consecutive failures)\nOPEN → requests rejected immediately, no HTTP call\n  ↓ (after cooldown seconds)\nHALF_OPEN → one probe request allowed through\n  ↓ success → CLOSED  |  failure → OPEN\n```\n\n**Enable in your config** (`config/database.php` or `config/d1-database.php`):\n\n```php\n'd1' =\u003e [\n    // ... other options ...\n    'circuit_breaker' =\u003e [\n        'enabled'      =\u003e env('CF_D1_CB_ENABLED', false),\n        'threshold'    =\u003e env('CF_D1_CB_THRESHOLD', 5),     // failures before opening\n        'cooldown'     =\u003e env('CF_D1_CB_COOLDOWN', 30),     // seconds before probe\n        'cache_driver' =\u003e env('CF_D1_CB_CACHE_DRIVER', 'file'),\n    ],\n],\n```\n\n**Handle the exception:**\n\n```php\nuse Ntanduy\\CFD1\\D1\\Exceptions\\CircuitBreakerOpenException;\n\ntry {\n    $users = DB::connection('d1')-\u003etable('users')-\u003eget();\n} catch (CircuitBreakerOpenException $e) {\n    // Circuit is open — fail fast, use fallback or return cached data\n}\n```\n\n\u003e **Note:** Use `file` or `redis` as the `cache_driver`. Avoid `database` to prevent a dependency loop when D1 itself is down.\n\n### Retry \u0026 Backoff\n\nThe driver automatically retries failed requests with exponential backoff and jitter:\n\n- **Retried:** 5xx server errors, 429 rate limiting, connection timeouts\n- **Not retried:** 4xx client errors (400, 401, 403, 404, etc.)\n\n```env\nCF_D1_RETRIES=2          # Max retry attempts (default: 2)\nCF_D1_RETRY_DELAY=100    # Base delay in ms (default: 100)\nCF_D1_TIMEOUT=10         # Request timeout in seconds (default: 10)\nCF_D1_CONNECT_TIMEOUT=5  # Connection timeout in seconds (default: 5)\n```\n\nBackoff formula: `delay × 2^(attempt-1) + random jitter (0-100ms)`\n\n| Attempt | Base Delay (100ms) |\n|---------|-------------------|\n| 1       | ~100-200ms        |\n| 2       | ~200-300ms        |\n\n## ⚙️ Configuration Reference\n\n### Manual Setup (Alternative)\n\nInstead of publishing the config, you can add the connection directly to `config/database.php`:\n\n```php\n'connections' =\u003e [\n    'd1' =\u003e [\n        'driver' =\u003e 'd1',\n        'd1_driver' =\u003e env('CF_D1_DRIVER', 'rest'),         // 'rest' or 'worker'\n        'prefix' =\u003e '',\n        'database' =\u003e env('CF_D1_DATABASE_ID', ''),\n\n        // REST driver credentials\n        'api' =\u003e 'https://api.cloudflare.com/client/v4',\n        'auth' =\u003e [\n            'token' =\u003e env('CF_D1_API_TOKEN', ''),\n            'account_id' =\u003e env('CF_D1_ACCOUNT_ID', ''),\n        ],\n\n        // Worker driver credentials\n        'worker_url' =\u003e env('CF_D1_WORKER_URL', ''),\n        'worker_secret' =\u003e env('CF_D1_WORKER_SECRET', ''),\n        'hmac' =\u003e env('CF_D1_HMAC', false),\n\n        // Performance tuning\n        'timeout' =\u003e env('CF_D1_TIMEOUT', 10),\n        'connect_timeout' =\u003e env('CF_D1_CONNECT_TIMEOUT', 5),\n        'retries' =\u003e env('CF_D1_RETRIES', 2),\n        'retry_delay' =\u003e env('CF_D1_RETRY_DELAY', 100),\n\n        // Sessions / Read Replication (Worker driver only)\n        'session' =\u003e [\n            'enabled' =\u003e env('CF_D1_SESSION_ENABLED', false),\n            'mode'    =\u003e env('CF_D1_SESSION_MODE', 'first-unconstrained'),\n        ],\n\n        // Circuit breaker (optional)\n        'circuit_breaker' =\u003e [\n            'enabled'      =\u003e env('CF_D1_CB_ENABLED', false),\n            'threshold'    =\u003e env('CF_D1_CB_THRESHOLD', 5),\n            'cooldown'     =\u003e env('CF_D1_CB_COOLDOWN', 30),\n            'cache_driver' =\u003e env('CF_D1_CB_CACHE_DRIVER', 'file'),\n        ],\n    ],\n],\n```\n\n### Options Reference\n\n| Option               | Default                                | Description                                                                 |\n|----------------------|----------------------------------------|-----------------------------------------------------------------------------|\n| `d1_driver`          | `rest`                                 | Connection driver: `rest` (Cloudflare REST API) or `worker` (custom Worker) |\n| `database`           | —                                      | Your Cloudflare D1 Database ID                                              |\n| `api`                | `https://api.cloudflare.com/client/v4` | Cloudflare API base URL (REST driver only)                                  |\n| `auth.token`         | —                                      | Cloudflare API Token (REST driver only)                                     |\n| `auth.account_id`    | —                                      | Cloudflare Account ID (REST driver only)                                    |\n| `worker_url`         | —                                      | Your Worker URL (Worker driver only)                                        |\n| `worker_secret`      | —                                      | Shared secret for Worker auth (Worker driver only)                          |\n| `hmac`               | `false`                                | Enable HMAC request signing for replay protection (Worker driver only)      |\n| `timeout`            | `10`                                   | HTTP request timeout in seconds                                             |\n| `connect_timeout`    | `5`                                    | HTTP connection timeout in seconds                                          |\n| `retries`            | `2`                                    | Max retry attempts on 5xx/429 errors                                        |\n| `retry_delay`        | `100`                                  | Base delay between retries in milliseconds                                  |\n| `session.enabled`        | `false`                            | Enable D1 sessions for read replication (Worker driver only)                |\n| `session.mode`           | `first-unconstrained`              | Session mode: `first-primary` or `first-unconstrained`                      |\n| `circuit_breaker.enabled` | `false`                           | Enable circuit breaker for fail-fast behavior                               |\n| `circuit_breaker.threshold` | `5`                             | Consecutive failures before opening the circuit                             |\n| `circuit_breaker.cooldown` | `30`                              | Seconds before allowing a probe request                                     |\n| `circuit_breaker.cache_driver` | `file`                        | Laravel cache driver for circuit state (`file`, `redis`)                    |\n\n\n### Environment Variables\n\n```env\n# Driver selection\nCF_D1_DRIVER=rest                    # 'rest' or 'worker'\n\n# REST driver\nCF_D1_API_TOKEN=your_api_token\nCF_D1_ACCOUNT_ID=your_account_id\nCF_D1_DATABASE_ID=your_database_id\n\n# Worker driver\nCF_D1_WORKER_URL=https://your-worker.workers.dev\nCF_D1_WORKER_SECRET=your_shared_secret\nCF_D1_HMAC=false                     # Enable HMAC request signing\n\n# Performance tuning (optional)\nCF_D1_TIMEOUT=10\nCF_D1_CONNECT_TIMEOUT=5\nCF_D1_RETRIES=2\nCF_D1_RETRY_DELAY=100\n\n# Sessions / Read Replication (Worker driver only)\nCF_D1_SESSION_ENABLED=false\nCF_D1_SESSION_MODE=first-unconstrained\n\n# Circuit breaker (optional)\nCF_D1_CB_ENABLED=false\nCF_D1_CB_THRESHOLD=5\nCF_D1_CB_COOLDOWN=30\nCF_D1_CB_CACHE_DRIVER=file\n```\n\n## ⚠️ Limitations\n\n- **No real transactions** — D1 is stateless over HTTP and doesn't support `BEGIN`/`COMMIT`/`ROLLBACK`. The driver makes these methods no-ops so Laravel internals (auth, sessions, middleware) work without crashing.\n\n  - `DB::transaction(Closure)` **will execute the closure**, but provides **no atomicity** — each query runs immediately and cannot be rolled back on failure.\n  - `DB::transaction(Closure, attempts: N)` retries the closure on any exception, but without real deadlock detection or isolation.\n  - For atomic multi-statement execution, use **`batch()`** which leverages D1's native batch API:\n\n    ```php\n    $connection-\u003ebatch([\n        ['sql' =\u003e 'INSERT INTO orders ...', 'params' =\u003e [...]],\n        ['sql' =\u003e 'UPDATE inventory ...', 'params' =\u003e [...]],\n    ]);\n    ```\n\n- **REST API latency** — Each query is an HTTP request routed through the Cloudflare API. The Worker driver offers significantly lower latency because the Worker is co-located with your D1 database. Latency varies by region, database size, and query complexity.\n- **Sessions / Read Replication — Worker driver only** — The D1 Sessions API is only available via the Worker Binding. The REST API does not support sessions; all queries go to the primary database. This is a [Cloudflare platform limitation](https://developers.cloudflare.com/d1/best-practices/read-replication/).\n- **Schema dump requires REST credentials** — `d1:schema-dump` uses the D1 export REST API. Even Worker-only users must set `CF_D1_API_TOKEN`, `CF_D1_ACCOUNT_ID`, and `CF_D1_DATABASE_ID`.\n- **Export blocks queries** — During export, D1 may be unavailable for queries (Cloudflare limitation for large databases).\n- **Bulk insert batch limit** — D1 batch supports max 100 statements. `bulkInsert()` automatically chunks larger datasets but each chunk is a separate HTTP call.\n- **No streaming** — Large result sets are loaded entirely into memory.\n\n## 🌱 Testing\n\n### PHP Tests\n\n```bash\nvendor/bin/pest\n```\n\n### Worker Tests (Vitest)\n\n```bash\ncd Worker\nnpm ci\nnpm test\n```\n\n### Local Development with Worker\n\nStart the built-in Worker to test against a local D1 instance:\n\n```bash\ncd Worker\nnpm ci\nnpm run dev\n```\n\n## 🤝 Contributing\n\nContributions are welcome! Please open an issue or pull request on [GitHub](https://github.com/TanDuy03/cloudflare-d1-database).\n\n## 🔒 Security\n\nIf you discover any security related issues, please email \u003ccontact@ntanduy.com\u003e instead of using the issue tracker.\n\n## 🎉 Credits\n\n- [TanDuy03](https://github.com/TanDuy03)\n- [All Contributors](../../contributors)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftanduy03%2Fcloudflare-d1-database","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftanduy03%2Fcloudflare-d1-database","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftanduy03%2Fcloudflare-d1-database/lists"}