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.
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:
- Kontraktor di Tangerang (inventory + project tracking)
- Klinik dental (patient management + scheduling)
- 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:
-
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.
-
Laravel ecosystem mature: form handling, validation, auth, queue, file upload, email — semua first-party + battle-tested. Less third-party glue code.
-
MySQL/Postgres hosting: PHP-MySQL deployment standard di Indonesia hosting (Niagahoster, IDcloudhost, dll). Kalau klien Anda already pakai cPanel hosting, Laravel deploy familiar.
-
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:
- Laravel API + React SPA terpisah: full SPA, butuh JWT auth, CORS handling, state management complex.
- Inertia.js: Laravel render controller return Inertia response, React render page. Feels SPA tanpa API complexity.
- 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:
| Item | Cost (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