C:\ARDI\PROJECTS\APONK_RED_POS> type case-study.txt
Aponk Red POS: selling when the internet drops
Aponk Red POS: tetap jualan saat internet putus
A point-of-sale and stock app for my family's arowana fish store. Each fish is one of a kind and can be worth millions of rupiah, so every one is tracked individually, and cash sales keep working offline.
Aplikasi kasir dan stok untuk toko ikan arwana keluarga saya. Setiap ekor ikan unik dan harganya bisa jutaan rupiah, jadi dilacak satu per satu, dan penjualan tunai tetap jalan saat offline.
My role: sole developer: requirements with the store, data model, app, deployment and support. Stack: Next.js 16 (App Router), React 19, TypeScript, Tailwind CSS, Supabase (Postgres + Storage), IndexedDB, PWA, Vercel.
Peran saya: developer tunggal: kebutuhan bersama toko, model data, aplikasi, deployment, dan dukungan. Stack: Next.js 16 (App Router), React 19, TypeScript, Tailwind CSS, Supabase (Postgres + Storage), IndexedDB, PWA, Vercel.
01 The briefKebutuhannya
- The store sells two very different things: individual arowana, each with its own ID, tank, size, photo and price, and everyday products like feed and vitamins with size variants and stock counts.
- A sale must never be lost, so cash sales have to work when the connection drops.
- At closing time the cash in the drawer must match what the system expects, and any difference must be recorded.
- It has to run on the phones and tablets the store already owns, with nothing to install from an app store.
- Toko ini menjual dua hal yang sangat berbeda: ikan arwana per ekor, masing-masing dengan ID, akuarium, ukuran, foto, dan harga sendiri, serta produk harian seperti pakan dan vitamin dengan varian ukuran dan jumlah stok.
- Penjualan tidak boleh hilang, jadi transaksi tunai harus tetap jalan saat koneksi putus.
- Saat tutup toko, uang di laci harus cocok dengan perhitungan sistem, dan selisihnya harus tercatat.
- Harus jalan di HP dan tablet yang sudah dimiliki toko, tanpa install dari app store.
02 What I builtYang saya bangun
- POS: sell a specific fish or stock products, apply discounts, take cash or transfer, and print a PDF receipt.
- Per-fish inventory: each fish is one database row that moves from
availabletosold, so the same fish can never be sold twice. - Stock with an audit trail: every stock change writes the quantity before, the change, the quantity after and the reason.
- Expenses and end-of-day cash count: opening balance + cash sales − expenses = expected cash, compared with the counted cash.
- Offline cash sales: queued in IndexedDB and sent automatically when the connection returns.
- Installable PWA for Android and iOS, with sales, expense and inventory reports.
- Kasir: jual ikan tertentu atau produk stok, beri diskon, terima tunai atau transfer, dan cetak struk PDF.
- Inventaris per ekor: tiap ikan adalah satu baris database yang berubah dari
availablekesold, jadi ikan yang sama tidak mungkin terjual dua kali. - Stok dengan jejak audit: setiap perubahan stok mencatat jumlah sebelum, perubahan, jumlah sesudah, dan alasannya.
- Pengeluaran dan tutup kas harian: saldo awal + penjualan tunai − pengeluaran = kas seharusnya, dibandingkan dengan uang yang dihitung.
- Penjualan tunai offline: diantrekan di IndexedDB dan terkirim otomatis saat koneksi kembali.
- PWA yang bisa di-install di Android dan iOS, lengkap dengan laporan penjualan, pengeluaran, dan inventaris.
03 ArchitectureArsitektur
The browser never talks to the database directly. All writes go through server routes that hold the service key.
Browser tidak pernah langsung ke database. Semua penulisan lewat route server yang memegang service key.
04 Key decisionsKeputusan penting
Trust the device, not a user account
A family store does not want to manage passwords. Each phone or tablet is registered once with the store access code (stored as a bcrypt hash), then recognised by a device token.
Fish are rows, products are numbers
A fish is unique, so it is a row with a status. Feed is interchangeable, so it is a stock count on a variant. Two models, because the business really has two kinds of goods.
Only cash goes offline
A transfer has to be checked against the bank, so it needs a connection. A cash sale does not, so only cash is queued.
Keep the original sale time
When a queued sale syncs, it keeps the time it actually happened (rejected if it is in the future), so last night's offline sale lands in last night's report.
Percayai perangkat, bukan akun pengguna
Toko keluarga tidak mau repot mengelola password. Tiap HP atau tablet didaftarkan sekali dengan kode akses toko (disimpan sebagai hash bcrypt), lalu dikenali lewat token perangkat.
Ikan adalah baris, produk adalah angka
Ikan itu unik, jadi disimpan sebagai baris dengan status. Pakan bisa ditukar, jadi disimpan sebagai jumlah stok per varian. Dua model, karena bisnisnya memang punya dua jenis barang.
Hanya tunai yang bisa offline
Transfer harus dicek ke bank, jadi butuh koneksi. Tunai tidak, jadi hanya tunai yang diantrekan.
Pertahankan waktu asli transaksi
Saat antrean tersinkron, transaksi tetap memakai waktu aslinya (ditolak kalau di masa depan), jadi penjualan offline semalam masuk ke laporan semalam.
05 Bugs worth tellingBug yang layak diceritakan
The day started at 7 a.m.
Daily totals used UTC day boundaries. In Batam (UTC+7) that meant the "business day" really began at 07:00 local time, so some sales were counted on the wrong day. I moved every daily calculation onto one function that uses the store's local business day.
A late sale erased the cash count
If a sale came in after the end-of-day count, the old code "reopened" the register and wiped the counted cash and the difference. Now a closed register stays closed, the screen recalculates the expected cash live, and staff can submit a new count.
Hari dimulai jam 7 pagi
Total harian memakai batas hari UTC. Di Batam (UTC+7) artinya "hari usaha" baru dimulai pukul 07:00 waktu lokal, sehingga sebagian penjualan masuk ke hari yang salah. Semua perhitungan harian saya pindahkan ke satu fungsi yang memakai hari usaha lokal toko.
Penjualan telat menghapus hitungan kas
Kalau ada penjualan setelah tutup kas, kode lama "membuka ulang" kas dan menghapus hasil hitungan serta selisihnya. Sekarang kas yang sudah ditutup tetap tertutup, layar menghitung ulang kas seharusnya secara live, dan staf bisa mengirim hitungan baru.
06 Known limits, and the fixKeterbatasan, dan solusinya
- No real database transaction. One sale is 5–8 separate writes held together by rollback functions. The fix is to move the sale into a single Postgres function so it is atomic.
- No idempotency key on sync. If the server saves a queued sale but the reply is lost, the sale could be sent twice. The fix is to send the local queue ID and let the server reject repeats.
- Belum ada transaksi database sungguhan. Satu penjualan terdiri dari 5–8 penulisan terpisah yang dijaga fungsi rollback. Solusinya: pindahkan penjualan ke satu fungsi Postgres supaya atomik.
- Belum ada kunci idempoten saat sinkronisasi. Kalau server sudah menyimpan penjualan tapi balasannya hilang, penjualan bisa terkirim dua kali. Solusinya: kirim ID antrean lokal dan biarkan server menolak yang berulang.
07 What I learnedPelajaran
- Offline-first is mostly about data integrity, not caching: timestamps, duplicates and partial writes are where the real bugs live.
- Model the business as it is. Two kinds of goods needed two data models.
- Write down your own system's weak points before someone else finds them.
- Offline-first terutama soal integritas data, bukan cache: waktu transaksi, duplikasi, dan penulisan setengah jalan adalah tempat bug sebenarnya.
- Modelkan bisnis apa adanya. Dua jenis barang butuh dua model data.
- Tulis sendiri titik lemah sistemmu sebelum orang lain menemukannya.
Need someone who builds apps that keep working when the network does not?
Butuh orang yang membangun aplikasi yang tetap jalan saat jaringan bermasalah?
WhatsApp Email Download CVUnduh CV More projectsProyek lain

