E-Commerce Platforms

eCommerce Platform with Laravel: Building a Comprehensive Online Store

By the Domain India teamPublished 8 min read
Knowledge base article
Contents (8 sections)

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.

Key takeaways

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:

bash
composer global require laravel/installer
laravel new shop

The 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.

php
// 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.

A resource controller covers listing, showing and editing products. Validate every input, and let Laravel's storage handle uploaded images:

php
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.

php
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:

php
$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.

Only your server decides that an order is paid

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:

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():

php
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.phar and run php composer.phar install --no-dev --optimize-autoloader; if it stops with a proc_open message, run it again with --no-scripts, then php artisan package:discover. Or run Composer on your computer or in CI, and upload the project including vendor/.
  • Email: Laravel's SMTP and sendmail transports fail on our cPanel servers. Send through PHP mail() with the -f envelope sender, as Laravel on cPanel and DirectAdmin explains. On DirectAdmin, test mail on your plan.
  • Queues: use QUEUE_CONNECTION=sync; long-running queue:work daemons are stopped.
  • Scheduler: use the single schedule:run cron entry, with closures or jobs rather than artisan commands.
  • Product images: php artisan storage:link needs the symlink function, which is disabled. Create the link with ln -s over SSH, or ask support to create it.
  • Security: set APP_ENV=production and APP_DEBUG=false, and point the domain's document root at public/.

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.

cPanel Growth
₹200/mo + GST
  • 50 GB NVMe SSD Storage
  • 100 GB Monthly Bandwidth
  • 5 Websites
  • 50 Email Accounts
See plan details
App Developer
₹250/mo + GST
  • 512 MB RAM per app
  • 1.5 GB RAM total
  • 2 vCPU
  • 10 GB NVMe SSD
See plan details

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.

Host your Laravel 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

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app
Build an Online Store with Laravel