dystcz/dystore
Dystore is a Laravel/PHP e-commerce package that provides a flexible foundation for building online stores, with tools for product, cart, and order management and an extendable architecture suited to custom storefronts and commerce features.
Dystore is a headless commerce API for Laravel built on top of Lunar. It exposes JSON:API endpoints (products, variants, carts, orders, customers, checkout, etc.) that you consume from a storefront (Next.js, Nuxt, mobile, etc.). As of 1.0.15 it targets PHP 8.3+ and Laravel 12, with Lunar 1.5.0-beta.6 as the underlying commerce engine.
php -v (≥ 8.3), composer show laravel/framework (12.x). Laravel 11 and PHP 8.2 are no longer supported.composer require dystcz/dystore. Lunar is pulled in transitively — check composer show lunarphp/lunar matches the beta line Dystore expects.php artisan migrate, then seed the Lunar base data (countries, currencies, tax classes, etc.) if you have not already.config/dystore.php — model bindings, route prefix, media/URL config.routes/dystore.php (or the route service provider) — the canonical endpoint list. php artisan route:list --path=api is the fastest way to see what is live.src/JsonApi/ — the resource/schema classes you will extend to add fields.GET /api/v1/products with ?include=variants,prices&fields[products]=name,slug to confirm the API boots end-to-end. Then wire an auth flow using the new check-account-exists endpoint before hitting register (which can now create users without a password).Adding fields to the API. Dystore's schemas are plain classes extending LaravelJsonApi\Eloquent\Schema. Register a custom schema in config/dystore.php, override fields() and getRelations(), and add a Field or Attribute for anything not exposed by default. Never edit vendor files.
Swapping models. Dystore binds Lunar models (Product, Variant, Order, Cart, User…). To extend behaviour — say a custom User with extra profile columns — subclass the Dystore model and rebind it in config/dystore.php. Because Dystore models already extend Lunar's, you compose rather than replace.
Password-less registration. Since 1.0.15 register accepts users without a password. Typical pattern: a guest checkout flow where the user is created on first order, then a "set your password" email is sent. Guard against duplicate emails by calling the check-account-exists endpoint first; it returns a boolean so the storefront can branch into login vs. registration.
Account deletion. The delete-user endpoint (1.0.15) enables self-service GDPR erasure. Wire it to a confirmation modal and consider queuing a soft-delete/anonymisation listener rather than hard-deleting, since orders and reviews reference the user.
Availability filtering. Use the new product and variant availability scopes instead of reimplementing "in stock and published" logic. They compose with the existing nested collection group filters (1.0.15) so a storefront can request e.g. filter[collection.groups]=... and filter[available]=true in one call — ideal for category pages and mega-menus.
Reviews. Reviews now carry an order relation (1.0.15) and the Filament admin shows all reviews (the PublishedScope was removed). If you want verified-purchase badges on the storefront, expose the relation and check review.order_id against the current customer's orders. Keep your own publish filter on the API-facing schema if you do not want unpublished reviews leaking.
Filament admin. Dystore ships admin resources. Run php artisan filament:upgrade after updating and ensure your panel provider registers the Dystore plugin. Use it to validate storefront behaviour against real order data.
Testing. Dystore moved to Pest 4. Use RefreshDatabase + the published factories, and assert against the JSON:API document shape (assertJsonPath('data.0.type', 'products')) rather than raw arrays. Fake the Lunar cart/checkout pipeline where you can — full checkout tests are slow.
Integration tips. Use sparse fieldsets aggressively (fields[...]) and cache product responses at the edge — the payloads are large by default. Keep Dystore routes behind a versioned prefix and add your own middleware (CORS, throttling) at the route-group level, not inside vendor routes.
1.5.0-beta.6 means internal APIs can shift between patch releases. Pin lunarphp/lunar with an exact constraint and read Lunar's release notes as carefully as Dystore's — this is the most common source of surprise breakage.Content-Type: application/vnd.api+json and Accept: application/vnd.api+json. A missing header yields a 4xx that looks like an auth error. Error bodies are JSON:API errors[] arrays, not Laravel's message shape — handle both if you have shared frontend error handling.Auth::attempt. Decide up front whether you use magic links, OTP, or a forced password-set step.config/dystore.php, run php artisan config:clear and route:clear. Stale config is the #1 cause of "my custom model is ignored."?include=variants,prices,media can fan out fast. Prefer sparse fieldsets plus explicit includes, and add indexes on product_id/variant_id foreign keys for large catalogues.php artisan route:list --path=api -v to confirm which controller/model served a request, Telescope for query counts, and log the raw JSON:API request body when a write endpoint returns 422 — validation errors are buried in errors[].source.pointer.How can I help you explore Laravel packages today?