---
title: "Configuration — Shops for Laravel | Roundly"
description: "Every config key and its default — tenant model, pricing, tax, inventory, media, reviews, slugs, payment, shipping, discounts and order numbers."
url: https://roundly-consulting.com/open-source/docs/shops-for-laravel/configuration
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Shops for Laravel](https://roundly-consulting.com/open-source/docs/shops-for-laravel.md)

# Configuration

The package works with zero host configuration. The published config/shops.php in full (comments trimmed):

```php
use RoundlyConsulting\Coupons\Models\Coupon;
use RoundlyConsulting\Shops\Discounts\CouponPackageDiscountResolver;
use RoundlyConsulting\Shops\Orders\NumberGenerators\DefaultNumberGenerator;
use RoundlyConsulting\Shops\Payments\NullPaymentGateway;
use RoundlyConsulting\Shops\Reviews\NullVerifiedPurchaseResolver;
use RoundlyConsulting\Shops\Shipping\FreeShippingMethod;
use RoundlyConsulting\Shops\Shops\Shop;
use RoundlyConsulting\Shops\Support\Tax\DatabaseTaxResolver;

return [
    'shop_model' => env('SHOPS_SHOP_MODEL', Shop::class),
    'key_type' => env('SHOPS_KEY_TYPE', 'bigint'),

    'pricing' => [
        'price_type' => env('SHOPS_PRICE_TYPE', 'gross'),
        'default_currency' => env('SHOPS_DEFAULT_CURRENCY', 'EUR'),
    ],

    'tax_classes' => [
        'standard' => env('SHOPS_TAX_RATE', 20),
        'reduced' => 10,
        'zero' => 0,
    ],

    'tax' => [
        'resolver' => DatabaseTaxResolver::class,
    ],

    'inventory' => [
        'low_stock_threshold' => env('SHOPS_LOW_STOCK_THRESHOLD', 0),
    ],

    'media' => [
        'featured_bucket' => env('SHOPS_MEDIA_FEATURED_BUCKET', 'featured'),
        'gallery_bucket' => env('SHOPS_MEDIA_GALLERY_BUCKET', 'gallery'),
        'variant_bucket' => env('SHOPS_MEDIA_VARIANT_BUCKET', 'gallery'),
        'banner_bucket' => env('SHOPS_MEDIA_BANNER_BUCKET', 'banner'),
        'disk' => env('SHOPS_MEDIA_DISK'),
        'public' => env('SHOPS_MEDIA_PUBLIC', true),
        'responsive_widths' => [320, 640, 1024, 1600],
        'max_file_size' => null,
    ],

    'reviews' => [
        'verified_purchase_resolver' => env(
            'SHOPS_VERIFIED_PURCHASE_RESOLVER',
            NullVerifiedPurchaseResolver::class,
        ),
    ],

    'attributes' => [
        'definitions' => [
            // 'material' => ['type' => 'string'],
            // 'weight'   => ['type' => 'integer', 'rules' => ['min:0']],
        ],
    ],

    'locales' => [
        'fallback' => env('SHOPS_FALLBACK_LOCALE', config('app.fallback_locale', 'en')),
    ],

    'slugs' => [
        'history' => env('SHOPS_SLUG_HISTORY', false),
    ],

    'payment' => [
        'gateway' => env('SHOPS_PAYMENT_GATEWAY', NullPaymentGateway::class),
        'allow_store_credit' => env('SHOPS_ALLOW_STORE_CREDIT', false),
        'store_credit_bucket' => env('SHOPS_STORE_CREDIT_BUCKET', 'store_credit'),
        'refund_to_store_credit' => env('SHOPS_REFUND_TO_STORE_CREDIT', false),
    ],

    'shipping' => [
        'method' => env('SHOPS_SHIPPING_METHOD', FreeShippingMethod::class),
    ],

    'discounts' => [
        'coupon_model' => env('SHOPS_COUPON_MODEL', Coupon::class),
        'resolver' => CouponPackageDiscountResolver::class,
    ],

    'addresses' => [
        'billing_same_as_shipping' => env('SHOPS_BILLING_SAME_AS_SHIPPING', true),
    ],

    'orders' => [
        'number_generator' => DefaultNumberGenerator::class,
    ],
];
```

## Every key

| Key | Default | Env | Purpose |
| --- | --- | --- | --- |
| `shop_model` | Shops\\Shop | `SHOPS_SHOP_MODEL` | The tenant model every shop\_id points to — any Eloquent model; a class that is not one throws InvalidConfigurationException naming the key. Extend Shop to keep the tenant features. |
| `key_type` | bigint | `SHOPS_KEY_TYPE` | Key type of the polymorphic customer, owner and reference columns: bigint, uuid or ulid. Anything else throws InvalidConfigurationException when the migrations run. Fixed once they have run. |
| `pricing.price_type` | gross | `SHOPS_PRICE_TYPE` | gross (tax is extracted from the price) or net (tax is added on top); anything else throws InvalidConfigurationException listing both. Orders snapshot it. |
| `pricing.default_currency` | EUR | `SHOPS_DEFAULT_CURRENCY` | ISO 4217 fallback for a shop without its own currency, an order without a shop and a shop-less product’s default variant. |
| `tax_classes` | standard 20, reduced 10, zero 0 | `SHOPS_TAX_RATE` | Whole-percent fallback floor used when a shop has no matching database rate. Each rate is 0–100, an int or a string such as "21"; "twenty" or "19.5" throws instead of becoming 0 %. A shipped class whose rate is not set — left out, null or blank (SHOPS\_TAX\_RATE=) — takes its shipped rate (standard 20 %, reduced 10 %, zero 0 %), never 0 %; one of your own classes that is not set uses the standard rate, and an empty map means the shipped rates. Set 0 explicitly for no tax. The env sets standard only. |
| `tax.resolver` | DatabaseTaxResolver | — | Resolves the rate for a (shop, class, country) lookup. Bind ConfigTaxResolver for config-only tax. |
| `inventory.low_stock_threshold` | 0 | `SHOPS_LOW_STOCK_THRESHOLD` | StockRanLow fires once when an adjustment takes a tracked variant’s available stock from above this level to at or below it. At least 0; a non-integer value throws. |
| `media.featured_bucket` | featured | `SHOPS_MEDIA_FEATURED_BUCKET` | Product single-image bucket. |
| `media.gallery_bucket` | gallery | `SHOPS_MEDIA_GALLERY_BUCKET` | Product multi-image bucket. |
| `media.variant_bucket` | gallery | `SHOPS_MEDIA_VARIANT_BUCKET` | Per-variant image bucket. |
| `media.banner_bucket` | banner | `SHOPS_MEDIA_BANNER_BUCKET` | Category banner bucket (single image). |
| `media.disk` | null | `SHOPS_MEDIA_DISK` | Disk for every catalog bucket; not set (null or blank) uses media-library’s default. |
| `media.public` | true | `SHOPS_MEDIA_PUBLIC` | Public (CDN, SEO) or private visibility for every catalog bucket. |
| `media.responsive_widths` | \[320, 640, 1024, 1600\] | — | Responsive width ladder; each width is generated as the variant responsive-<width>. Every width must be a positive integer — a bad or blank entry throws rather than being dropped. |
| `media.max_file_size` | null | — | Optional per-image cap in bytes, applied to every catalog bucket — product, variant and category. Not set (null or blank) for none; otherwise at least 1. |
| `reviews.verified_purchase_resolver` | NullVerifiedPurchaseResolver | `SHOPS_VERIFIED_PURCHASE_RESOLVER` | Decides whether Product::review() pre-flags a review as a verified purchase. |
| `attributes.definitions` | \[\] | — | Product spec-sheet definitions keyed by name, registered at boot. |
| `locales.fallback` | app.fallback\_locale | `SHOPS_FALLBACK_LOCALE` | Fallback locale for translations and slugs; the default-variant SKU derives from it. |
| `slugs.history` | false | `SHOPS_SLUG_HISTORY` | Keep retired slugs and answer old URLs with a 301 to the current one. |
| `payment.gateway` | NullPaymentGateway | `SHOPS_PAYMENT_GATEWAY` | Charges orders. Refunds are host-driven. |
| `payment.allow_store_credit` | false | `SHOPS_ALLOW_STORE_CREDIT` | Shops::order($order)->charge() applies the buyer’s store credit before the gateway. |
| `payment.store_credit_bucket` | store\_credit | `SHOPS_STORE_CREDIT_BUCKET` | The credits bucket used; it must be denominated in the order currency. |
| `payment.refund_to_store_credit` | false | `SHOPS_REFUND_TO_STORE_CREDIT` | On OrderRefunded, grant the order’s final price back as store credit. |
| `shipping.method` | FreeShippingMethod | `SHOPS_SHIPPING_METHOD` | Your ShippingMethod; checkout quotes the shipping address through it. |
| `discounts.coupon_model` | Coupon (coupons) | `SHOPS_COUPON_MODEL` | Model behind $order->coupon: the coupons Coupon or your subclass of it. Anything else throws InvalidConfigurationException naming the key. |
| `discounts.resolver` | CouponPackageDiscountResolver | — | Turns a code and a goods subtotal into a DiscountResult. |
| `addresses.billing_same_as_shipping` | true | `SHOPS_BILLING_SAME_AS_SHIPPING` | fromAddressBook() reuses the primary shipping address as billing when there is no billing address. |
| `orders.number_generator` | DefaultNumberGenerator | — | Generates the order number; must implement NumberGenerator. |

## Environment

The common knobs are env-driven, so you rarely need to publish the config at all:

```bash
SHOPS_PRICE_TYPE=gross
SHOPS_DEFAULT_CURRENCY=EUR
SHOPS_TAX_RATE=20
SHOPS_LOW_STOCK_THRESHOLD=5
SHOPS_MEDIA_DISK=s3
SHOPS_SLUG_HISTORY=true
SHOPS_ALLOW_STORE_CREDIT=true
SHOPS_PAYMENT_GATEWAY='App\Payments\AcmePayGateway'
```

The bool switches accept true/false, 1/0, on/off and yes/no, so any env spelling works. Anything else throws an InvalidConfigurationException naming the key, so a typo never quietly becomes the default.

## Strict values

The other settings are just as strict. A key that is not set — left out, null or blank ('' or whitespace, such as a SHOPS\_DEFAULT\_CURRENCY= line) — takes its default; a value of the wrong shape throws an InvalidConfigurationException naming the key — nothing is cast to 0 or swapped for the default:

- Integers take an int or a whole-number string such as "5" — never "five" or "5.5". Tax-class rates must be 0–100, the low-stock threshold at least 0, every responsive width and max\_file\_size at least 1.
- pricing.price\_type must be gross or net; key\_type must be bigint, uuid or ulid.
- Names — the currency, bucket names, the media disk and the fallback locale — must be strings; a blank one is not set and takes its default, so a blank SHOPS\_MEDIA\_DISK= means the media package’s disk.
- tax\_classes, media.responsive\_widths and attributes.definitions must be arrays, and each attribute definition an array of its own. Each responsive width must be a real width — a blank entry inside the list throws.
- A tax class whose rate is not set takes its shipped rate, so a blank SHOPS\_TAX\_RATE= still charges the shipped 20 % on the standard class — set SHOPS\_TAX\_RATE=0 to charge none.
- shop\_model must be an Eloquent model; discounts.coupon\_model must be the coupons Coupon or a subclass of it.

php artisan about shows a broken setting as INVALID instead of failing, so you can spot it at a glance.

## Notes

- tax\_classes is in whole percents (20 = 20 %) for authoring convenience; database tax rates are basis points (1900 = 19.00 %).
- A shop may override the default currency, and every order snapshots its currency on insert — changing pricing.default\_currency never re-denominates placed orders.
- Store credit requires credits-for-laravel’s currencies map to denominate payment.store\_credit\_bucket.
- Driver keys accept any class implementing the contract — or bind the contract yourself in a service provider.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/shops-for-laravel.md)
