Bukan sekadar CRUD bootcamp, ini produk SaaS multi-tenant nyata: ada landing page, paket harga, pembayaran langganan, dan puluhan tenant bisa jalan berdampingan di satu instance yang sama.
- Full-stack production app, bukan tutorial project: multi-tenant SaaS, payment gateway asli (bukan sandbox iseng), AI terintegrasi ke data bisnis real-time, dan sudah live dipakai lewat demo publik.
- Dikerjakan solo, dari nol sampai deploy, sebagai final project Full Stack Web Development Bootcamp 2026, desain, database, backend API, frontend, integrasi pihak ketiga, testing, sampai infrastruktur produksi.
- Bagian paling layak dilihat kalau waktu terbatas: Tantangan Teknis & Solusi, di situ terlihat cara berpikir saat debugging masalah production yang tidak diajarkan langsung di kelas.
KasirAI adalah SaaS Point of Sale (POS) berbasis web untuk UMKM Indonesia. Setiap toko yang daftar jadi satu tenant terisolasi dengan datanya sendiri, bisa langsung pakai kasir, kelola stok, terima pembayaran digital via Midtrans (QRIS, GoPay, OVO), kirim struk otomatis ke WhatsApp pelanggan, dan tanya-jawab soal bisnisnya ke AI Assistant berbahasa Indonesia. Ada landing page publik, paket harga (Free/Pro/Enterprise), dan alur upgrade langganan yang dibayar sungguhan lewat Midtrans.
"Saya tidak hanya belajar coding, saya membangun produk SaaS yang punya model bisnis, bukan cuma fitur."
Kebanyakan project bootcamp berhenti di CRUD. KasirAI melangkah lebih jauh ke arah produk SaaS yang bisa dijual:
| Fitur | Tingkat Kompleksitas |
|---|---|
| Multi-tenant architecture dengan Laravel Global Scope | |
| Subscription billing (Free/Pro/Enterprise) via Midtrans + webhook | |
| AI Assistant dengan dual-provider + auto fallback | |
| Per-tenant Midtrans keys (split payment model) | |
| Cloudflare R2 cloud storage + backend proxy (bypass SSL) | |
| Shift management per-tenant, custom time range + realtime enforcement | |
| Landing page marketing dengan Framer Motion (parallax, reveal, stagger) | |
| Server-side WhatsApp proxy (token tidak expose ke browser) | |
| E2E Testing dengan TestSprite (20/20 PASSED) |
| Teknologi | Kegunaan |
|---|---|
| Next.js 14 (App Router) | Framework utama, SSR, routing, API routes |
| React 18 | UI component-based |
| Tailwind CSS 3.4 | Styling, custom neobrutalism design system |
| Framer Motion | Animasi halaman, landing page (parallax/reveal/stagger), 3D card flip |
| Zustand 5 | Global state management (cart, auth, AI, subscription) |
| Axios | HTTP client dengan Bearer token interceptor |
| Recharts | Chart interaktif untuk dashboard analytics |
| Lucide React | Icon library |
| Teknologi | Kegunaan |
|---|---|
| Laravel 11 | REST API framework |
| MySQL 8 | Database relasional, ACID compliant |
| Laravel Sanctum | Token-based authentication |
| Laravel Global Scope | Multi-tenant data isolation otomatis |
| DomPDF | Generate laporan PDF server-side |
| Maatwebsite Excel | Export data ke .xlsx |
| Midtrans PHP SDK | Payment gateway, transaksi kasir & subscription billing |
| Platform / Service | |
|---|---|
| Frontend | Vercel + Speed Insights |
| Backend | Render (Docker + Nginx + PHP-FPM) |
| Database | TiDB Cloud MySQL |
| AI Primary | Groq API, LLaMA 3.3 70B (gratis & cepat) |
| AI Fallback | OpenRouter, LLaMA 3.1 8B (auto-switch) |
| Payment | Midtrans Snap (Production), QRIS, GoPay, OVO, VA, kartu kredit |
| Storage | Cloudflare R2, upload & serve foto produk (S3-compatible) |
| Fonnte API, struk digital otomatis | |
| Analytics & SEO | Google Analytics 4, sitemap/robots/OG image di-generate dinamis (Next.js Metadata API) |
| Domain | sikasirai.com (custom domain), DNS dikelola manual via Domainesia |
Bagian ini merangkum masalah nyata yang muncul saat membangun & men-deploy KasirAI ke production, dan bagaimana saya menyelesaikannya.
Masalah: Kalau isolasi tenant ditulis manual di setiap controller, satu query yang lupa filter tenant = kebocoran data pelanggan lain. Ini fatal untuk SaaS.
Solusi: Laravel Global Scope (TenantScope) yang otomatis inject WHERE tenant_id = ? ke semua model tenant-aware, di level query builder, bukan di controller. Developer (role khusus, tenant_id = null) tetap bisa melihat lintas tenant untuk keperluan support/monitoring.
// TenantScope.php, aktif otomatis di semua model: Product, Order, Shift, dll
public function apply(Builder $builder, Model $model): void
{
if (Auth::check() && Auth::user()->tenant_id !== null) {
$builder->where($model->getTable() . '.tenant_id', Auth::user()->tenant_id);
}
}Masalah: Groq API gratis dan cepat, tapi ada limit request per menit. Kalau limit kena saat tenant lagi butuh, fitur AI Assistant langsung error ke user.
Solusi: Dual-provider dengan circuit breaker pattern pakai Laravel Cache. Saat Groq melempar error rate-limit, sistem set flag cooldown 65 detik dan otomatis alihkan semua request ke OpenRouter tanpa user sadar ada masalah, tanpa perlu restart atau intervensi manual.
public function ask(string $systemPrompt, string $userQuery): array
{
if (Cache::get('groq_rate_limited', false)) {
return $this->callOpenRouter($systemPrompt, $userQuery);
}
try {
return $this->callGroq($systemPrompt, $userQuery);
} catch (\Exception $e) {
if ($this->isRateLimitError($e)) {
Cache::put('groq_rate_limited', true, 65);
return $this->callOpenRouter($systemPrompt, $userQuery);
}
throw $e;
}
}Masalah: Model payment gateway biasa cuma punya satu server key terpusat, semua uang masuk ke satu akun. Untuk SaaS multi-merchant, tiap tenant butuh split payment ke akun Midtrans masing-masing, dan credential itu sensitif.
Solusi: Server key per-tenant disimpan terenkripsi di database (encrypted cast Eloquent), dengan fallback ke key platform kalau tenant belum setting sendiri. Client key dikirim ke frontend untuk load Snap.js secara dinamis per tenant.
private function configureServerKey(Request $request): void
{
$serverKey = $request->user()->tenant?->midtrans_server_key
?? config('services.midtrans.server_key');
if (empty($serverKey)) {
abort(422, 'Midtrans belum dikonfigurasi. Atur key di halaman Profil.');
}
Config::$serverKey = $serverKey;
}4. "Setelah pindah dari sandbox ke Midtrans production, alur upgrade langganan malah gagal untuk sebagian tenant."
Masalah: Saat migrasi ke Midtrans production, konfigurasi key campur antara key platform (untuk billing subscription internal) dan key per-tenant (untuk transaksi kasir tenant), keduanya sempat saling menimpa sehingga webhook subscription tidak match dengan konteks Midtrans yang benar.
Solusi: Dipisah tegas dua jalur konfigurasi Midtrans: SubscriptionController selalu pakai server key platform (karena yang menagih adalah KasirAI, bukan tenant), sementara TransactionController resolve key per-tenant saat runtime. Webhook subscription juga difilter berdasar prefix order_id (SUB-) supaya tidak pernah tercampur dengan notifikasi transaksi kasir tenant manapun.
5. "Foto produk disimpan di Cloudflare R2, tapi browser menolak load karena masalah SSL/CORS di custom domain."
Masalah: R2 public URL butuh custom domain + sertifikat sendiri untuk bisa diakses langsung dari browser dengan aman, tidak praktis untuk skala kecil dan menambah biaya infrastruktur.
Solusi: Semua request gambar diproksikan lewat backend Laravel, jadi URL R2 asli tidak pernah terekspos ke publik dan tidak butuh domain SSL tambahan.
Route::get('/media/{path}', function (string $path) {
$disk = !empty(config('filesystems.disks.r2.key')) ? 'r2' : 'public';
if (! Storage::disk($disk)->exists($path)) abort(404);
return Storage::disk($disk)->response($path);
})->where('path', '.*');6. "Satu toko punya banyak kasir bergantian shift, gimana caranya transaksi tetap tercatat rapi tanpa kasir harus login ulang tiap ganti orang?"
Masalah: Shift biasanya didesain per-user. Tapi realita warung/toko: satu perangkat kasir dipakai bergantian, dan pemilik ingin satu shift aktif yang dipakai bersama, dengan jam operasional yang bisa mereka atur sendiri (bukan hardcode 8 jam).
Solusi: Shift dibuat per-tenant (bukan per-user), dengan jam mulai/selesai custom yang didefinisikan pemilik toko. Sistem melakukan realtime enforcement, transaksi otomatis diblokir kalau di luar jam shift, lengkap dengan banner peringatan. Saat login dan shift masih dalam jam aktif, sistem auto-resume tanpa perlu isi ulang form buka shift. Saat tutup shift, selisih kas dihitung otomatis dari kalkulator pecahan uang fisik.
Seharusnya = Modal Awal + Total Penjualan Tunai − Kas Kecil
Selisih = Saldo Fisik (dari hitung pecahan) − Seharusnya
Masalah: Kalau dipanggil langsung dari client, token pihak ketiga bisa dicuri lewat Network tab browser siapa saja.
Solusi: Kedua integrasi selalu lewat lapisan server, WhatsApp lewat Next.js API Route (token di env server Next.js), Midtrans server key hanya pernah dipakai di controller Laravel, tidak pernah dikirim ke response API.
8. "Webhook Midtrans untuk tenant yang pakai server key sendiri diam-diam selalu gagal, status transaksi tidak pernah ter-update otomatis."
Masalah: SDK midtrans/midtrans-php versi yang dipakai ternyata memanggil balik API Midtrans (Transaction::status()) di dalam constructor Notification, memakai Config::$serverKey yang aktif saat itu, bukan sekadar mencocokkan hash signature secara lokal seperti dugaan awal. Karena webhook() tidak pernah set server key sebelum verifikasi, request selalu memakai key default platform, sehingga verifikasi gagal total untuk tenant yang pakai Midtrans sendiri.
Solusi: Sebelum bikin instance Notification, sistem cari dulu Transaction + tenant pemiliknya dari order_id mentah di body request, lalu set Config::$serverKey sesuai tenant tersebut. Diverifikasi dengan simulasi notifikasi nyata (server key custom vs default) sebelum dinyatakan beres, bukan cuma asumsi dari membaca kode.
// TransactionController::webhook(), urutan ini krusial
$transaction = Transaction::with('order.tenant')
->where('midtrans_order_id', $request->input('order_id'))
->first();
$this->configureServerKeyForTenant($transaction->order?->tenant); // set key dulu
$notification = new Notification(); // baru verifikasi, SDK panggil API pakai key di atasMasalah: Backend cuma pernah menghitung satu rentang waktu (whereMonth) dan mengirimnya ke LLM apa pun pertanyaan user. AI tidak salah paham bahasa, dia memang cuma pernah dikasih satu angka, jadi terpaksa "menebak" label periode yang diminta.
Solusi: Hitung 3 rentang waktu sekaligus (hari ini/minggu ini/bulan ini, pakai whereBetween bukan whereMonth) dan kirim semuanya ke LLM dengan instruksi eksplisit untuk mencocokkan periode sesuai kata di pertanyaan user. Divalidasi dengan skenario nyata: buat transaksi dummy di 3 rentang waktu berbeda, tanya AI satu-satu, pastikan jawabannya berbeda dan sesuai, bukan cuma percaya kode "kelihatan benar".
- Halaman marketing publik: Hero, Problem, Features, How It Works, AI Spotlight, Testimonials, Pricing, CTA
- Animasi scroll (parallax, reveal, stagger) dengan Framer Motion
- Alur daftar → pilih paket → dashboard, siap dipakai calon pelanggan asli
- 3 paket dengan harga terpusat di backend (
SubscriptionController::PRICES), frontend membaca angka yang sama saat initiate, jadi tidak mungkin mis-match:- Free (Rp 0): 1 outlet, maks. 50 produk & 15 kategori, transaksi tanpa batas, 5 prompt AI/bulan, pembayaran tunai saja (tanpa QRIS/digital, tanpa export PDF/Excel)
- Pro, Rp 129.000/bulan (Rp 100.000/bulan jika bayar tahunan): produk/kategori unlimited, transaksi tak terbatas, AI Assistant 10 prompt/hari, export laporan PDF/Excel, QRIS & e-wallet
- Enterprise, Rp 499.000/bulan (Rp 399.000/bulan jika bayar tahunan): semua fitur Pro + AI Assistant 50 prompt/hari, outlet unlimited, API & integrasi kustom, account manager, SLA
- Enforcement berlapis: backend memblokir (QRIS free → 422
plan_required, export free → 403, AI free > 5/bulan → 429limit_reached), frontend juga memblokir/menyembunyikan tombolnya - Kasir mengikuti plan admin tenant-nya (
effective_plandi response/me), bukan plan kolom user sendiri - Upgrade paket dibayar langsung via Midtrans Snap (bulanan/tahunan, harga tahunan diskon)
- Webhook otomatis aktivasi paket + upgrade role user begitu pembayaran
settlement - User bisa batalkan transaksi pending sendiri; developer bisa monitor semua tenant, ubah plan, atau suspend akun dari panel khusus
- Split-screen: grid produk + keranjang belanja
- Search real-time dengan debounce 400ms, filter per kategori
- Validasi stok real-time saat tambah ke keranjang
- Bayar Tunai, kalkulasi kembalian otomatis
- Bayar Digital, Midtrans Snap (QRIS, GoPay, OVO, VA, Kartu Kredit)
- Struk digital → cetak / kirim WhatsApp
- Panel kelola produk langsung dari kasir (slide-over)
- Shift per-tenant, satu shift aktif dipakai bersama semua kasir
- Auto-resume saat login dalam jam shift aktif
- Form buka shift: nama custom + preset (Pagi/Siang/Malam) + jam custom + kalkulator denominasi + modal awal
- Realtime enforcement, transaksi diblokir di luar jam shift
- Tutup shift 6 section: identitas, ringkasan penjualan, breakdown pembayaran, hitung kas fisik, kas kecil, rekonsiliasi otomatis dengan indikator selisih warna
- Riwayat shift lengkap per-shift
- Revenue, order, stok kritis, total produk, stat cards real-time
- Line chart tren 7 hari, bar chart top produk, pie chart metode pembayaran
- 10 transaksi terbaru
- Chat bahasa Indonesia, tanya apa saja tentang bisnis
- Sadar periode waktu, data penjualan dihitung terpisah per hari ini/minggu ini/bulan ini di setiap request, jadi AI tidak salah kira konteks waktu yang ditanya user
- Backend inject katalog & stok produk lengkap sebagai konteks AI (bukan cuma ringkasan)
- 3 mode: analisis penjualan, prediksi stok habis, rekomendasi bundling
- Badge provider aktif: Groq / FALLBACK OpenRouter
- Kuota AI per paket (Free dihitung per bulan kalender, Pro/Enterprise per hari): Free = 5 prompt/bulan (badge peringatan saat sisa tipis), Pro = 10 prompt/hari, Enterprise = 50 prompt/hari, tersedia untuk admin & kasir
- Filter: hari ini, 7 hari, 30 hari, custom range
- Download PDF & Excel (generate di backend)
- Tab penjualan + tab stok
- CRUD lengkap + upload foto
- Badge stok: Normal / Menipis / Habis
- Update status order, void, batalkan transaksi
- CRUD user dengan role-based guard (Developer only)
- Panel developer: monitor semua tenant & status langganan, ubah plan, suspend/aktifkan akun
- Dashboard monitoring penggunaan AI: token, provider, per-user quota
| Fitur | user | kasir | admin | developer |
|---|---|---|---|---|
| POS + Shift Management | ||||
| Kelola Produk & Kategori | , | |||
| Pesanan & Transaksi | , | |||
| Dashboard & Laporan | , | , | ||
| Konfigurasi Midtrans | , | , | ||
| AI Assistant | , | |||
| AI Monitoring | , | , | ||
| User Management | , | , | , | |
| Kelola Tenant & Subscription (semua toko) | , | , | , |
Diuji menggunakan TestSprite, AI testing agent yang menjalankan test end-to-end di browser nyata.
Hasil TestSprite: 20/20 test PASSED
| Test Flow | Verdict |
|---|---|
| Login & autentikasi | PASSED |
| Buka shift dengan kalkulator pecahan | PASSED |
| Checkout tunai + struk digital | PASSED |
| Tutup shift + rekonsiliasi kas | PASSED |
| Edge: checkout tanpa shift (harus ditolak) | PASSED |
| Edge: tombol bayar disabled saat cart kosong | PASSED |
| Search produk real-time | PASSED |
| Riwayat shift | PASSED |
PHPUnit (backend): 22/22 test PASSED , php artisan test di folder backend/ (memakai SQLite :memory:, termasuk test isolasi tenant AI/report, shift per-tenant, gating plan Free vs Pro (kuota AI harian & bulanan), dan sinkronisasi harga paket).
cd backend
composer install
cp .env.example .env
# Isi .env:
# DB_CONNECTION=mysql
# DB_DATABASE=kasirai
# GROQ_API_KEY=gsk_...
# MIDTRANS_SERVER_KEY=Mid-server-... # production (SB-Mid-server-... untuk sandbox)
# MIDTRANS_CLIENT_KEY=Mid-client-... # production (SB-Mid-client-... untuk sandbox)
# MIDTRANS_IS_PRODUCTION=true
php artisan key:generate
php artisan migrate --seed
php artisan storage:link
php artisan servecd frontend
npm install
# Buat .env.local:
# NEXT_PUBLIC_API_URL=http://localhost:8000/api
# NEXT_PUBLIC_MIDTRANS_CLIENT_KEY=Mid-client-...
# NEXT_PUBLIC_MIDTRANS_SNAP_URL=https://app.midtrans.com/snap/snap.js
# FONNTE_TOKEN=...
npm run devAda juga
dev.ps1di root repo untuk menjalankan backend & frontend sekaligus di Windows.
| Layer | Implementasi |
|---|---|
| Auth | Laravel Sanctum Bearer Token |
| Otorisasi | RoleMiddleware per route group |
| Isolasi Data | TenantScope, WHERE tenant_id = ? otomatis |
| Payment Key | Server Key dienkripsi di DB (encrypted cast) |
| Webhook Payment | Validasi prefix order_id + update status dalam DB transaction |
| WhatsApp Token | Server-side only, tidak pernah ke browser |
| File Upload | Validasi MIME + max 2MB, disimpan di Cloudflare R2 |
| Gambar Produk | Diproksikan via backend, URL R2 tidak pernah terekspos ke browser |
| SQL Injection | Eloquent ORM + parameter binding |
|
Saifudin Reza Full Stack Developer Final Project, Full Stack Web Development Bootcamp 2026 donojomi@gmail.com |
Next.js 14 App Router React 18 Laravel 11 PHP 8.3 MySQL REST API Design
Zustand Tailwind CSS Framer Motion Neobrutalism Design System
Multi-tenant Architecture Laravel Sanctum Laravel Global Scope
SaaS Subscription Billing Groq AI Integration OpenRouter Fallback LLM Rate Limiting
Midtrans Split Payment (Production) WhatsApp API PDF & Excel Export
Cloudflare R2 (S3-compatible) Backend Media Proxy Cloud Storage
Shift Management & Cash Reconciliation Realtime Time Enforcement
Role-based Access Control E2E Testing (TestSprite) Docker Vercel Render
Coba Live Demo → sikasirai.com
Dibangun dengan sepenuh hati, Mei 2026–sekarang · Production ready