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.

11family store, in daily usetoko keluarga, dipakai tiap hari
1212database tablestabel database
2424API routes on the serverAPI route di server
22kinds of stock: single fish and productsjenis stok: ikan per ekor dan produk

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

02 What I builtYang saya bangun

pos-dashboard.jpg
POS dashboard with demo sales data

Dashboard — demo data

Dashboard — data demo

pos-stocks.jpg
Stock list with individual arowana and products, demo data

Stock: each fish listed by ID and size

Stok: tiap ikan tercatat dengan ID dan ukuran

pos-eod.jpg
End-of-day cash count screen with demo data

End-of-day cash count

Tutup kas harian

03 ArchitectureArsitektur

aponk_red_pos — architecture
Phones / tabletsregistered devicesNext.js PWAAPI routes · React 19IndexedDBoffline sales queueVercelhostingSupabasePostgres · service roleStoragefish-photos bucketinstalled appqueuesync on reconnectservesphotos

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

07 What I learnedPelajaran

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