erimeilis/laravel-cloudflare-d1

Production-ready Cloudflare D1 database driver for Laravel with full Eloquent support, 20x faster bulk inserts, and complete MySQL migration tooling

Installs: 47

Dependents: 0

Suggesters: 0

Security: 0

Stars: 2

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/erimeilis/laravel-cloudflare-d1

1.1.0 2025-11-04 10:52 UTC

This package is auto-updated.

Last update: 2025-12-04 16:17:40 UTC


README

Supercharge your Laravel apps with Cloudflare's edge database

Latest Version on Packagist Total Downloads Tests PHP Version Laravel 11+ License: MIT

🌍 Deploy your database to Cloudflare's global edge network ⚑ 20x faster bulk inserts with automatic raw SQL optimization πŸ”„ One-command MySQL to D1 migration 🎯 Zero-config Eloquent ORM support β€” just worksβ„’

✨ Features

🎯 Core Functionality

  • βœ… Drop-in Replacement β€” Works with existing Laravel database code
  • πŸ”„ Full Eloquent ORM β€” Models, relationships, migrations, seeds... everything!
  • πŸ›‘οΈ Foreign Key Constraints β€” Automatically enabled (unlike standard SQLite)
  • πŸ“‹ Schema Builder Support β€” Create/modify tables with familiar Laravel syntax

⚑ Performance & Optimization

  • πŸš€ 20x Faster Bulk Inserts β€” Automatic raw SQL conversion (47s β†’ 2.3s on 250 rows)
  • πŸ“¦ Smart Chunking β€” Leverages D1's 100KB SQL limit vs 100-parameter limit
  • πŸ”„ Zero Configuration β€” Works transparently with insert(), insertOrIgnore(), upsert()
  • 🎯 Zero Overhead β€” Direct REST API communication with D1
  • ⏱️ Production Tested β€” Validated with real-world workloads

🌍 Global Distribution

  • 🌐 Edge Database β€” Data stored on Cloudflare's global network
  • πŸ—ΊοΈ Low Latency β€” 50-150ms reads from anywhere in the world
  • πŸ“ˆ Scales to Zero β€” Pay only for what you use
  • πŸ’° Free Tier Friendly β€” 500MB storage, 5M reads/day included

πŸ”„ MySQL Migration

  • πŸ“¦ One-Command Migration β€” Migrate entire MySQL database with single command
  • 🎯 Smart Schema Conversion β€” Automatic MySQLβ†’SQLite type mapping
  • βœ… Data Integrity β€” Automatic validation and verification
  • πŸš€ High Performance β€” 33x faster with intelligent batching

πŸ”§ Developer Experience

  • 🎯 Laravel 11 & 12 Compatible β€” Tested with modern Laravel versions
  • πŸ§ͺ Full Test Coverage β€” 57 automated tests, 100% passing
  • πŸ“– Comprehensive Docs β€” Every feature explained with examples
  • πŸ’‘ Easy Setup β€” 5-minute configuration, no complex setup

πŸ“¦ Installation

composer require erimeilis/laravel-cloudflare-d1

The package will automatically register via Laravel's package discovery.

Requirements:

  • PHP 8.2 or higher
  • Laravel 11.x or 12.x
  • Cloudflare account (free tier works!)

Getting Started with Cloudflare D1

Before using this package, you need to set up a D1 database in your Cloudflare account and get your credentials.

Step 1: Create a Cloudflare D1 Database

  1. Sign up/Login to Cloudflare

  2. Create a D1 Database

    • In the Cloudflare dashboard, navigate to Workers & Pages β†’ D1 SQL Database
    • Click "Create database"
    • Enter a database name (e.g., my-laravel-db)
    • Click "Create"
  3. Note Your Database ID

    • After creation, you'll see your database listed
    • Click on your database name
    • Copy the Database ID (looks like: a1b2c3d4-e5f6-7890-abcd-ef1234567890)

Step 2: Get Your Cloudflare Credentials

πŸ†” Account ID

You can find your Account ID using any of these methods:

Method 1: From the URL (Easiest) ⚑

  1. Go to your Cloudflare dashboard: dash.cloudflare.com
  2. Look at the URL in your browser's address bar
  3. The Account ID is the string of characters immediately after dash.cloudflare.com/
    • Example: dash.cloudflare.com/1234567890abcdef1234567890abcdef/workers-and-pages
    • Your Account ID: 1234567890abcdef1234567890abcdef

Method 2: Workers & Pages Section

  1. Go to dash.cloudflare.com
  2. Navigate to Workers & Pages in the left sidebar
  3. Look for the Account details section on the right
  4. Click Click to copy next to your Account ID

Method 3: Account Overview API Section

  1. Go to your Account Home in the dashboard
  2. Scroll down to the API section at the bottom of the page
  3. You'll see your Account ID displayed there

πŸ”‘ API Token

  1. Go to dash.cloudflare.com/profile/api-tokens
  2. Click "Create Token"
  3. Use the "Edit Cloudflare Workers" template OR create a custom token with these permissions:
    • Account β†’ D1 β†’ Edit
  4. Click "Continue to summary"
  5. Click "Create Token"
  6. ⚠️ IMPORTANT: Copy your token immediately - you won't see it again!

πŸ“‹ Quick Summary

You need three values:

  • πŸ†” CLOUDFLARE_ACCOUNT_ID: From dashboard URL or Workers & Pages section (see Method 1 above)
  • πŸ’Ύ CLOUDFLARE_D1_DATABASE_ID: From D1 database details page
  • πŸ”‘ CLOUDFLARE_D1_API_TOKEN: Generated via API Tokens page

βš™οΈ Configuration

1. Environment Variables

Add these to your .env file:

# Get from: Dashboard sidebar
CLOUDFLARE_ACCOUNT_ID=1234567890abcdef1234567890abcdef

# Get from: D1 database details page
CLOUDFLARE_D1_DATABASE_ID=a1b2c3d4-e5f6-7890-abcd-ef1234567890

# Get from: API Tokens page (create new token)
CLOUDFLARE_D1_API_TOKEN=your_secret_token_here

2. Database Configuration

Add to config/database.php:

'connections' => [
    // ... existing connections

    'd1' => [
        'driver' => 'd1',
        'account_id' => env('CLOUDFLARE_ACCOUNT_ID'),
        'database_id' => env('CLOUDFLARE_D1_DATABASE_ID'),
        'api_token' => env('CLOUDFLARE_D1_API_TOKEN'),
        'prefix' => '',
        'prefix_indexes' => true,
    ],
],

3. Publish Configuration (Optional)

php artisan vendor:publish --provider="EriMeilis\CloudflareD1\D1ServiceProvider" --tag="config"

This creates config/cloudflare-d1.php for advanced configuration.

πŸ§ͺ Quick Start Testing

Want to verify everything works? Here's a 2-minute test:

Test 1: Check Connection

php artisan tinker
// Test the connection
DB::connection('d1')->select('SELECT 1 as test');
// Should return: [{"test": 1}]

Test 2: Create a Table

Create a simple migration:

php artisan make:migration create_test_users_table

Edit the migration:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    protected $connection = 'd1';

    public function up(): void
    {
        Schema::connection('d1')->create('test_users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::connection('d1')->dropIfExists('test_users');
    }
};

Run the migration:

php artisan migrate --database=d1

Test 3: Insert and Query Data

php artisan tinker
// Insert
DB::connection('d1')->table('test_users')->insert([
    'name' => 'Alice',
    'email' => 'alice@example.com',
    'created_at' => now(),
    'updated_at' => now(),
]);

// Query
$users = DB::connection('d1')->table('test_users')->get();
// Should return your inserted record!

// Test batching (10x faster!)
DB::connection('d1')->transaction(function () {
    for ($i = 1; $i <= 10; $i++) {
        DB::connection('d1')->table('test_users')->insert([
            'name' => "User {$i}",
            'email' => "user{$i}@example.com",
            'created_at' => now(),
            'updated_at' => now(),
        ]);
    }
});
// All 10 INSERTs executed in ONE batch API call! πŸš€

Test 4: Eloquent Model

Create a model:

php artisan make:model TestUser
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class TestUser extends Model
{
    protected $connection = 'd1';
    protected $table = 'test_users';
    protected $fillable = ['name', 'email'];
}

Use it:

php artisan tinker
// Create
$user = App\Models\TestUser::create([
    'name' => 'Bob',
    'email' => 'bob@example.com'
]);

// Find
$user = App\Models\TestUser::find(1);

// Update
$user->update(['name' => 'Bob Updated']);

// All
$users = App\Models\TestUser::all();

βœ… If all tests pass, you're ready to use D1 in your Laravel app!

πŸ“š Usage

🎯 Models

Use D1 exactly like any other Laravel database:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $connection = 'd1';
    protected $fillable = ['name', 'email'];
}

// Usage
User::create(['name' => 'Alice', 'email' => 'alice@example.com']);
$users = User::where('active', true)->get();

πŸ—οΈ Migrations

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    protected $connection = 'd1';

    public function up(): void
    {
        Schema::connection('d1')->create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::connection('d1')->dropIfExists('users');
    }
};

Run migrations:

php artisan migrate --database=d1

πŸ”§ Query Builder

use Illuminate\Support\Facades\DB;

// Select
$users = DB::connection('d1')
    ->table('users')
    ->where('active', true)
    ->get();

// Insert
DB::connection('d1')
    ->table('users')
    ->insert([
        'name' => 'Bob',
        'email' => 'bob@example.com',
    ]);

// Update
DB::connection('d1')
    ->table('users')
    ->where('id', 1)
    ->update(['name' => 'Bob Updated']);

// Delete
DB::connection('d1')
    ->table('users')
    ->where('id', 1)
    ->delete();

⚑ Performance Optimization

1. πŸš€ Use Transactions for Bulk Operations (10x Faster!)

DB::connection('d1')->transaction(function () {
    foreach ($users as $userData) {
        User::create($userData);
    }
});
// All INSERTs executed in a single batch API call!

Performance: 10 INSERTs go from ~1000ms to ~150ms

2. πŸ”— Eager Load Relationships

// ❌ Bad: N+1 queries
$users = User::all();
foreach ($users as $user) {
    echo $user->posts->count();
}

// βœ… Good: 2 queries total
$users = User::with('posts')->get();

3. πŸ“¦ Chunk Large Datasets

User::chunk(1000, function ($users) {
    foreach ($users as $user) {
        // Process
    }
});

πŸ”§ Advanced Configuration

πŸ’Ύ Multiple D1 Databases

// config/database.php
'connections' => [
    'd1_primary' => [
        'driver' => 'd1',
        'database_id' => env('D1_PRIMARY_DATABASE_ID'),
        // ...
    ],
    'd1_analytics' => [
        'driver' => 'd1',
        'database_id' => env('D1_ANALYTICS_DATABASE_ID'),
        // ...
    ],
],

πŸ“Š Custom Batch Size

// config/cloudflare-d1.php
'batch' => [
    'enabled' => true,
    'size' => 100, // Max queries per batch (1-100)
],

⚑ Query Caching (Read-Heavy Workloads)

// config/cloudflare-d1.php
'cache' => [
    'enabled' => true,
    'driver' => 'redis',
    'ttl' => 300, // 5 minutes
],

πŸ” How It Works

  1. πŸ”Œ Custom PDO Driver: Translates PDO calls to D1 REST API requests
  2. πŸ“¦ Query Batching: Accumulates queries in transactions β†’ single batch API call
  3. πŸ“ SQLite Grammar: D1 uses SQLite syntax, so we extend Laravel's SQLite grammar
  4. πŸ”— Foreign Keys: Automatically enabled (disabled by default in SQLite)

πŸ—οΈ Architecture

Laravel Eloquent/Query Builder
           ↓
      D1 Connection
           ↓
         D1 PDO
           ↓
    Query Batcher (batching enabled in transactions)
           ↓
     D1 API Client
           ↓
   Cloudflare D1 REST API

⚠️ Limitations

πŸ”§ D1/SQLite Limitations

  • ❌ No FULLTEXT indexes β†’ Use Laravel Scout for full-text search
  • ❌ No stored procedures β†’ Move logic to application layer
  • ⚠️ Limited ALTER TABLE β†’ Some schema changes require table rebuild
  • βœ… 100 parameter limit per query β†’ Automatically handled by this package
  • πŸ’Ύ Database size: 10 GB max (Paid plan), 500 MB (Free plan)

πŸ“Š Performance Characteristics

  • 🎯 Best for: Read-heavy workloads, globally distributed apps
  • ⏱️ Write latency: ~50-200ms per query (50-150ms with batching)
  • πŸš€ Read latency: ~50-150ms per query
  • ⚑ Batch operations: 10-11x faster for multiple operations

πŸ› οΈ Troubleshooting

πŸ”— Foreign Key Constraint Errors

D1/SQLite has foreign keys disabled by default. This package automatically enables them, but if you encounter issues:

// Manually enable
DB::connection('d1')->statement('PRAGMA foreign_keys = ON');

// Or disable for specific operations
DB::connection('d1')->disableForeignKeyConstraints();
// ... operations ...
DB::connection('d1')->enableForeignKeyConstraints();

⚑ Slow Query Performance

Enable query logging to identify slow queries:

// config/cloudflare-d1.php
'monitoring' => [
    'slow_query_threshold' => 1000, // Log queries > 1000ms
    'log_api_requests' => true,
],

πŸ” API Authentication Errors

Error: "D1 API request failed: Unauthorized" or "Invalid credentials"

This means your Cloudflare credentials are incorrect or missing. Verify them:

# Check environment variables are loaded
php artisan tinker
>>> env('CLOUDFLARE_ACCOUNT_ID')
>>> env('CLOUDFLARE_D1_DATABASE_ID')
>>> env('CLOUDFLARE_D1_API_TOKEN')

If any return null, check:

  1. Environment file: Ensure .env has the correct values (no quotes needed)

  2. Config cache: Clear Laravel's config cache

    php artisan config:clear
  3. Credential format:

    • Account ID: 32-character hexadecimal (e.g., 1234567890abcdef1234567890abcdef)
    • Database ID: UUID format (e.g., a1b2c3d4-e5f6-7890-abcd-ef1234567890)
    • API Token: Long alphanumeric string starting with token identifier
  4. API Token permissions: Ensure your token has D1 Edit permissions

    • Go to API Tokens
    • Click on your token
    • Verify it has "Account - D1 - Edit" permission

Error: "Database not found" or "Database ID invalid"

  1. Verify the database ID is correct:

    • Go to Cloudflare D1 Dashboard
    • Navigate to Workers & Pages β†’ D1 SQL Database
    • Click on your database
    • Copy the Database ID from the details page
  2. Ensure the database exists and is associated with the correct account

Common Mistakes:

  • ❌ Using quotes around values in .env: CLOUDFLARE_ACCOUNT_ID="abc123" (wrong)
  • βœ… No quotes: CLOUDFLARE_ACCOUNT_ID=abc123 (correct)
  • ❌ Missing .env entry after adding to config/database.php
  • ❌ Using old cached config after changing .env (run php artisan config:clear)
  • ❌ API token without sufficient permissions

πŸ”„ MySQL to D1 Migration

Migrate your existing MySQL database to Cloudflare D1 with one command!

Quick Migration

# Migrate all tables
php artisan d1:migrate-from-mysql

# Preview first (dry run)
php artisan d1:migrate-from-mysql --dry-run

# Migrate specific tables only
php artisan d1:migrate-from-mysql --tables=users,posts,comments

# Exclude specific tables
php artisan d1:migrate-from-mysql --exclude=logs,cache

What Gets Migrated

βœ… Schema Conversion

  • All MySQL data types β†’ SQLite equivalents
  • AUTO_INCREMENT β†’ AUTOINCREMENT
  • ENUM β†’ TEXT with CHECK constraints
  • Foreign keys with CASCADE actions
  • Indexes (regular and UNIQUE)

βœ… Data Migration

  • Memory-efficient chunked export
  • Batch INSERT operations (33x faster)
  • Progress tracking
  • Automatic validation

Migration Features

  • One-Command Migration: Complete database migration in a single Artisan command
  • Selective Migration: Choose which tables to migrate with --tables or --exclude
  • Structure Only: Migrate schema without data using --structure-only
  • Data Only: Migrate data into existing tables using --data-only
  • Dry Run: Preview migration plan without executing
  • Validation: Automatic row count and data integrity verification

Complete Migration Guide

For detailed migration instructions, schema conversion details, troubleshooting, and best practices, see MIGRATION_GUIDE.md.

πŸ§ͺ Testing

composer test

🀝 Contributing

Contributions are welcome! Please:

  1. 🍴 Fork the repository
  2. 🌿 Create a feature branch
  3. βœ… Add tests for new functionality
  4. πŸš€ Submit a pull request

πŸ“„ License

MIT License - see LICENSE file

Made with πŸ’™πŸ’› using Laravel and Cloudflare D1