Laravel is a good base for a custom online store when an off-the-shelf platform does not fit your products, pricing or workflow. This guide walks through the parts every store needs (catalogue, cart, orders, payments, accounts, reviews and an admin area) with current Laravel code, and ends with how to run the finished store on shared hosting or a server of your own.
Use a current Laravel release with PHP 8.3 or newer and one of Laravel's official starter kits for login and registration. Store prices as whole paise in integer columns, keep the cart in the session, and copy each product's price into the order when it is placed. Mark an order paid only after your server has verified the payment with the gateway, never because the browser says so. For Indian customers, Razorpay supports UPI, cards and netbanking. If your store needs no custom code at all, WooCommerce or an AI-built site with a cart may be quicker.
1. Before you start
Building a store is a real software project. Payment handling, tax invoices, stock, returns and security all become your responsibility. Choose Laravel when you need something a ready-made platform cannot do, such as custom pricing rules, a B2B ordering flow or integration with your own systems. Otherwise compare the options in choosing the right eCommerce platform.
Create the project on your own computer:
composer global require laravel/installer
laravel new shopThe installer asks which starter kit to use. Pick one: it gives you registration, login, password reset and email verification, so you do not write them by hand. Then set your database details in .env.
2. The data model
Four tables carry the store: categories, products, orders and order items. Store money as integers in paise so that totals never suffer rounding errors.
// database/migrations/xxxx_create_products_table.php
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->foreignId('category_id')->constrained();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->unsignedInteger('price_paise');
$table->unsignedInteger('stock')->default(0);
$table->string('image_path')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('status')->default('pending'); // pending, paid, shipped, cancelled, refunded
$table->unsignedInteger('total_paise');
$table->string('gateway_order_id')->nullable()->unique();
$table->json('shipping_address');
$table->timestamps();
});
Schema::create('order_items', function (Blueprint $table) {
$table->id();
$table->foreignId('order_id')->constrained()->cascadeOnDelete();
$table->foreignId('product_id')->constrained();
$table->string('name'); // copied at order time
$table->unsignedInteger('price_paise'); // copied at order time
$table->unsignedInteger('quantity');
});Copying the name and price into order_items matters: when you change a price next month, old orders and invoices must still show what the customer paid. Add the usual relations (Product belongsTo Category, Order hasMany OrderItem) in the models.
3. Catalogue and search
A resource controller covers listing, showing and editing products. Validate every input, and let Laravel's storage handle uploaded images:
public function store(Request $request)
{
$data = $request->validate([
'category_id' => ['required', 'exists:categories,id'],
'name' => ['required', 'string', 'max:200'],
'slug' => ['required', 'alpha_dash', 'unique:products,slug'],
'price_paise' => ['required', 'integer', 'min:1'],
'stock' => ['required', 'integer', 'min:0'],
'image' => ['nullable', 'image', 'max:4096'],
]);
if ($request->hasFile('image')) {
$data['image_path'] = $request->file('image')->store('products', 'public');
}
Product::create($data);
return redirect()->route('admin.products.index');
}For search, a where('name', 'like', "%{$term}%") query is fine for a few thousand products; beyond that, consider Laravel Scout. Always paginate listings with ->paginate(24).
4. The cart
For most stores a session cart is enough and needs no extra package. Keep only product IDs and quantities in the session, and read prices from the database every time, so a customer cannot change a price in their browser.
class Cart
{
public function add(int $productId, int $qty = 1): void
{
$items = session('cart', []);
$items[$productId] = ($items[$productId] ?? 0) + $qty;
session(['cart' => $items]);
}
public function lines(): Collection
{
$items = session('cart', []);
return Product::whereIn('id', array_keys($items))->where('is_active', true)->get()
->map(fn ($p) => ['product' => $p, 'qty' => $items[$p->id],
'subtotal' => $p->price_paise * $items[$p->id]]);
}
public function totalPaise(): int
{
return $this->lines()->sum('subtotal');
}
}5. Checkout and payment
Create the order inside a database transaction, then send the customer to the gateway:
$order = DB::transaction(function () use ($cart, $address) {
$order = Order::create([
'user_id' => auth()->id(),
'total_paise' => $cart->totalPaise(),
'shipping_address' => $address,
]);
foreach ($cart->lines() as $line) {
$order->items()->create([
'product_id' => $line['product']->id,
'name' => $line['product']->name,
'price_paise' => $line['product']->price_paise,
'quantity' => $line['qty'],
]);
}
return $order;
});Then follow the gateway's order flow: create a gateway order for total_paise, save its ID in gateway_order_id, and open the gateway's checkout. Our guides cover the details with working code: Razorpay in PHP and Stripe Checkout in PHP.
The redirect back from the gateway can be faked or lost. Mark an order paid only after you verify the payment signature on your server, or after the gateway's webhook confirms it, and check that the amount matches total_paise. Reduce stock at the same moment. Make the webhook handler safe to run twice, because gateways retry.
Webhook routes receive no CSRF token, so exclude them in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: ['webhooks/razorpay']);
})6. Accounts, admin and reviews
Admin access. Add an is_admin boolean to users and define a gate in AppServiceProvider::boot():
Gate::define('manage-store', fn (User $user) => $user->is_admin);Protect the admin routes with ->middleware('can:manage-store'). For several staff roles, the spatie/laravel-permission package is the common choice.
Order history. A customer's orders are auth()->user()->orders()->latest()->paginate(). Always scope queries to the logged-in user, so nobody can view another customer's order by changing the ID in the URL.
Reviews. Allow one review per customer per product (a unique index on product_id and user_id), accept reviews only from customers with a paid order for that product, and escape the text when you display it. Blade's {{ }} does that for you.
7. Tests before launch
Laravel ships with a test runner (php artisan test). At minimum, test that the cart total is computed from database prices, that an unverified payment never marks an order paid, that a duplicate webhook does not double-reduce stock, and that a customer cannot open another customer's order.
8. Running the store on Domain India
Shared hosting (cPanel, DirectAdmin, Webuzo) runs a normal Laravel store, with a few rules:
- Composer isn't pre-installed on the server. With jailed SSH (on request) you can download
composer.pharand runphp composer.phar install --no-dev --optimize-autoloader; if it stops with a proc_open message, run it again with--no-scripts, thenphp artisan package:discover. Or run Composer on your computer or in CI, and upload the project includingvendor/. - Email: Laravel's SMTP and sendmail transports fail on our cPanel servers. Send through PHP
mail()with the-fenvelope sender, as Laravel on cPanel and DirectAdmin explains. On DirectAdmin, test mail on your plan. - Queues: use
QUEUE_CONNECTION=sync; long-runningqueue:workdaemons are stopped. - Scheduler: use the single
schedule:runcron entry, with closures or jobs rather than artisan commands. - Product images:
php artisan storage:linkneeds thesymlinkfunction, which is disabled. Create the link withln -sover SSH, or ask support to create it. - Security: set
APP_ENV=productionandAPP_DEBUG=false, and point the domain's document root atpublic/.
Jailed SSH is available on every shared plan, off by default; ask support to enable it if you want to run php artisan migrate --force from a shell.
For queues, Redis or more traffic, move to the App Platform (build from a Dockerfile) or a self-managed VPS.
- 50 GB NVMe SSD Storage
- 100 GB Monthly Bandwidth
- 5 Websites
- 50 Email Accounts
- 512 MB RAM per app
- 1.5 GB RAM total
- 2 vCPU
- 10 GB NVMe SSD
Prices on the cards are Domain India list prices and exclude 18% GST.
Which Laravel and PHP versions should I use for a new store?
Use the current Laravel release and PHP 8.3 or newer. On Domain India cPanel and DirectAdmin hosting you choose the PHP version per account in the control panel.
Why store prices in paise instead of rupees with decimals?
Integers avoid rounding errors when you add up carts, taxes and discounts. Convert to rupees only when you display a price.
Can I run Composer on Domain India shared hosting?
Yes, over jailed SSH (on request). Composer isn't pre-installed, so download composer.phar and run php composer.phar install; if it stops with a proc_open message, run it again with --no-scripts, then run php artisan package:discover. Or run composer install on your computer or in CI and upload the project with its vendor folder.
Which payment gateway suits an Indian Laravel store?
Razorpay supports UPI, cards, netbanking and wallets and has a PHP SDK. Whichever gateway you choose, verify every payment on your server before you mark an order paid.
Do Laravel queues work on shared hosting?
Long-running queue workers do not. Use the sync queue driver, or move to the App Platform or a VPS if your store depends on background jobs.
Ready to build? Read Laravel on cPanel and DirectAdmin before you deploy, compare cPanel hosting and the App Platform, or ask us in a support ticket which fits your store.
Run a Laravel store on cPanel shared hosting with free SSL and weekly backups, and move up to the App Platform or a VPS when it grows.
See cPanel hosting plans