Teknologi

Kasir offline-first: cara POS tetap jalan saat internet mati

Bagaimana transaksi tetap tercatat saat internet putus: SQLite lokal, kunci idempotensi, dan antrean gagal-kirim.

Jam tujuh malam, toko ramai, dan internet mati. Pada sistem kasir cloud-only, antrean berhenti dan pelanggan pergi. Pada sistem offline-first, tidak ada yang menyadari apa pun terjadi.

Perbedaannya bukan soal kualitas koneksi, melainkan soal di mana kebenaran disimpan saat transaksi terjadi. Tulisan ini menjelaskan bagaimana kami membangunnya — termasuk satu keputusan yang awalnya kami ambil dengan cara yang keliru.

Prinsipnya: tulis lokal dulu, kirim belakangan

Setiap perangkat kasir membawa database SQLite lokal. Transaksi ditulis ke sana lebih dulu — commit lokal adalah kebenaran pada saat itu — lalu antrean sinkronisasi mengirimkannya ke server begitu koneksi tersedia.

[POS] → tulis ke SQLite lokal → antrean sync → server pusat
         (selalu berhasil)      (saat online)

Urutan ini yang menentukan segalanya. Sistem cloud-only membaliknya: kirim dulu, baru anggap transaksi terjadi. Begitu jaringan hilang, kasir kehilangan kemampuan menjual — bukan karena datanya rusak, melainkan karena arsitekturnya menaruh sumber kebenaran di tempat yang sedang tidak bisa dijangkau.

Apa yang dibawa perangkat, dan apa yang tidak

Perangkat tidak menyimpan salinan seluruh sistem. Ia membawa secukupnya untuk menyelesaikan satu transaksi penjualan:

Data lokalSifat
Produk dan varianSalinan baca-saja dari server
Daftar harga untuk kanal toko ituSalinan baca-saja
Data pelanggan untuk pencarianSalinan baca-saja
Komponen bundelSalinan baca-saja
Transaksi yang belum terkirimDitulis lokal, didorong saat online
Data shift kasirDitulis lokal, disinkron saat online

Pembagian ini disengaja. Semua yang bersifat master mengalir satu arah dari server ke perangkat, sehingga perangkat tidak pernah menjadi pihak yang berhak mengubahnya. Yang ditulis perangkat hanya hal-hal yang memang lahir di sana: transaksi dan shift.

Yang sengaja tidak boleh dikerjakan offline

Sama pentingnya dengan daftar di atas. Tidak semua operasi aman dijalankan tanpa server, dan sistem yang jujur mengakuinya alih-alih berpura-pura.

Pembatalan transaksi, misalnya, membutuhkan persetujuan manajer. Menjalankannya offline berarti membuka celah kontrol yang serius, jadi permintaannya diantrekan sampai koneksi kembali. Prinsipnya sederhana: operasi yang butuh otorisasi tidak boleh kehilangan otorisasinya hanya karena jaringan sedang mati.

Tiga masalah yang harus diselesaikan

Pengiriman ganda

Koneksi yang putus di tengah pengiriman bisa membuat satu transaksi terkirim dua kali. Ini bukan kemungkinan teoretis; di jaringan yang buruk, ini rutin.

Solusinya kunci idempotensi. Setiap transaksi membawa UUID yang dibuat di perangkat, dan server memperlakukan UUID itu sebagai identitas final: kalau transaksi dengan UUID tersebut sudah ada, pengiriman ulang tidak melakukan apa-apa, dan server mengembalikan kode transaksi yang sudah tersimpan.

Efeknya, mengirim ulang selalu aman. Perangkat tidak perlu tahu apakah pengiriman sebelumnya berhasil — ia cukup mengirim lagi.

Konflik, dan mengapa sebagian besar tidak pernah terjadi

Ini bagian yang paling sering disalahpahami, termasuk oleh kami sendiri di rancangan awal.

Kami sempat merancang mekanisme penyelesaian konflik yang rumit, lengkap dengan penataan ulang urutan peristiwa antar-perangkat. Rancangan itu akhirnya dibuang, karena ternyata menyelesaikan masalah yang tidak ada.

Transaksi bersifat append-only, dan tiap satuannya punya UUID unik. Dua kasir di dua toko yang sama-sama offline tidak sedang mengubah baris data yang sama — masing-masing menciptakan catatan baru yang berdiri sendiri. Tidak ada yang saling menimpa, jadi tidak ada yang perlu didamaikan.

Yang benar-benar butuh aturan adalah data master: harga berubah di pusat selagi perangkat offline. Di sini server selalu menang. Perangkat tidak pernah memenangkan perselisihan tentang data yang bukan miliknya.

Pelajarannya: sebagian besar kerumitan sinkronisasi bisa dihindari dengan memilih bentuk data yang tidak bisa berkonflik — bukan dengan membangun mesin pendamai yang canggih.

Kegagalan yang menetap

Sebagian pengiriman gagal bukan karena jaringan, melainkan karena datanya sendiri bermasalah. Mengulanginya seribu kali tidak akan menolong.

Karena itu percobaan ulang punya batas dan jarak yang melebar — sekitar sepuluh detik, lalu setengah menit, lalu satu setengah menit. Setelah percobaan ketiga gagal, transaksi dipindahkan ke antrean khusus dan dimunculkan ke manajer toko, bukan dibiarkan mengendap diam-diam di perangkat.

Bagian terakhir itu yang penting. Sistem sinkronisasi yang gagal tanpa memberi tahu siapa pun jauh lebih berbahaya daripada sistem yang jelas-jelas mati, karena semua orang telanjur mengira angkanya benar.

Kasir harus tahu ia sedang offline

Keputusan desain yang mudah dilewatkan: status koneksi harus terlihat, dan tidak boleh biner.

Di antara "online" dan "mati total" ada keadaan ketiga yang jauh lebih sering terjadi di lapangan — koneksi yang masih hidup tapi lambat. Kalau respons server mulai memakan waktu lebih dari beberapa detik, sistem memperlakukannya sebagai kondisi menurun dan mulai mengantre tulisan sebagai tindakan berjaga, sebelum koneksi benar-benar putus.

Kasir melihat indikator status beserta jumlah transaksi yang belum terkirim. Kalau ada yang gagal, spanduk peringatan menetap di layar sampai persoalannya selesai. Kasir tidak diminta melakukan apa pun — informasi itu untuk manajer — tapi tidak ada lagi yang menutup shift dengan asumsi keliru bahwa semuanya sudah terkirim.

Hasilnya di lapangan

Transaksi tetap tercatat, shift tetap bisa ditutup, dan rekonsiliasi kas tetap akurat — dengan atau tanpa internet.

Bagi kami ini bukan fitur premium. Di negara dengan kondisi jaringan seperti Indonesia, ini syarat minimum sistem kasir yang layak dipakai. Yang membedakan sistem bagus dari sistem biasa bukan perilakunya saat semua normal, melainkan saat ada yang tidak normal — dan seberapa jujur ia memberi tahu Anda soal itu.