<?php

namespace App\Enums;

enum StoreUserRole: string
{
    case Owner = 'owner';
    case Admin = 'admin';
    case Staff = 'staff';
    case Support = 'support';

    public function canManageStore(): bool
    {
        return in_array($this, [self::Owner, self::Admin], true);
    }

    public function canDeleteStore(): bool
    {
        return $this === self::Owner;
    }

    public function canManageProducts(): bool
    {
        return in_array($this, [self::Owner, self::Admin, self::Staff], true);
    }

    public function canDeleteProducts(): bool
    {
        return in_array($this, [self::Owner, self::Admin], true);
    }

    public function canProcessRefunds(): bool
    {
        return in_array($this, [self::Owner, self::Admin], true);
    }

    public function canWriteOrders(): bool
    {
        return in_array($this, [self::Owner, self::Admin, self::Staff], true);
    }

    public function canViewAnalytics(): bool
    {
        return in_array($this, [self::Owner, self::Admin, self::Staff], true);
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/StoreUserRole.php

<?php

namespace App\Enums;

enum StoreUserRole: string
{
    case Owner = 'owner';
    case Admin = 'admin';
    case Staff = 'staff';
    case Support = 'support';

    public function canManageStore(): bool
    {
        return in_array($this, [self::Owner, self::Admin], true);
    }

    public function canDeleteStore(): bool
    {
        return $this === self::Owner;
    }

    public function canManageProducts(): bool
    {
        return in_array($this, [self::Owner, self::Admin, self::Staff], true);
    }

    public function canDeleteProducts(): bool
    {
        return in_array($this, [self::Owner, self::Admin], true);
    }

    public function canProcessRefunds(): bool
    {
        return in_array($this, [self::Owner, self::Admin], true);
    }

    public function canWriteOrders(): bool
    {
        return in_array($this, [self::Owner, self::Admin, self::Staff], true);
    }

    public function canViewAnalytics(): bool
    {
        return in_array($this, [self::Owner, self::Admin, self::Staff], true);
    }
}


Write

<?php

namespace App\Enums;

enum StoreDomainType: string
{
    case Storefront = 'storefront';
    case Admin = 'admin';
    case Api = 'api';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/StoreDomainType.php

<?php

namespace App\Enums;

enum StoreDomainType: string
{
    case Storefront = 'storefront';
    case Admin = 'admin';
    case Api = 'api';
}


<?php

namespace App\Enums;

enum ProductStatus: string
{
    case Draft = 'draft';
    case Active = 'active';
    case Archived = 'archived';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/ProductStatus.php

<?php

namespace App\Enums;

enum ProductStatus: string
{
    case Draft = 'draft';
    case Active = 'active';
    case Archived = 'archived';
}


<?php

namespace App\Enums;

enum VariantStatus: string
{
    case Active = 'active';
    case Archived = 'archived';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/VariantStatus.php

<?php

namespace App\Enums;

enum VariantStatus: string
{
    case Active = 'active';
    case Archived = 'archived';
}


<?php

namespace App\Enums;

enum CollectionStatus: string
{
    case Draft = 'draft';
    case Active = 'active';
    case Archived = 'archived';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/CollectionStatus.php

<?php

namespace App\Enums;

enum CollectionStatus: string
{
    case Draft = 'draft';
    case Active = 'active';
    case Archived = 'archived';
}


<?php

namespace App\Enums;

enum CollectionType: string
{
    case Manual = 'manual';
    case Rule = 'rule';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/CollectionType.php

<?php

namespace App\Enums;

enum CollectionType: string
{
    case Manual = 'manual';
    case Rule = 'rule';
}


<?php

namespace App\Enums;

enum MediaType: string
{
    case Image = 'image';
    case Video = 'video';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/MediaType.php

<?php

namespace App\Enums;

enum MediaType: string
{
    case Image = 'image';
    case Video = 'video';
}


<?php

namespace App\Enums;

enum MediaStatus: string
{
    case Processing = 'processing';
    case Ready = 'ready';
    case Failed = 'failed';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/MediaStatus.php

<?php

namespace App\Enums;

enum MediaStatus: string
{
    case Processing = 'processing';
    case Ready = 'ready';
    case Failed = 'failed';
}


<?php

namespace App\Enums;

enum InventoryPolicy: string
{
    case Deny = 'deny';
    case Continue = 'continue';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/InventoryPolicy.php

<?php

namespace App\Enums;

enum InventoryPolicy: string
{
    case Deny = 'deny';
    case Continue = 'continue';
}


<?php

namespace App\Enums;

enum ThemeStatus: string
{
    case Draft = 'draft';
    case Published = 'published';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/ThemeStatus.php

<?php

namespace App\Enums;

enum ThemeStatus: string
{
    case Draft = 'draft';
    case Published = 'published';
}


<?php

namespace App\Enums;

enum PageStatus: string
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/PageStatus.php

<?php

namespace App\Enums;

enum PageStatus: string
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';
}


<?php

namespace App\Enums;

enum NavigationItemType: string
{
    case Link = 'link';
    case Page = 'page';
    case Collection = 'collection';
    case Product = 'product';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/NavigationItemType.php

<?php

namespace App\Enums;

enum NavigationItemType: string
{
    case Link = 'link';
    case Page = 'page';
    case Collection = 'collection';
    case Product = 'product';
}


<?php

namespace App\Enums;

enum CartStatus: string
{
    case Active = 'active';
    case Converted = 'converted';
    case Abandoned = 'abandoned';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/CartStatus.php

<?php

namespace App\Enums;

enum CartStatus: string
{
    case Active = 'active';
    case Converted = 'converted';
    case Abandoned = 'abandoned';
}


<?php

namespace App\Enums;

enum CheckoutStatus: string
{
    case Started = 'started';
    case Addressed = 'addressed';
    case ShippingSelected = 'shipping_selected';
    case PaymentPending = 'payment_pending';
    case Completed = 'completed';
    case Expired = 'expired';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/CheckoutStatus.php

<?php

namespace App\Enums;

enum CheckoutStatus: string
{
    case Started = 'started';
    case Addressed = 'addressed';
    case ShippingSelected = 'shipping_selected';
    case PaymentPending = 'payment_pending';
    case Completed = 'completed';
    case Expired = 'expired';
}


<?php

namespace App\Enums;

enum DiscountType: string
{
    case Code = 'code';
    case Automatic = 'automatic';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/DiscountType.php

<?php

namespace App\Enums;

enum DiscountType: string
{
    case Code = 'code';
    case Automatic = 'automatic';
}


<?php

namespace App\Enums;

enum DiscountValueType: string
{
    case Percent = 'percent';
    case Fixed = 'fixed';
    case FreeShipping = 'free_shipping';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/DiscountValueType.php

<?php

namespace App\Enums;

enum DiscountValueType: string
{
    case Percent = 'percent';
    case Fixed = 'fixed';
    case FreeShipping = 'free_shipping';
}


<?php

namespace App\Enums;

enum DiscountStatus: string
{
    case Draft = 'draft';
    case Active = 'active';
    case Expired = 'expired';
    case Disabled = 'disabled';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/DiscountStatus.php

<?php

namespace App\Enums;

enum DiscountStatus: string
{
    case Draft = 'draft';
    case Active = 'active';
    case Expired = 'expired';
    case Disabled = 'disabled';
}


<?php

namespace App\Enums;

enum ShippingRateType: string
{
    case Flat = 'flat';
    case Weight = 'weight';
    case Price = 'price';
    case Carrier = 'carrier';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/ShippingRateType.php

<?php

namespace App\Enums;

enum ShippingRateType: string
{
    case Flat = 'flat';
    case Weight = 'weight';
    case Price = 'price';
    case Carrier = 'carrier';
}


<?php

namespace App\Enums;

enum TaxMode: string
{
    case Manual = 'manual';
    case Provider = 'provider';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/TaxMode.php

<?php

namespace App\Enums;

enum TaxMode: string
{
    case Manual = 'manual';
    case Provider = 'provider';
}


<?php

namespace App\Enums;

enum OrderStatus: string
{
    case Open = 'open';
    case Closed = 'closed';
    case Cancelled = 'cancelled';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/OrderStatus.php

<?php

namespace App\Enums;

enum OrderStatus: string
{
    case Open = 'open';
    case Closed = 'closed';
    case Cancelled = 'cancelled';
}


<?php

namespace App\Enums;

enum FinancialStatus: string
{
    case Pending = 'pending';
    case Authorized = 'authorized';
    case Paid = 'paid';
    case PartiallyRefunded = 'partially_refunded';
    case Refunded = 'refunded';
    case Voided = 'voided';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/FinancialStatus.php

<?php

namespace App\Enums;

enum FinancialStatus: string
{
    case Pending = 'pending';
    case Authorized = 'authorized';
    case Paid = 'paid';
    case PartiallyRefunded = 'partially_refunded';
    case Refunded = 'refunded';
    case Voided = 'voided';
}


<?php

namespace App\Enums;

enum FulfillmentStatus: string
{
    case Unfulfilled = 'unfulfilled';
    case Partial = 'partial';
    case Fulfilled = 'fulfilled';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/FulfillmentStatus.php

<?php

namespace App\Enums;

enum FulfillmentStatus: string
{
    case Unfulfilled = 'unfulfilled';
    case Partial = 'partial';
    case Fulfilled = 'fulfilled';
}


<?php

namespace App\Enums;

enum FulfillmentShipmentStatus: string
{
    case Pending = 'pending';
    case Shipped = 'shipped';
    case Delivered = 'delivered';
    case Cancelled = 'cancelled';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/FulfillmentShipmentStatus.php

<?php

namespace App\Enums;

enum FulfillmentShipmentStatus: string
{
    case Pending = 'pending';
    case Shipped = 'shipped';
    case Delivered = 'delivered';
    case Cancelled = 'cancelled';
}


<?php

namespace App\Enums;

enum PaymentMethod: string
{
    case CreditCard = 'credit_card';
    case PayPal = 'paypal';
    case BankTransfer = 'bank_transfer';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/PaymentMethod.php

<?php

namespace App\Enums;

enum PaymentMethod: string
{
    case CreditCard = 'credit_card';
    case PayPal = 'paypal';
    case BankTransfer = 'bank_transfer';
}


<?php

namespace App\Enums;

enum PaymentStatus: string
{
    case Pending = 'pending';
    case Authorized = 'authorized';
    case Captured = 'captured';
    case Failed = 'failed';
    case Voided = 'voided';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/PaymentStatus.php

<?php

namespace App\Enums;

enum PaymentStatus: string
{
    case Pending = 'pending';
    case Authorized = 'authorized';
    case Captured = 'captured';
    case Failed = 'failed';
    case Voided = 'voided';
}


<?php

namespace App\Enums;

enum RefundStatus: string
{
    case Pending = 'pending';
    case Succeeded = 'succeeded';
    case Failed = 'failed';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/RefundStatus.php

<?php

namespace App\Enums;

enum RefundStatus: string
{
    case Pending = 'pending';
    case Succeeded = 'succeeded';
    case Failed = 'failed';
}


<?php

namespace App\Enums;

enum AnalyticsEventType: string
{
    case PageView = 'page_view';
    case ProductView = 'product_view';
    case AddToCart = 'add_to_cart';
    case CheckoutStarted = 'checkout_started';
    case CheckoutCompleted = 'checkout_completed';
    case Search = 'search';
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Enums/AnalyticsEventType.php

<?php

namespace App\Enums;

enum AnalyticsEventType: string
{
    case PageView = 'page_view';
    case ProductView = 'product_view';
    case AddToCart = 'add_to_cart';
    case CheckoutStarted = 'checkout_started';
    case CheckoutCompleted = 'checkout_completed';
    case Search = 'search';
}


Phase 6: Customer accounts

Customer login, register, forgot password, dashboard, orders list, order detail, addresses. Livewire components for storefront account.

Phase 7: Admin panel

Admin Dashboard, Products (list/create/edit), Collections, Inventory, Orders, Customers, Discounts, Settings (shipping, taxes, general), Pages, Navigation, Themes.

Phase 1: Foundation — migrations, enums, models, tenancy, auth

Create config updates, all foundation migrations (organizations, stores, store_domains, store_users, store_settings, customers, customer_addresses), enums, models with BelongsToStore trait + StoreScope, ResolveStore middleware, admin auth login/logout, customer auth provider.

Phase 2: Catalog — products, variants, inventory, collections, media

Migrations + models + services for products, product_options, product_option_values, product_variants, variant_option_values, inventory_items, collections, collection_products, product_media. Includes ProductService, InventoryService, HandleGenerator.

Phase 3: Themes + storefront layout

Theme, theme_files, theme_settings, pages, navigation_menus, navigation_items migrations and models. Blade storefront layout + Livewire components (Home, Collections, Products, Cart, Search, Pages).

Phase 8-10: Search (FTS), analytics, apps/webhooks stubs

SQLite FTS5 search index, analytics event tracking + daily rollups, apps/installations/webhook_subscriptions/webhook_deliveries tables and admin UI.

Phase 4: Cart, checkout, discounts, shipping, taxes

Migrations + models for carts, cart_lines, checkouts, shipping_zones, shipping_rates, tax_settings, discounts. Services: CartService, PricingService, ShippingService, TaxService, DiscountService.

Phase 5: Payments, orders, fulfillment

Migrations + models for orders, order_lines, payments, refunds, fulfillments, fulfillment_lines. MockPaymentProvider, OrderService, CheckoutCompletionService, FulfillmentService, RefundService.

Phase 11-12: Seeders, Pest tests, Playwright E2E

Database seeders (demo store, demo products, demo orders). Pest unit + feature tests for each module. Playwright browser tests for customer journey + admin operations.


Undefined type 'App\Models\Customer'.


Your mission is to implement an entire shop system based on the specifications im specs/*. You must do in one go without stopping. You must use team mode! You must test everything via Pest (unit, and…


Enums done. Now all foundation migrations in a single batch.


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('organizations', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('billing_email');
            $table->timestamps();

            $table->index('billing_email', 'idx_organizations_billing_email');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('organizations');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000001_create_organizations_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('organizations', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('billing_email');
            $table->timestamps();

            $table->index('billing_email', 'idx_organizations_billing_email');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('organizations');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('stores', function (Blueprint $table) {
            $table->id();
            $table->foreignId('organization_id')->constrained()->cascadeOnDelete();
            $table->string('name');
            $table->string('handle')->unique();
            $table->string('status')->default('active');
            $table->string('default_currency', 3)->default('USD');
            $table->string('default_locale', 10)->default('en');
            $table->string('timezone')->default('UTC');
            $table->timestamps();

            $table->index('status', 'idx_stores_status');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('stores');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000002_create_stores_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('stores', function (Blueprint $table) {
            $table->id();
            $table->foreignId('organization_id')->constrained()->cascadeOnDelete();
            $table->string('name');
            $table->string('handle')->unique();
            $table->string('status')->default('active');
            $table->string('default_currency', 3)->default('USD');
            $table->string('default_locale', 10)->default('en');
            $table->string('timezone')->default('UTC');
            $table->timestamps();

            $table->index('status', 'idx_stores_status');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('stores');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('store_domains', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('hostname')->unique();
            $table->string('type')->default('storefront');
            $table->boolean('is_primary')->default(false);
            $table->string('tls_mode')->default('managed');
            $table->timestamp('created_at')->nullable();

            $table->index(['store_id', 'is_primary'], 'idx_store_domains_store_primary');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('store_domains');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000003_create_store_domains_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('store_domains', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('hostname')->unique();
            $table->string('type')->default('storefront');
            $table->boolean('is_primary')->default(false);
            $table->string('tls_mode')->default('managed');
            $table->timestamp('created_at')->nullable();

            $table->index(['store_id', 'is_primary'], 'idx_store_domains_store_primary');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('store_domains');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('status')->default('active')->after('password');
            $table->timestamp('last_login_at')->nullable()->after('email_verified_at');

            $table->index('status', 'idx_users_status');
        });
    }

    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropIndex('idx_users_status');
            $table->dropColumn(['status', 'last_login_at']);
        });
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000004_extend_users_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('status')->default('active')->after('password');
            $table->timestamp('last_login_at')->nullable()->after('email_verified_at');

            $table->index('status', 'idx_users_status');
        });
    }

    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropIndex('idx_users_status');
            $table->dropColumn(['status', 'last_login_at']);
        });
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('store_users', function (Blueprint $table) {
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->string('role')->default('staff');
            $table->timestamp('created_at')->nullable();

            $table->primary(['store_id', 'user_id']);
            $table->index('user_id', 'idx_store_users_user_id');
            $table->index(['store_id', 'role'], 'idx_store_users_role');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('store_users');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000005_create_store_users_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('store_users', function (Blueprint $table) {
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->string('role')->default('staff');
            $table->timestamp('created_at')->nullable();

            $table->primary(['store_id', 'user_id']);
            $table->index('user_id', 'idx_store_users_user_id');
            $table->index(['store_id', 'role'], 'idx_store_users_role');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('store_users');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('store_settings', function (Blueprint $table) {
            $table->foreignId('store_id')->primary()->constrained()->cascadeOnDelete();
            $table->text('settings_json')->default('{}');
            $table->timestamp('updated_at')->nullable();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('store_settings');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000006_create_store_settings_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('store_settings', function (Blueprint $table) {
            $table->foreignId('store_id')->primary()->constrained()->cascadeOnDelete();
            $table->text('settings_json')->default('{}');
            $table->timestamp('updated_at')->nullable();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('store_settings');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('customers', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('email');
            $table->string('password_hash')->nullable();
            $table->string('name')->nullable();
            $table->boolean('marketing_opt_in')->default(false);
            $table->timestamp('email_verified_at')->nullable();
            $table->string('remember_token', 100)->nullable();
            $table->timestamps();

            $table->unique(['store_id', 'email'], 'idx_customers_store_email');
        });

        Schema::create('customer_addresses', function (Blueprint $table) {
            $table->id();
            $table->foreignId('customer_id')->constrained()->cascadeOnDelete();
            $table->string('label')->nullable();
            $table->text('address_json');
            $table->boolean('is_default')->default(false);

            $table->index(['customer_id', 'is_default'], 'idx_customer_addresses_default');
        });

        Schema::create('customer_password_reset_tokens', function (Blueprint $table) {
            $table->string('email')->primary();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('customer_password_reset_tokens');
        Schema::dropIfExists('customer_addresses');
        Schema::dropIfExists('customers');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000010_create_customers_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('customers', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('email');
            $table->string('password_hash')->nullable();
            $table->string('name')->nullable();
            $table->boolean('marketing_opt_in')->default(false);
            $table->timestamp('email_verified_at')->nullable();
            $table->string('remember_token', 100)->nullable();
            $table->timestamps();

            $table->unique(['store_id', 'email'], 'idx_customers_store_email');
        });

        Schema::create('customer_addresses', function (Blueprint $table) {
            $table->id();
            $table->foreignId('customer_id')->constrained()->cascadeOnDelete();
            $table->string('label')->nullable();
            $table->text('address_json');
            $table->boolean('is_default')->default(false);

            $table->index(['customer_id', 'is_default'], 'idx_customer_addresses_default');
        });

        Schema::create('customer_password_reset_tokens', function (Blueprint $table) {
            $table->string('email')->primary();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('customer_password_reset_tokens');
        Schema::dropIfExists('customer_addresses');
        Schema::dropIfExists('customers');
    }
};


Now the catalog migrations (products, variants, inventory, collections, media).


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('title');
            $table->string('handle');
            $table->string('status')->default('draft');
            $table->text('description_html')->nullable();
            $table->string('vendor')->nullable();
            $table->string('product_type')->nullable();
            $table->text('tags')->default('[]');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();

            $table->unique(['store_id', 'handle'], 'idx_products_store_handle');
            $table->index(['store_id', 'status'], 'idx_products_store_status');
            $table->index(['store_id', 'published_at'], 'idx_products_published_at');
            $table->index(['store_id', 'vendor'], 'idx_products_vendor');
            $table->index(['store_id', 'product_type'], 'idx_products_product_type');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('products');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000020_create_products_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('title');
            $table->string('handle');
            $table->string('status')->default('draft');
            $table->text('description_html')->nullable();
            $table->string('vendor')->nullable();
            $table->string('product_type')->nullable();
            $table->text('tags')->default('[]');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();

            $table->unique(['store_id', 'handle'], 'idx_products_store_handle');
            $table->index(['store_id', 'status'], 'idx_products_store_status');
            $table->index(['store_id', 'published_at'], 'idx_products_published_at');
            $table->index(['store_id', 'vendor'], 'idx_products_vendor');
            $table->index(['store_id', 'product_type'], 'idx_products_product_type');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('products');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('product_options', function (Blueprint $table) {
            $table->id();
            $table->foreignId('product_id')->constrained()->cascadeOnDelete();
            $table->string('name');
            $table->integer('position')->default(0);

            $table->unique(['product_id', 'position'], 'idx_product_options_product_position');
        });

        Schema::create('product_option_values', function (Blueprint $table) {
            $table->id();
            $table->foreignId('product_option_id')->constrained()->cascadeOnDelete();
            $table->string('value');
            $table->integer('position')->default(0);

            $table->unique(['product_option_id', 'position'], 'idx_product_option_values_option_position');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('product_option_values');
        Schema::dropIfExists('product_options');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000021_create_product_options_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('product_options', function (Blueprint $table) {
            $table->id();
            $table->foreignId('product_id')->constrained()->cascadeOnDelete();
            $table->string('name');
            $table->integer('position')->default(0);

            $table->unique(['product_id', 'position'], 'idx_product_options_product_position');
        });

        Schema::create('product_option_values', function (Blueprint $table) {
            $table->id();
            $table->foreignId('product_option_id')->constrained()->cascadeOnDelete();
            $table->string('value');
            $table->integer('position')->default(0);

            $table->unique(['product_option_id', 'position'], 'idx_product_option_values_option_position');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('product_option_values');
        Schema::dropIfExists('product_options');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('product_variants', function (Blueprint $table) {
            $table->id();
            $table->foreignId('product_id')->constrained()->cascadeOnDelete();
            $table->string('sku')->nullable();
            $table->string('barcode')->nullable();
            $table->integer('price_amount')->default(0);
            $table->integer('compare_at_amount')->nullable();
            $table->string('currency', 3)->default('USD');
            $table->integer('weight_g')->nullable();
            $table->boolean('requires_shipping')->default(true);
            $table->boolean('is_default')->default(false);
            $table->integer('position')->default(0);
            $table->string('status')->default('active');
            $table->timestamps();

            $table->index('sku', 'idx_product_variants_sku');
            $table->index('barcode', 'idx_product_variants_barcode');
            $table->index(['product_id', 'position'], 'idx_product_variants_product_position');
            $table->index(['product_id', 'is_default'], 'idx_product_variants_product_default');
        });

        Schema::create('variant_option_values', function (Blueprint $table) {
            $table->foreignId('variant_id')->constrained('product_variants')->cascadeOnDelete();
            $table->foreignId('product_option_value_id')->constrained()->cascadeOnDelete();

            $table->primary(['variant_id', 'product_option_value_id']);
            $table->index('product_option_value_id', 'idx_variant_option_values_value_id');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('variant_option_values');
        Schema::dropIfExists('product_variants');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000022_create_product_variants_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('product_variants', function (Blueprint $table) {
            $table->id();
            $table->foreignId('product_id')->constrained()->cascadeOnDelete();
            $table->string('sku')->nullable();
            $table->string('barcode')->nullable();
            $table->integer('price_amount')->default(0);
            $table->integer('compare_at_amount')->nullable();
            $table->string('currency', 3)->default('USD');
            $table->integer('weight_g')->nullable();
            $table->boolean('requires_shipping')->default(true);
            $table->boolean('is_default')->default(false);
            $table->integer('position')->default(0);
            $table->string('status')->default('active');
            $table->timestamps();

            $table->index('sku', 'idx_product_variants_sku');
            $table->index('barcode', 'idx_product_variants_barcode');
            $table->index(['product_id', 'position'], 'idx_product_variants_product_position');
            $table->index(['product_id', 'is_default'], 'idx_product_variants_product_default');
        });

        Schema::create('variant_option_values', function (Blueprint $table) {
            $table->foreignId('variant_id')->constrained('product_variants')->cascadeOnDelete();
            $table->foreignId('product_option_value_id')->constrained()->cascadeOnDelete();

            $table->primary(['variant_id', 'product_option_value_id']);
            $table->index('product_option_value_id', 'idx_variant_option_values_value_id');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('variant_option_values');
        Schema::dropIfExists('product_variants');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('inventory_items', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('variant_id')->unique()->constrained('product_variants')->cascadeOnDelete();
            $table->integer('quantity_on_hand')->default(0);
            $table->integer('quantity_reserved')->default(0);
            $table->string('policy')->default('deny');

            $table->index('store_id', 'idx_inventory_items_store_id');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('inventory_items');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000023_create_inventory_items_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('inventory_items', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('variant_id')->unique()->constrained('product_variants')->cascadeOnDelete();
            $table->integer('quantity_on_hand')->default(0);
            $table->integer('quantity_reserved')->default(0);
            $table->string('policy')->default('deny');

            $table->index('store_id', 'idx_inventory_items_store_id');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('inventory_items');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('collections', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('title');
            $table->string('handle');
            $table->text('description_html')->nullable();
            $table->string('type')->default('manual');
            $table->text('rules_json')->nullable();
            $table->string('status')->default('draft');
            $table->timestamps();

            $table->unique(['store_id', 'handle'], 'idx_collections_store_handle');
            $table->index(['store_id', 'status'], 'idx_collections_store_status');
        });

        Schema::create('collection_products', function (Blueprint $table) {
            $table->foreignId('collection_id')->constrained()->cascadeOnDelete();
            $table->foreignId('product_id')->constrained()->cascadeOnDelete();
            $table->integer('position')->default(0);

            $table->primary(['collection_id', 'product_id']);
            $table->index('product_id', 'idx_collection_products_product_id');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('collection_products');
        Schema::dropIfExists('collections');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000024_create_collections_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('collections', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('title');
            $table->string('handle');
            $table->text('description_html')->nullable();
            $table->string('type')->default('manual');
            $table->text('rules_json')->nullable();
            $table->string('status')->default('draft');
            $table->timestamps();

            $table->unique(['store_id', 'handle'], 'idx_collections_store_handle');
            $table->index(['store_id', 'status'], 'idx_collections_store_status');
        });

        Schema::create('collection_products', function (Blueprint $table) {
            $table->foreignId('collection_id')->constrained()->cascadeOnDelete();
            $table->foreignId('product_id')->constrained()->cascadeOnDelete();
            $table->integer('position')->default(0);

            $table->primary(['collection_id', 'product_id']);
            $table->index('product_id', 'idx_collection_products_product_id');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('collection_products');
        Schema::dropIfExists('collections');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('product_media', function (Blueprint $table) {
            $table->id();
            $table->foreignId('product_id')->constrained()->cascadeOnDelete();
            $table->string('type')->default('image');
            $table->string('storage_key');
            $table->string('alt_text')->nullable();
            $table->integer('width')->nullable();
            $table->integer('height')->nullable();
            $table->string('mime_type')->nullable();
            $table->integer('byte_size')->nullable();
            $table->integer('position')->default(0);
            $table->string('status')->default('ready');
            $table->timestamp('created_at')->nullable();

            $table->index(['product_id', 'position'], 'idx_product_media_product_position');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('product_media');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000025_create_product_media_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('product_media', function (Blueprint $table) {
            $table->id();
            $table->foreignId('product_id')->constrained()->cascadeOnDelete();
            $table->string('type')->default('image');
            $table->string('storage_key');
            $table->string('alt_text')->nullable();
            $table->integer('width')->nullable();
            $table->integer('height')->nullable();
            $table->string('mime_type')->nullable();
            $table->integer('byte_size')->nullable();
            $table->integer('position')->default(0);
            $table->string('status')->default('ready');
            $table->timestamp('created_at')->nullable();

            $table->index(['product_id', 'position'], 'idx_product_media_product_position');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('product_media');
    }
};


Phase 6: Customer accounts

Phase 7: Admin panel

Phase 3: Themes + storefront layout

Phase 5: Payments, orders, fulfillment


Continuing with themes, pages, navigation, cart, checkout, shipping, taxes, discounts, orders, payments, fulfillments migrations.


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('themes', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('name');
            $table->string('version')->default('1.0.0');
            $table->string('status')->default('draft');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();

            $table->index(['store_id', 'status'], 'idx_themes_store_status');
        });

        Schema::create('theme_files', function (Blueprint $table) {
            $table->id();
            $table->foreignId('theme_id')->constrained()->cascadeOnDelete();
            $table->string('path');
            $table->string('storage_key');
            $table->string('sha256', 64)->nullable();
            $table->integer('byte_size')->nullable();

            $table->unique(['theme_id', 'path'], 'idx_theme_files_theme_path');
        });

        Schema::create('theme_settings', function (Blueprint $table) {
            $table->foreignId('theme_id')->primary()->constrained()->cascadeOnDelete();
            $table->text('settings_json')->default('{}');
            $table->timestamp('updated_at')->nullable();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('theme_settings');
        Schema::dropIfExists('theme_files');
        Schema::dropIfExists('themes');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000030_create_themes_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('themes', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('name');
            $table->string('version')->default('1.0.0');
            $table->string('status')->default('draft');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();

            $table->index(['store_id', 'status'], 'idx_themes_store_status');
        });

        Schema::create('theme_files', function (Blueprint $table) {
            $table->id();
            $table->foreignId('theme_id')->constrained()->cascadeOnDelete();
            $table->string('path');
            $table->string('storage_key');
            $table->string('sha256', 64)->nullable();
            $table->integer('byte_size')->nullable();

            $table->unique(['theme_id', 'path'], 'idx_theme_files_theme_path');
        });

        Schema::create('theme_settings', function (Blueprint $table) {
            $table->foreignId('theme_id')->primary()->constrained()->cascadeOnDelete();
            $table->text('settings_json')->default('{}');
            $table->timestamp('updated_at')->nullable();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('theme_settings');
        Schema::dropIfExists('theme_files');
        Schema::dropIfExists('themes');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('pages', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('title');
            $table->string('handle');
            $table->text('body_html')->nullable();
            $table->string('status')->default('draft');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();

            $table->unique(['store_id', 'handle'], 'idx_pages_store_handle');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('pages');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000031_create_pages_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('pages', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('title');
            $table->string('handle');
            $table->text('body_html')->nullable();
            $table->string('status')->default('draft');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();

            $table->unique(['store_id', 'handle'], 'idx_pages_store_handle');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('pages');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('navigation_menus', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('handle');
            $table->string('title');

            $table->unique(['store_id', 'handle'], 'idx_navigation_menus_store_handle');
        });

        Schema::create('navigation_items', function (Blueprint $table) {
            $table->id();
            $table->foreignId('menu_id')->constrained('navigation_menus')->cascadeOnDelete();
            $table->foreignId('parent_id')->nullable()->constrained('navigation_items')->nullOnDelete();
            $table->string('type');
            $table->string('label');
            $table->string('url')->nullable();
            $table->unsignedBigInteger('resource_id')->nullable();
            $table->integer('position')->default(0);

            $table->index('menu_id', 'idx_navigation_items_menu_id');
            $table->index('parent_id', 'idx_navigation_items_parent_id');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('navigation_items');
        Schema::dropIfExists('navigation_menus');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000032_create_navigation_menus_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('navigation_menus', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('handle');
            $table->string('title');

            $table->unique(['store_id', 'handle'], 'idx_navigation_menus_store_handle');
        });

        Schema::create('navigation_items', function (Blueprint $table) {
            $table->id();
            $table->foreignId('menu_id')->constrained('navigation_menus')->cascadeOnDelete();
            $table->foreignId('parent_id')->nullable()->constrained('navigation_items')->nullOnDelete();
            $table->string('type');
            $table->string('label');
            $table->string('url')->nullable();
            $table->unsignedBigInteger('resource_id')->nullable();
            $table->integer('position')->default(0);

            $table->index('menu_id', 'idx_navigation_items_menu_id');
            $table->index('parent_id', 'idx_navigation_items_parent_id');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('navigation_items');
        Schema::dropIfExists('navigation_menus');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('carts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
            $table->string('currency', 3)->default('USD');
            $table->integer('cart_version')->default(1);
            $table->string('status')->default('active');
            $table->timestamps();

            $table->index(['store_id', 'customer_id'], 'idx_carts_store_customer');
            $table->index(['store_id', 'status'], 'idx_carts_store_status');
        });

        Schema::create('cart_lines', function (Blueprint $table) {
            $table->id();
            $table->foreignId('cart_id')->constrained()->cascadeOnDelete();
            $table->foreignId('variant_id')->constrained('product_variants')->cascadeOnDelete();
            $table->integer('quantity');
            $table->integer('unit_price_amount');
            $table->integer('line_subtotal_amount')->default(0);
            $table->integer('line_discount_amount')->default(0);
            $table->integer('line_total_amount')->default(0);

            $table->unique(['cart_id', 'variant_id'], 'idx_cart_lines_cart_variant');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('cart_lines');
        Schema::dropIfExists('carts');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000040_create_carts_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('carts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
            $table->string('currency', 3)->default('USD');
            $table->integer('cart_version')->default(1);
            $table->string('status')->default('active');
            $table->timestamps();

            $table->index(['store_id', 'customer_id'], 'idx_carts_store_customer');
            $table->index(['store_id', 'status'], 'idx_carts_store_status');
        });

        Schema::create('cart_lines', function (Blueprint $table) {
            $table->id();
            $table->foreignId('cart_id')->constrained()->cascadeOnDelete();
            $table->foreignId('variant_id')->constrained('product_variants')->cascadeOnDelete();
            $table->integer('quantity');
            $table->integer('unit_price_amount');
            $table->integer('line_subtotal_amount')->default(0);
            $table->integer('line_discount_amount')->default(0);
            $table->integer('line_total_amount')->default(0);

            $table->unique(['cart_id', 'variant_id'], 'idx_cart_lines_cart_variant');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('cart_lines');
        Schema::dropIfExists('carts');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('checkouts', function (Blueprint $table) {
            $table->id();
            $table->string('token', 64)->unique();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('cart_id')->constrained()->cascadeOnDelete();
            $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
            $table->string('status')->default('started');
            $table->string('payment_method')->nullable();
            $table->string('email')->nullable();
            $table->text('shipping_address_json')->nullable();
            $table->text('billing_address_json')->nullable();
            $table->unsignedBigInteger('shipping_method_id')->nullable();
            $table->string('discount_code')->nullable();
            $table->text('tax_provider_snapshot_json')->nullable();
            $table->text('totals_json')->nullable();
            $table->timestamp('expires_at')->nullable();
            $table->timestamps();

            $table->index(['store_id', 'status'], 'idx_checkouts_store_status');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('checkouts');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000041_create_checkouts_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('checkouts', function (Blueprint $table) {
            $table->id();
            $table->string('token', 64)->unique();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('cart_id')->constrained()->cascadeOnDelete();
            $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
            $table->string('status')->default('started');
            $table->string('payment_method')->nullable();
            $table->string('email')->nullable();
            $table->text('shipping_address_json')->nullable();
            $table->text('billing_address_json')->nullable();
            $table->unsignedBigInteger('shipping_method_id')->nullable();
            $table->string('discount_code')->nullable();
            $table->text('tax_provider_snapshot_json')->nullable();
            $table->text('totals_json')->nullable();
            $table->timestamp('expires_at')->nullable();
            $table->timestamps();

            $table->index(['store_id', 'status'], 'idx_checkouts_store_status');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('checkouts');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('shipping_zones', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('name');
            $table->text('countries_json')->default('[]');
            $table->text('regions_json')->default('[]');

            $table->index('store_id', 'idx_shipping_zones_store_id');
        });

        Schema::create('shipping_rates', function (Blueprint $table) {
            $table->id();
            $table->foreignId('zone_id')->constrained('shipping_zones')->cascadeOnDelete();
            $table->string('name');
            $table->string('type')->default('flat');
            $table->text('config_json')->default('{}');
            $table->boolean('is_active')->default(true);

            $table->index('zone_id', 'idx_shipping_rates_zone_id');
        });

        Schema::create('tax_settings', function (Blueprint $table) {
            $table->foreignId('store_id')->primary()->constrained()->cascadeOnDelete();
            $table->string('mode')->default('manual');
            $table->string('provider')->nullable();
            $table->boolean('prices_include_tax')->default(false);
            $table->text('config_json')->default('{}');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('tax_settings');
        Schema::dropIfExists('shipping_rates');
        Schema::dropIfExists('shipping_zones');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000042_create_shipping_tables.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('shipping_zones', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('name');
            $table->text('countries_json')->default('[]');
            $table->text('regions_json')->default('[]');

            $table->index('store_id', 'idx_shipping_zones_store_id');
        });

        Schema::create('shipping_rates', function (Blueprint $table) {
            $table->id();
            $table->foreignId('zone_id')->constrained('shipping_zones')->cascadeOnDelete();
            $table->string('name');
            $table->string('type')->default('flat');
            $table->text('config_json')->default('{}');
            $table->boolean('is_active')->default(true);

            $table->index('zone_id', 'idx_shipping_rates_zone_id');
        });

        Schema::create('tax_settings', function (Blueprint $table) {
            $table->foreignId('store_id')->primary()->constrained()->cascadeOnDelete();
            $table->string('mode')->default('manual');
            $table->string('provider')->nullable();
            $table->boolean('prices_include_tax')->default(false);
            $table->text('config_json')->default('{}');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('tax_settings');
        Schema::dropIfExists('shipping_rates');
        Schema::dropIfExists('shipping_zones');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('discounts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('type')->default('code');
            $table->string('code')->nullable();
            $table->string('value_type');
            $table->integer('value_amount');
            $table->timestamp('starts_at')->nullable();
            $table->timestamp('ends_at')->nullable();
            $table->integer('usage_limit')->nullable();
            $table->integer('usage_count')->default(0);
            $table->text('rules_json')->default('{}');
            $table->string('status')->default('draft');
            $table->timestamps();

            $table->unique(['store_id', 'code'], 'idx_discounts_store_code');
            $table->index(['store_id', 'status'], 'idx_discounts_store_status');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('discounts');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000043_create_discounts_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('discounts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('type')->default('code');
            $table->string('code')->nullable();
            $table->string('value_type');
            $table->integer('value_amount');
            $table->timestamp('starts_at')->nullable();
            $table->timestamp('ends_at')->nullable();
            $table->integer('usage_limit')->nullable();
            $table->integer('usage_count')->default(0);
            $table->text('rules_json')->default('{}');
            $table->string('status')->default('draft');
            $table->timestamps();

            $table->unique(['store_id', 'code'], 'idx_discounts_store_code');
            $table->index(['store_id', 'status'], 'idx_discounts_store_status');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('discounts');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
            $table->string('order_number')->unique();
            $table->string('payment_method')->nullable();
            $table->string('status')->default('open');
            $table->string('financial_status')->default('pending');
            $table->string('fulfillment_status')->default('unfulfilled');
            $table->string('currency', 3)->default('USD');
            $table->integer('subtotal_amount')->default(0);
            $table->integer('discount_amount')->default(0);
            $table->integer('shipping_amount')->default(0);
            $table->integer('tax_amount')->default(0);
            $table->integer('total_amount')->default(0);
            $table->string('email')->nullable();
            $table->text('billing_address_json')->nullable();
            $table->text('shipping_address_json')->nullable();
            $table->timestamp('placed_at')->nullable();
            $table->timestamps();

            $table->index(['store_id', 'status'], 'idx_orders_store_status');
            $table->index(['store_id', 'financial_status'], 'idx_orders_store_financial');
            $table->index(['store_id', 'placed_at'], 'idx_orders_store_placed_at');
            $table->index(['store_id', 'customer_id'], 'idx_orders_store_customer');
        });

        Schema::create('order_lines', function (Blueprint $table) {
            $table->id();
            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
            $table->foreignId('product_id')->nullable()->constrained()->nullOnDelete();
            $table->foreignId('variant_id')->nullable()->constrained('product_variants')->nullOnDelete();
            $table->string('title_snapshot');
            $table->string('sku_snapshot')->nullable();
            $table->integer('quantity');
            $table->integer('unit_price_amount');
            $table->integer('total_amount');
            $table->text('tax_lines_json')->default('[]');
            $table->text('discount_allocations_json')->default('[]');

            $table->index('order_id', 'idx_order_lines_order_id');
        });

        Schema::create('payments', function (Blueprint $table) {
            $table->id();
            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
            $table->string('provider')->default('mock');
            $table->string('method');
            $table->string('provider_payment_id')->nullable();
            $table->string('status')->default('pending');
            $table->integer('amount');
            $table->string('currency', 3)->default('USD');
            $table->text('raw_json_encrypted')->nullable();
            $table->timestamp('created_at')->nullable();

            $table->index('order_id', 'idx_payments_order_id');
            $table->index('status', 'idx_payments_status');
        });

        Schema::create('refunds', function (Blueprint $table) {
            $table->id();
            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
            $table->foreignId('payment_id')->nullable()->constrained()->nullOnDelete();
            $table->integer('amount');
            $table->string('reason')->nullable();
            $table->string('status')->default('pending');
            $table->string('provider_refund_id')->nullable();
            $table->timestamp('created_at')->nullable();

            $table->index('order_id', 'idx_refunds_order_id');
        });

        Schema::create('fulfillments', function (Blueprint $table) {
            $table->id();
            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
            $table->string('status')->default('pending');
            $table->string('tracking_company')->nullable();
            $table->string('tracking_number')->nullable();
            $table->string('tracking_url')->nullable();
            $table->timestamp('shipped_at')->nullable();
            $table->timestamp('created_at')->nullable();

            $table->index('order_id', 'idx_fulfillments_order_id');
        });

        Schema::create('fulfillment_lines', function (Blueprint $table) {
            $table->id();
            $table->foreignId('fulfillment_id')->constrained()->cascadeOnDelete();
            $table->foreignId('order_line_id')->constrained()->cascadeOnDelete();
            $table->integer('quantity');

            $table->unique(['fulfillment_id', 'order_line_id'], 'idx_fulfillment_lines_unique');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('fulfillment_lines');
        Schema::dropIfExists('fulfillments');
        Schema::dropIfExists('refunds');
        Schema::dropIfExists('payments');
        Schema::dropIfExists('order_lines');
        Schema::dropIfExists('orders');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000050_create_orders_table.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
            $table->string('order_number')->unique();
            $table->string('payment_method')->nullable();
            $table->string('status')->default('open');
            $table->string('financial_status')->default('pending');
            $table->string('fulfillment_status')->default('unfulfilled');
            $table->string('currency', 3)->default('USD');
            $table->integer('subtotal_amount')->default(0);
            $table->integer('discount_amount')->default(0);
            $table->integer('shipping_amount')->default(0);
            $table->integer('tax_amount')->default(0);
            $table->integer('total_amount')->default(0);
            $table->string('email')->nullable();
            $table->text('billing_address_json')->nullable();
            $table->text('shipping_address_json')->nullable();
            $table->timestamp('placed_at')->nullable();
            $table->timestamps();

            $table->index(['store_id', 'status'], 'idx_orders_store_status');
            $table->index(['store_id', 'financial_status'], 'idx_orders_store_financial');
            $table->index(['store_id', 'placed_at'], 'idx_orders_store_placed_at');
            $table->index(['store_id', 'customer_id'], 'idx_orders_store_customer');
        });

        Schema::create('order_lines', function (Blueprint $table) {
            $table->id();
            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
            $table->foreignId('product_id')->nullable()->constrained()->nullOnDelete();
            $table->foreignId('variant_id')->nullable()->constrained('product_variants')->nullOnDelete();
            $table->string('title_snapshot');
            $table->string('sku_snapshot')->nullable();
            $table->integer('quantity');
            $table->integer('unit_price_amount');
            $table->integer('total_amount');
            $table->text('tax_lines_json')->default('[]');
            $table->text('discount_allocations_json')->default('[]');

            $table->index('order_id', 'idx_order_lines_order_id');
        });

        Schema::create('payments', function (Blueprint $table) {
            $table->id();
            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
            $table->string('provider')->default('mock');
            $table->string('method');
            $table->string('provider_payment_id')->nullable();
            $table->string('status')->default('pending');
            $table->integer('amount');
            $table->string('currency', 3)->default('USD');
            $table->text('raw_json_encrypted')->nullable();
            $table->timestamp('created_at')->nullable();

            $table->index('order_id', 'idx_payments_order_id');
            $table->index('status', 'idx_payments_status');
        });

        Schema::create('refunds', function (Blueprint $table) {
            $table->id();
            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
            $table->foreignId('payment_id')->nullable()->constrained()->nullOnDelete();
            $table->integer('amount');
            $table->string('reason')->nullable();
            $table->string('status')->default('pending');
            $table->string('provider_refund_id')->nullable();
            $table->timestamp('created_at')->nullable();

            $table->index('order_id', 'idx_refunds_order_id');
        });

        Schema::create('fulfillments', function (Blueprint $table) {
            $table->id();
            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
            $table->string('status')->default('pending');
            $table->string('tracking_company')->nullable();
            $table->string('tracking_number')->nullable();
            $table->string('tracking_url')->nullable();
            $table->timestamp('shipped_at')->nullable();
            $table->timestamp('created_at')->nullable();

            $table->index('order_id', 'idx_fulfillments_order_id');
        });

        Schema::create('fulfillment_lines', function (Blueprint $table) {
            $table->id();
            $table->foreignId('fulfillment_id')->constrained()->cascadeOnDelete();
            $table->foreignId('order_line_id')->constrained()->cascadeOnDelete();
            $table->integer('quantity');

            $table->unique(['fulfillment_id', 'order_line_id'], 'idx_fulfillment_lines_unique');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('fulfillment_lines');
        Schema::dropIfExists('fulfillments');
        Schema::dropIfExists('refunds');
        Schema::dropIfExists('payments');
        Schema::dropIfExists('order_lines');
        Schema::dropIfExists('orders');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('analytics_events', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('type');
            $table->string('session_id', 64)->nullable();
            $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
            $table->text('properties_json')->default('{}');
            $table->string('client_event_id', 64)->nullable();
            $table->timestamp('occurred_at');
            $table->timestamp('created_at')->nullable();

            $table->index(['store_id', 'type', 'occurred_at'], 'idx_analytics_events_type_time');
            $table->unique('client_event_id', 'idx_analytics_events_client_id');
        });

        Schema::create('analytics_daily', function (Blueprint $table) {
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->date('date');
            $table->integer('orders_count')->default(0);
            $table->integer('revenue_amount')->default(0);
            $table->integer('aov_amount')->default(0);
            $table->integer('visits_count')->default(0);
            $table->integer('add_to_cart_count')->default(0);
            $table->integer('checkout_started_count')->default(0);
            $table->integer('checkout_completed_count')->default(0);

            $table->primary(['store_id', 'date']);
        });

        Schema::create('search_queries', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('query');
            $table->integer('results_count')->default(0);
            $table->timestamp('created_at')->nullable();

            $table->index(['store_id', 'created_at'], 'idx_search_queries_store_time');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('search_queries');
        Schema::dropIfExists('analytics_daily');
        Schema::dropIfExists('analytics_events');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000060_create_analytics_tables.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('analytics_events', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('type');
            $table->string('session_id', 64)->nullable();
            $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
            $table->text('properties_json')->default('{}');
            $table->string('client_event_id', 64)->nullable();
            $table->timestamp('occurred_at');
            $table->timestamp('created_at')->nullable();

            $table->index(['store_id', 'type', 'occurred_at'], 'idx_analytics_events_type_time');
            $table->unique('client_event_id', 'idx_analytics_events_client_id');
        });

        Schema::create('analytics_daily', function (Blueprint $table) {
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->date('date');
            $table->integer('orders_count')->default(0);
            $table->integer('revenue_amount')->default(0);
            $table->integer('aov_amount')->default(0);
            $table->integer('visits_count')->default(0);
            $table->integer('add_to_cart_count')->default(0);
            $table->integer('checkout_started_count')->default(0);
            $table->integer('checkout_completed_count')->default(0);

            $table->primary(['store_id', 'date']);
        });

        Schema::create('search_queries', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('query');
            $table->integer('results_count')->default(0);
            $table->timestamp('created_at')->nullable();

            $table->index(['store_id', 'created_at'], 'idx_search_queries_store_time');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('search_queries');
        Schema::dropIfExists('analytics_daily');
        Schema::dropIfExists('analytics_events');
    }
};


<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('apps', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('handle')->unique();
            $table->string('description')->nullable();
            $table->text('scopes_json')->default('[]');
            $table->timestamps();
        });

        Schema::create('app_installations', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('app_id')->constrained()->cascadeOnDelete();
            $table->string('status')->default('active');
            $table->text('scopes_granted_json')->default('[]');
            $table->timestamp('installed_at')->nullable();

            $table->unique(['store_id', 'app_id'], 'idx_app_installations_store_app');
        });

        Schema::create('webhook_subscriptions', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('app_installation_id')->nullable()->constrained('app_installations')->nullOnDelete();
            $table->string('topic');
            $table->string('target_url');
            $table->string('secret');
            $table->boolean('is_active')->default(true);
            $table->timestamps();

            $table->index(['store_id', 'topic'], 'idx_webhook_subs_store_topic');
        });

        Schema::create('webhook_deliveries', function (Blueprint $table) {
            $table->id();
            $table->foreignId('webhook_subscription_id')->constrained('webhook_subscriptions')->cascadeOnDelete();
            $table->string('topic');
            $table->text('payload_json');
            $table->integer('response_status')->nullable();
            $table->integer('attempts')->default(0);
            $table->string('status')->default('pending');
            $table->timestamp('delivered_at')->nullable();
            $table->timestamp('created_at')->nullable();

            $table->index('webhook_subscription_id', 'idx_webhook_deliveries_sub_id');
            $table->index('status', 'idx_webhook_deliveries_status');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('webhook_deliveries');
        Schema::dropIfExists('webhook_subscriptions');
        Schema::dropIfExists('app_installations');
        Schema::dropIfExists('apps');
    }
};


File created successfully at: /Users/fabianwesner/Herd/shop/database/migrations/2026_01_01_000070_create_apps_tables.php

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('apps', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('handle')->unique();
            $table->string('description')->nullable();
            $table->text('scopes_json')->default('[]');
            $table->timestamps();
        });

        Schema::create('app_installations', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('app_id')->constrained()->cascadeOnDelete();
            $table->string('status')->default('active');
            $table->text('scopes_granted_json')->default('[]');
            $table->timestamp('installed_at')->nullable();

            $table->unique(['store_id', 'app_id'], 'idx_app_installations_store_app');
        });

        Schema::create('webhook_subscriptions', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('app_installation_id')->nullable()->constrained('app_installations')->nullOnDelete();
            $table->string('topic');
            $table->string('target_url');
            $table->string('secret');
            $table->boolean('is_active')->default(true);
            $table->timestamps();

            $table->index(['store_id', 'topic'], 'idx_webhook_subs_store_topic');
        });

        Schema::create('webhook_deliveries', function (Blueprint $table) {
            $table->id();
            $table->foreignId('webhook_subscription_id')->constrained('webhook_subscriptions')->cascadeOnDelete();
            $table->string('topic');
            $table->text('payload_json');
            $table->integer('response_status')->nullable();
            $table->integer('attempts')->default(0);
            $table->string('status')->default('pending');
            $table->timestamp('delivered_at')->nullable();
            $table->timestamp('created_at')->nullable();

            $table->index('webhook_subscription_id', 'idx_webhook_deliveries_sub_id');
            $table->index('status', 'idx_webhook_deliveries_status');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('webhook_deliveries');
        Schema::dropIfExists('webhook_subscriptions');
        Schema::dropIfExists('app_installations');
        Schema::dropIfExists('apps');
    }
};


All migrations written. Let me verify they run, then build all models.


Bash

php artisan migrate:fresh --force 2>&1 | tail -40

Run migrations


   Illuminate\Database\QueryException

  Database file at path [database/database.sqlite] does not exist. Ensure this is an absolute path to the database. (Connection: sqlite, Database: database/database.sqlite, SQL: select exists (select 1 from "main".sqlite_master where name = 'migrations' and type = 'table') as "exists")

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838
    834▕             $exceptionType = $this->isUniqueConstraintError($e)
    835▕                 ? UniqueConstraintViolationException::class
    836▕                 : QueryException::class;
    837▕
  ➜ 838▕             throw new $exceptionType(
    839▕                 $this->getNameWithReadWriteType(),
    840▕                 $query,
    841▕                 $this->prepareBindings($bindings),
    842▕                 $e,

      +32 vendor frames

  33  artisan:16
      Illuminate\Foundation\Application::handleCommand(Object(Symfony\Component\Console\Input\ArgvInput))


touch /Users/fabianwesner/Herd/shop/database/database.sqlite && php artisan migrate:fresh --force 2>&1 | tail -60

Create SQLite file + migrate


   INFO  Preparing database.

  Creating migration table ....................................... 2.82ms DONE

   INFO  Running migrations.

  0001_01_01_000000_create_users_table ........................... 3.86ms DONE
  0001_01_01_000001_create_cache_table ........................... 1.46ms DONE
  0001_01_01_000002_create_jobs_table ............................ 2.10ms DONE
  2025_08_14_170933_add_two_factor_columns_to_users_table ........ 1.04ms DONE
  2026_01_01_000001_create_organizations_table ................... 0.62ms DONE
  2026_01_01_000002_create_stores_table .......................... 5.84ms DONE
  2026_01_01_000003_create_store_domains_table ................... 1.26ms DONE
  2026_01_01_000004_extend_users_table ........................... 1.23ms DONE
  2026_01_01_000005_create_store_users_table ..................... 1.25ms DONE
  2026_01_01_000006_create_store_settings_table .................. 0.45ms DONE
  2026_01_01_000010_create_customers_table ....................... 2.01ms DONE
  2026_01_01_000020_create_products_table ........................ 2.48ms DONE
  2026_01_01_000021_create_product_options_table ................. 3.65ms DONE
  2026_01_01_000022_create_product_variants_table ................ 2.64ms DONE
  2026_01_01_000023_create_inventory_items_table ................. 1.17ms DONE
  2026_01_01_000024_create_collections_table ..................... 1.94ms DONE
  2026_01_01_000025_create_product_media_table ................... 0.81ms DONE
  2026_01_01_000030_create_themes_table .......................... 4.99ms DONE
  2026_01_01_000031_create_pages_table ........................... 1.50ms DONE
  2026_01_01_000032_create_navigation_menus_table ................ 2.71ms DONE
  2026_01_01_000040_create_carts_table ........................... 2.20ms DONE
  2026_01_01_000041_create_checkouts_table ....................... 1.52ms DONE
  2026_01_01_000042_create_shipping_tables ....................... 1.98ms DONE
  2026_01_01_000043_create_discounts_table ....................... 1.19ms DONE
  2026_01_01_000050_create_orders_table .......................... 7.51ms DONE
  2026_01_01_000060_create_analytics_tables ...................... 2.44ms DONE
  2026_01_01_000070_create_apps_tables ........................... 4.02ms DONE


All migrations run cleanly. Now let me build all models. I'll create them as single concise files.


Write

<?php

namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class StoreScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        if (app()->bound('current_store')) {
            $store = app('current_store');
            $builder->where($model->getTable().'.store_id', $store->id);
        }
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Models/Scopes/StoreScope.php

<?php

namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class StoreScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        if (app()->bound('current_store')) {
            $store = app('current_store');
            $builder->where($model->getTable().'.store_id', $store->id);
        }
    }
}


<?php

namespace App\Models\Concerns;

use App\Models\Scopes\StoreScope;
use App\Models\Store;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

trait BelongsToStore
{
    public static function bootBelongsToStore(): void
    {
        static::addGlobalScope(new StoreScope);

        static::creating(function ($model): void {
            if (empty($model->store_id) && app()->bound('current_store')) {
                $model->store_id = app('current_store')->id;
            }
        });
    }

    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Models/Concerns/BelongsToStore.php

<?php

namespace App\Models\Concerns;

use App\Models\Scopes\StoreScope;
use App\Models\Store;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

trait BelongsToStore
{
    public static function bootBelongsToStore(): void
    {
        static::addGlobalScope(new StoreScope);

        static::creating(function ($model): void {
            if (empty($model->store_id) && app()->bound('current_store')) {
                $model->store_id = app('current_store')->id;
            }
        });
    }

    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }
}


<?php

namespace App\Models;

use App\Enums\StoreUserRole;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
use Laravel\Fortify\TwoFactorAuthenticatable;

class User extends Authenticatable
{
    /** @use HasFactory<\Database\Factories\UserFactory> */
    use HasFactory, Notifiable, TwoFactorAuthenticatable;

    protected $fillable = [
        'name',
        'email',
        'password',
        'status',
        'last_login_at',
    ];

    protected $hidden = [
        'password',
        'two_factor_secret',
        'two_factor_recovery_codes',
        'remember_token',
    ];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'last_login_at' => 'datetime',
            'password' => 'hashed',
        ];
    }

    public function stores(): BelongsToMany
    {
        return $this->belongsToMany(Store::class, 'store_users')
            ->using(StoreUser::class)
            ->withPivot('role', 'created_at');
    }

    public function roleForStore(Store $store): ?StoreUserRole
    {
        $pivot = $this->stores()->where('stores.id', $store->id)->first()?->pivot;

        if (! $pivot) {
            return null;
        }

        return StoreUserRole::tryFrom((string) $pivot->role);
    }

    public function initials(): string
    {
        return Str::of($this->name)
            ->explode(' ')
            ->take(2)
            ->map(fn ($word) => Str::substr($word, 0, 1))
            ->implode('');
    }
}


The file /Users/fabianwesner/Herd/shop/app/Models/User.php has been updated successfully.

<?php

namespace App\Models;

use App\Enums\StoreUserRole;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
use Laravel\Fortify\TwoFactorAuthenticatable;

class User extends Authenticatable
{
    /** @use HasFactory<\Database\Factories\UserFactory> */
    use HasFactory, Notifiable, TwoFactorAuthenticatable;

    protected $fillable = [
        'name',
        'email',
        'password',
        'status',
        'last_login_at',
    ];

    protected $hidden = [
        'password',
        'two_factor_secret',
        'two_factor_recovery_codes',
        'remember_token',
    ];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'last_login_at' => 'datetime',
            'password' => 'hashed',
        ];
    }

    public function stores(): BelongsToMany
    {
        return $this->belongsToMany(Store::class, 'store_users')
            ->using(StoreUser::class)
            ->withPivot('role', 'created_at');
    }

    public function roleForStore(Store $store): ?StoreUserRole
    {
        $pivot = $this->stores()->where('stores.id', $store->id)->first()?->pivot;

        if (! $pivot) {
            return null;
        }

        return StoreUserRole::tryFrom((string) $pivot->role);
    }

    public function initials(): string
    {
        return Str::of($this->name)
            ->explode(' ')
            ->take(2)
            ->map(fn ($word) => Str::substr($word, 0, 1))
            ->implode('');
    }
}

-     * The attributes that are mass assignable.

-     * The attributes that should be hidden for serialization.

-     * Get the attributes that should be cast.

+            'last_login_at' => 'datetime',

+    public function stores(): BelongsToMany

+        return $this->belongsToMany(Store::class, 'store_users')

+            ->withPivot('role', 'created_at');

+    public function roleForStore(Store $store): ?StoreUserRole

+        $pivot = $this->stores()->where('stores.id', $store->id)->first()?->pivot;

+        return StoreUserRole::tryFrom((string) $pivot->role);

<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
use Laravel\Fortify\TwoFactorAuthenticatable;

class User extends Authenticatable
{
    /** @use HasFactory<\Database\Factories\UserFactory> */
    use HasFactory, Notifiable, TwoFactorAuthenticatable;

    /**
     * The attributes that are mass assignable.
     *
     * @var list<string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var list<string>
     */
    protected $hidden = [
        'password',
        'two_factor_secret',
        'two_factor_recovery_codes',
        'remember_token',
    ];

    /**
     * Get the attributes that should be cast.
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
        ];
    }

    /**
     * Get the user's initials
     */
    public function initials(): string
    {
        return Str::of($this->name)
            ->explode(' ')
            ->take(2)
            ->map(fn ($word) => Str::substr($word, 0, 1))
            ->implode('');
    }
}


<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Organization extends Model
{
    /** @use HasFactory<\Database\Factories\OrganizationFactory> */
    use HasFactory;

    protected $fillable = ['name', 'billing_email'];

    public function stores(): HasMany
    {
        return $this->hasMany(Store::class);
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Models/Organization.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Organization extends Model
{
    /** @use HasFactory<\Database\Factories\OrganizationFactory> */
    use HasFactory;

    protected $fillable = ['name', 'billing_email'];

    public function stores(): HasMany
    {
        return $this->hasMany(Store::class);
    }
}