← Semua picks

Bundle Recipe Recommended

Bundle: Internal tool untuk SMB dengan Laravel + Inertia + React

Stack untuk internal tool yang non-customer-facing: Laravel backend + Inertia.js + React. Cocok untuk SMB Indonesia dengan tim PHP existing.

22 Maret 2026 · 8 menit ·Use case: Admin dashboard internal SMB Indonesia
LaravelInertia.jsReactMySQLTailwind

Konteks

Internal tool ≠ SaaS public-facing. Internal tool punya:

  • User: 5-50 internal staff
  • Workflow: complex (CRUD heavy, approval flow, reporting)
  • Performance budget: relaxed (load 1-3 detik OK)
  • Security: critical (data internal SMB)

Bundle ini saya pakai untuk 3 klien Indonesia:

  1. Kontraktor di Tangerang (inventory + project tracking)
  2. Klinik dental (patient management + scheduling)
  3. Toko bangunan Cikupa (stock + supplier order)

The stack

Backend:        Laravel 12 (PHP 8.3+)
Frontend:       Inertia.js + React 19 + TypeScript
Styling:        Tailwind CSS v4
Database:       MySQL 8 (atau Postgres 16)
ORM:            Eloquent (Laravel built-in)
Auth:           Laravel Sanctum + role-based access
Queue:          Laravel Horizon (Redis-backed)
Storage:        Laravel Cloud / local + S3
Hosting:        Hetzner / DigitalOcean VPS

Mengapa Laravel (bukan Node)

Alasan strategis untuk SMB Indonesia:

  1. Talent pool: Cari developer Laravel di Indonesia 2026 sangat mudah. LinkedIn search “Laravel Indonesia” → 800+ result. Vs “Node.js” → 600 result, tapi competition lebih ketat.

  2. Laravel ecosystem mature: form handling, validation, auth, queue, file upload, email — semua first-party + battle-tested. Less third-party glue code.

  3. MySQL/Postgres hosting: PHP-MySQL deployment standard di Indonesia hosting (Niagahoster, IDcloudhost, dll). Kalau klien Anda already pakai cPanel hosting, Laravel deploy familiar.

  4. Eloquent ORM: query expressive, relationship handling smooth. Untuk CRUD-heavy internal tool, ini saving signifikan time.

Trade-off vs Node/Bun:

  • Lebih lambat (PHP-FPM overhead vs Bun native compile)
  • Tidak as edge-friendly (Laravel butuh long-lived process, bukan serverless)
  • Tidak ideal untuk real-time (WebSocket OK tapi tidak as smooth Node)

Untuk internal tool: trade-off accept.

Mengapa Inertia.js (bukan API + SPA atau Livewire)

3 pendekatan untuk Laravel + React:

  1. Laravel API + React SPA terpisah: full SPA, butuh JWT auth, CORS handling, state management complex.
  2. Inertia.js: Laravel render controller return Inertia response, React render page. Feels SPA tanpa API complexity.
  3. Livewire: PHP-only, hyperscript-like reactive. Tidak React.

Inertia.js pick karena:

  • Tidak butuh API design (controller return Inertia view langsung)
  • Form validation Laravel-side, error langsung available di React props
  • Session auth standard (no JWT complexity)
  • File upload work seamlessly
  • Page transition smooth tanpa router complexity

Trade-off: Inertia tidak ideal untuk public API yang juga di-consume mobile app. Untuk internal tool only, perfect.

Project structure

/
├── app/
│   ├── Http/
│   │   ├── Controllers/
│   │   ├── Middleware/
│   │   └── Requests/        # Form Request validation
│   ├── Models/              # Eloquent models
│   └── Policies/            # Authorization
├── resources/
│   ├── js/
│   │   ├── Pages/           # Inertia React pages
│   │   ├── Components/      # Shared React components
│   │   ├── Layouts/         # Auth layout, guest layout
│   │   └── app.tsx          # Entry
│   ├── views/
│   │   └── app.blade.php    # Inertia root template
│   └── css/
│       └── app.css          # Tailwind
├── routes/
│   ├── web.php              # Inertia routes
│   └── api.php              # API routes (kalau perlu)
└── database/
    ├── migrations/
    └── seeders/

Setup time

Fresh project to working “Hello World dashboard with auth”:

# Create Laravel project
composer create-project laravel/laravel internal-tool
cd internal-tool

# Install Breeze with Inertia + React + TypeScript
composer require laravel/breeze --dev
php artisan breeze:install react --typescript

# Install dependencies
bun install
bun run dev

# Migrate
php artisan migrate

Total: 15-20 menit untuk fully working auth + dashboard skeleton.

Common patterns

CRUD page (e.g., supplier list)

// app/Http/Controllers/SupplierController.php
class SupplierController extends Controller
{
    public function index()
    {
        return Inertia::render('Suppliers/Index', [
            'suppliers' => Supplier::with('contacts')->latest()->paginate(20),
            'filters' => request()->only(['search', 'category']),
        ]);
    }

    public function store(StoreSupplierRequest $request)
    {
        $supplier = Supplier::create($request->validated());
        return redirect()->route('suppliers.index')->with('success', 'Supplier berhasil ditambah');
    }
}
// resources/js/Pages/Suppliers/Index.tsx
import { Head, useForm } from '@inertiajs/react';
import AppLayout from '@/Layouts/AppLayout';

export default function Index({ suppliers, filters }) {
    return (
        <AppLayout>
            <Head title="Suppliers" />
            <h1>Daftar Supplier</h1>
            <ul>
                {suppliers.data.map(s => (
                    <li key={s.id}>{s.name}{s.contacts.length} kontak</li>
                ))}
            </ul>
        </AppLayout>
    );
}

Server-side data + client-side rendering, single mental model.

Form dengan validation

import { useForm } from '@inertiajs/react';

function CreateSupplierForm() {
    const { data, setData, post, processing, errors } = useForm({
        name: '',
        category: '',
        phone: '',
    });

    const submit = (e) => {
        e.preventDefault();
        post('/suppliers');
    };

    return (
        <form onSubmit={submit}>
            <input value={data.name} onChange={(e) => setData('name', e.target.value)} />
            {errors.name && <p className="text-red-500">{errors.name}</p>}
            
            <button type="submit" disabled={processing}>Save</button>
        </form>
    );
}

Laravel Form Request handle validation server-side, error otomatis available di errors object.

Role-based access

// app/Policies/SupplierPolicy.php
class SupplierPolicy
{
    public function viewAny(User $user): bool
    {
        return $user->hasRole(['admin', 'manager', 'staff']);
    }

    public function create(User $user): bool
    {
        return $user->hasRole(['admin', 'manager']);
    }

    public function delete(User $user, Supplier $supplier): bool
    {
        return $user->hasRole('admin');
    }
}
// Controller usage
public function destroy(Supplier $supplier)
{
    $this->authorize('delete', $supplier);
    $supplier->delete();
    return redirect()->back();
}

Cost (per bulan)

Hosting Laravel app untuk internal tool 20-50 user:

ItemCost (USD)
Hetzner CX21 (2 vCPU, 4GB, Singapore)$5
MySQL self-hosted on VPS (atau RDS)$0 (atau $20 untuk managed)
Domain$1
SSL (Let’s Encrypt)$0
Backup ke Cloudflare R2$1
Total$7-27/bulan

Untuk internal tool yang user-nya staff (bukan public), VPS murah cukup.

Performance untuk internal tool

Bench dari 1 klien (kontraktor Tangerang, 30 user, 8 jam/hari pakai):

  • Page load: 400-800ms (acceptable untuk internal)
  • Database query: 5-50ms (MySQL local di VPS)
  • File upload (PDF dokumen proyek): 1-3 detik untuk 5MB file
  • Report generation (PDF invoice): 2-5 detik

Tidak as cepat sebagai Astro/Bun stack, tapi cukup untuk internal workflow.

Deployment

# Initial deploy
ssh [email protected]
cd /var/www
git clone https://github.com/klien/internal-tool.git
cd internal-tool
composer install --no-dev --optimize-autoloader
bun install && bun run build
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache

# Setup PHP-FPM + Nginx (skip — standar Laravel deployment)
# Setup SSL via Certbot
# Setup Horizon untuk queue worker

Subsequent deploy via GitHub Actions atau simple git pull && composer install && bun run build && php artisan migrate.

Yang sering bermasalah

File permission: PHP-FPM + Laravel butuh permission tertentu untuk storage/ dan bootstrap/cache/. Common cause of 500 error post-deploy.

Queue worker stop: Horizon worker bisa stop unexpected. Supervisor monitoring penting.

Memory leak Inertia + React: Inertia hold previous page memory longer dari typical SPA. Untuk tab yang user open all day, occasional refresh diperlukan.

Database backup: kalau klien tidak set automatic backup, data loss risk. Selalu setup mysqldump cron + sync ke S3/R2.

Konteks Indonesia

Untuk SMB Indonesia, Laravel + Inertia + React adalah sweet spot untuk internal tool:

  • Talent pool besar (mudah hire / outsource)
  • Hosting straightforward (banyak provider Indonesia support)
  • Total cost <Rp 500rb/bulan untuk internal tool 20-50 user
  • Setup time fast (1-2 minggu untuk MVP)

Verdict

Recommended untuk internal tool SMB Indonesia.

Tidak cocok untuk:

  • Public-facing SaaS yang prioritize edge latency (pakai Astro + Bun stack)
  • Mobile-app heavy product (perlu API design serious)
  • Real-time collaborative tool (WebSocket-heavy)
  • Solo developer yang sudah deep di Node ecosystem (switching cost)

Untuk SMB yang butuh internal tool quick + reliable + hire-able team: bundle ini sudah saya ship 3 kali tanpa major issue.

Ditulis oleh Asti Larasati

// Pick Bundle Recipe lain


← Semua picks RSS feed