Kata Pengantar
rd · monetisasi · 2026
Ada momen yang sunyi dalam hidup setiap pembuat perangkat lunak: kode selesai, aplikasi berjalan mulus di layar, dan tidak ada satu rupiah pun yang masuk. Aplikasi yang hebat bukanlah aplikasi yang jalan; aplikasi yang hebat adalah aplikasi yang dibayar. Perbedaan di antara keduanya bukanlah kualitas kode — melainkan sebuah lapisan tipis yang jarang diajarkan di kelas pemrograman: lisensi, penagihan, dan pembayaran.
Buku ini menutup celah itu. Ia ditulis untuk developer yang sudah bisa membangun aplikasi tetapi belum pernah membangun mesin uang di sekelilingnya — kunci lisensi yang tak bisa dipalsukan, langganan yang menagih otomatis tiap bulan, webhook Midtrans yang mengubah status pembayaran menjadi akses, dan invoice yang sah secara pajak. Semuanya, dari nol.
"Harga adalah apa yang Anda bayar. Nilai adalah apa yang Anda dapatkan."
— Warren Buffett
Kita akan memperlakukan monetisasi sebagai disiplin rekayasa, bukan renungan pemasaran. Setiap bab menyertakan kode siap salin, alur yang bisa langsung diterapkan, dan — di Bab 14 — dua alat interaktif yang hidup di dalam halaman ini: sebuah generator & validator kunci lisensi dan sebuah kalkulator MRR/ARR. Anda bisa mencobanya sekarang, tanpa memasang apa pun.
Solo developer yang menjual aplikasi desktop; tim SaaS yang menyusun tier langganan; konsultan yang menagih lisensi on-premise ke korporat; dan siapa pun yang pernah bertanya "bagaimana caranya orang membayar aplikasiku dengan aman?"
Cara Membaca Buku Ini
peta · konvensi · jalur cepat
Buku ini bisa dibaca lurus dari depan ke belakang, atau dilompati sesuai kebutuhan. Berikut konvensi yang dipakai di seluruh halaman.
Konvensi visual
- § Judul emas menandai konsep inti sebuah bab.
- Blok kode berwarna gelap dengan font monospace — semuanya siap salin, tanpa placeholder tersembunyi.
- Kotak Tip (emerald) berisi praktik yang disarankan; Kotak Peringatan (emas) menandai jebakan umum; Kotak Bahaya (merah) menandai kesalahan yang membuat sistem uang Anda bocor.
- Kotak AI (abu) memberi petunjuk bagaimana asisten AI sebaiknya memakai isi bab tersebut — dijelaskan penuh di Bab 15.
Tiga jalur pembaca
| Kalau Anda... | Mulai dari | Lompati dulu |
|---|---|---|
| Jual app desktop sekali bayar | Bab 2 → 3 → 11 → 12 | Bab 5, 10 |
| Bangun SaaS langganan | Bab 2 → 5 → 6/7 → 8 → 10 | Bab 11 |
| Jual lisensi ke korporat Indonesia | Bab 3 → 4 → 6 → 9 → 13 | Bab 7 |
Apa pun jalurnya, Bab 14 (alat interaktif) dan Bab 16 (checklist rilis) layak dikunjungi di akhir.
Dari Bikin App ke Jual App
Sebagian besar developer memperlakukan pembayaran sebagai renungan terakhir — sesuatu yang "nanti dipasang kalau sudah ada yang mau beli". Ini terbalik. Sistem penjualan bukan aksesori; ia adalah antarmuka antara nilai yang Anda ciptakan dan uang yang Anda terima. Bab ini menata ulang cara pandang: memperlakukan lisensi, penagihan, dan pembayaran sebagai fitur produk kelas satu, dengan bug-nya sendiri, keamanannya sendiri, dan kegembiraannya sendiri saat pertama kali sebuah webhook berbunyi "settlement".
Kenapa Monetisasi Terasa Sulit
Membangun aplikasi itu deterministik: Anda menulis kode, ia jalan atau tidak. Menjualnya melibatkan pihak-pihak yang tak Anda kendalikan — bank, gateway pembayaran, pembeli yang lupa membayar, penipu yang mencoba membajak. Kompleksitasnya bukan pada satu bagian, melainkan pada rantai-nya: sebuah pembelian harus menghasilkan sebuah kunci, kunci itu harus divalidasi, validasi harus tahan pemalsuan, langganan harus menagih ulang, penagihan gagal harus ditangani dengan anggun, dan seluruh jejaknya harus sah untuk pajak.
"Jika Anda tidak menemukan cara untuk menghasilkan uang saat tidur, Anda akan bekerja sampai mati."
— Warren Buffett
Kabar baiknya: setiap mata rantai itu sudah punya pola baku. Anda tidak sedang menemukan sistem perbankan; Anda sedang merangkai balok-balok yang teruji. Buku ini adalah katalog balok tersebut.
Anatomi Sebuah Sistem Penjualan
Apa pun modelnya, sebuah sistem penjualan perangkat lunak punya lima komponen yang selalu muncul. Kenali kelimanya sekarang; sisa buku ini hanyalah pendalaman masing-masing.
| Komponen | Tugas | Dibahas di |
|---|---|---|
| Katalog & Harga | Mendefinisikan apa yang dijual dan berapa (tier, paket, kuota). | Bab 2, 14 |
| Entitlement | Menyatakan siapa berhak atas apa — kunci lisensi atau baris langganan. | Bab 3, 4 |
| Billing | Menagih uang: sekali, berulang, atau berdasarkan pemakaian. | Bab 5, 10 |
| Payment Gateway | Memindahkan uang sungguhan (Midtrans, Stripe) dan mengabari via webhook. | Bab 6, 7, 8 |
| Dokumen & Legal | Invoice, kwitansi, pajak — bukti sah bahwa transaksi terjadi. | Bab 9, 13 |
Ketika membantu merancang monetisasi, mulailah dengan menanyakan model bisnis (Bab 2) sebelum menyarankan gateway. Salah memilih model membuat seluruh implementasi teknis sia-sia.
Model Lisensi
Sebelum satu baris kode ditulis, sebuah keputusan menentukan segalanya: bagaimana bentuk uang mengalir? Sekali bayar untuk selamanya, atau sedikit-sedikit tiap bulan? Model lisensi bukan hanya soal harga — ia menentukan arsitektur teknis, hubungan dengan pelanggan, dan bahkan kesehatan arus kas Anda selama bertahun-tahun.
Perpetual (Sekali Bayar, Selamanya)
Pelanggan membayar sekali dan memiliki versi itu selamanya. Model klasik perangkat lunak desktop. Sederhana secara moral ("saya beli, saya punya") tetapi berat secara arus kas: Anda harus terus mencari pembeli baru untuk tumbuh, karena pembeli lama tak membayar lagi.
- Cocok untuk: aplikasi desktop, plugin, tool sekali pakai, produk yang murah dipelihara.
- Varian umum: "perpetual + update 1 tahun" — lisensi selamanya, tetapi pembaruan hanya gratis setahun; setelahnya beli upgrade.
- Teknisnya: paling cocok dengan kunci lisensi ber-signature (Bab 3) yang bisa divalidasi offline.
"Adalah jauh lebih baik membeli perusahaan hebat dengan harga wajar ketimbang perusahaan biasa dengan harga hebat."
— Warren Buffett
Subscription (Langganan)
Pelanggan membayar berulang — bulanan atau tahunan — dan berhenti mengakses saat berhenti membayar. Mesin di balik hampir semua SaaS modern. Menghasilkan pendapatan yang bisa diprediksi (MRR/ARR), tetapi menuntut nilai yang berkelanjutan: pelanggan mengevaluasi ulang setiap siklus.
| Aspek | Perpetual | Subscription |
|---|---|---|
| Arus kas | Lonjakan di muka | Rata & dapat diprediksi |
| Metrik utama | Unit terjual | MRR, churn, LTV |
| Beban server | Rendah (offline OK) | Perlu server lisensi aktif |
| Risiko | Harus terus cari pembeli baru | Churn menggerus pertumbuhan |
Untuk subscription, dua metrik menjadi napas: MRR (Monthly Recurring Revenue) dan churn (persentase pelanggan yang pergi tiap bulan). Kalkulator di Bab 14 membiarkan Anda memainkan angka-angka ini secara langsung.
Usage-based (Bayar Sesuai Pemakaian)
Pelanggan membayar sebanding dengan yang mereka pakai — jumlah panggilan API, GB data, menit komputasi, token diproses. Adil dan selaras dengan nilai, tetapi menuntut metering yang akurat (Bab 10) dan membuat tagihan sulit diprediksi pelanggan.
- Pola hibrida populer: biaya dasar bulanan + kelebihan pemakaian (mis. "$29 termasuk 10.000 request, lalu $1 per 1.000 berikutnya").
- Jebakan: tagihan kejut membuat pelanggan marah. Selalu sediakan dasbor pemakaian real-time dan peringatan kuota.
Freemium
Sebuah tingkat gratis selamanya menarik banyak pengguna; sebagian kecil naik ke tier berbayar. Bukan model penagihan melainkan strategi akuisisi yang ditumpangkan pada subscription/usage. Kuncinya adalah garis pembatas yang jujur: fitur gratis harus benar-benar berguna, fitur berbayar harus benar-benar bernilai.
Jika tingkat gratis terlalu murah hati, tak ada yang upgrade. Jika terlalu pelit, tak ada yang datang. Ukur tingkat konversi free→paid (patokan sehat SaaS: 2–5%) dan sesuaikan garisnya, bukan harganya.
Memilih model: pohon keputusan singkat
# pseudo-decision
if nilai_produk == "terus bertambah tiap bulan":
return "subscription"
elif biaya_variabel_tinggi (API, komputasi):
return "usage-based" # atau hibrida
elif produk == "tool desktop, jarang update":
return "perpetual (+ update 1 thn)"
else:
return "freemium di atas subscription"
Anda boleh mengubah model, tetapi mahal. Mulailah dengan model yang paling cocok dengan bentuk nilai produk Anda, bukan yang paling populer di Twitter.
License Key
Kunci lisensi adalah kalimat pertama dalam percakapan antara aplikasi Anda dan uang. Ia harus mudah dibaca manusia, tahan salah ketik, mustahil dipalsukan, dan — idealnya — bisa memberi tahu aplikasi apa yang boleh dilakukan pemiliknya bahkan tanpa internet. Bab ini membangun kunci semacam itu dari nol, lengkap dengan kode yang bisa Anda salin.
Format Key yang Manusiawi
Format klasik XXXX-XXXX-XXXX-XXXX bukan sekadar estetika. Pengelompokan empat-empat memudahkan mata, tanda hubung mencegah salah baca, dan pilihan alfabet menentukan keramahannya. Aturan emas: buang huruf ambigu — I, L, O, U — karena tertukar dengan 1, 0, dan V saat diketik ulang dari email.
| Keputusan | Pilihan umum | Alasan |
|---|---|---|
| Alfabet | Crockford Base32 (32 simbol tanpa I,L,O,U) | Ramah manusia, padat, tahan salah baca |
| Panjang | 16–25 simbol | Cukup entropi tanpa menyiksa pengetik |
| Pemisah | Tanda hubung tiap 4–5 simbol | Mengelompokkan agar mudah dibaca |
| Grup terakhir | Checksum | Menolak salah ketik sebelum menyentuh server |
Alat interaktif di Bab 14 memakai persis format ini: tiga grup payload ditambah satu grup checksum. Anda bisa membongkar cara kerjanya di sana.
Signing & HMAC — Kunci yang Membawa Bukti
Ada dua filosofi menyimpan hak pada sebuah kunci:
1. Kunci sebagai penanda (opaque token)
Kunci hanyalah nomor acak; seluruh arti ("Pro, kadaluarsa 2027, 3 perangkat") disimpan di database server. Sederhana, mudah dicabut, tetapi butuh server online untuk memverifikasi apa pun.
2. Kunci sebagai pembawa hak (signed license)
Kunci — atau berkas lisensi yang menyertainya — memuat data hak sekaligus tanda tangan kriptografis. Aplikasi bisa memverifikasi sendiri secara offline memakai kunci publik. Inilah jantung lisensi desktop & on-premise.
Untuk menandatangani, dua keluarga algoritma dipakai:
- HMAC (simetris) — cepat, satu secret dipakai untuk membuat dan memverifikasi. Cocok bila hanya server Anda yang memverifikasi. Jangan pernah menaruh secret HMAC di dalam aplikasi klien — siapa pun yang membongkarnya bisa memalsukan kunci.
- Tanda tangan asimetris (Ed25519 / RSA) — Anda menandatangani dengan private key yang dirahasiakan di server; aplikasi klien memverifikasi dengan public key yang boleh disebar. Ini cara aman untuk validasi offline.
Node.js — menandatangani berkas lisensi dengan HMAC (server-side)
const crypto = require('crypto');
// Payload hak — apa yang boleh dilakukan pemilik
function signLicense(payload, secret) {
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
const sig = crypto.createHmac('sha256', secret)
.update(body).digest('base64url');
return body + '.' + sig; // format: <payload>.<signature>
}
function verifyLicense(token, secret) {
const [body, sig] = token.split('.');
if (!body || !sig) return { valid: false, reason: 'format' };
const expected = crypto.createHmac('sha256', secret)
.update(body).digest('base64url');
// Bandingkan konstan-waktu agar tak bocor lewat timing attack
const ok = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!ok) return { valid: false, reason: 'signature' };
const data = JSON.parse(Buffer.from(body, 'base64url').toString());
if (data.exp && Date.now() > data.exp) return { valid: false, reason: 'expired' };
return { valid: true, data };
}
// Contoh pemakaian
const token = signLicense(
{ sku: 'aurora-pro', email: 'a@b.com', seats: 3, exp: 1798761600000 },
process.env.LICENSE_SECRET
);
Jangan pernah membandingkan tanda tangan dengan ===. Perbandingan biasa berhenti di byte pertama yang beda, membocorkan informasi lewat waktu eksekusi. Selalu pakai crypto.timingSafeEqual atau setara.
Node.js — tanda tangan asimetris Ed25519 (verifikasi offline aman)
const crypto = require('crypto');
// Sekali saja: buat pasangan kunci. Simpan private di server, sebar public di app.
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
function signEd(payloadJson, privateKey) {
return crypto.sign(null, Buffer.from(payloadJson), privateKey).toString('base64');
}
// Di dalam APP (offline): hanya butuh publicKey — tak bisa memalsukan kunci
function verifyEd(payloadJson, sigB64, publicKey) {
return crypto.verify(null, Buffer.from(payloadJson),
publicKey, Buffer.from(sigB64, 'base64'));
}
Checksum — Penjaga Gerbang Pertama
Sebelum kunci menyentuh server atau kriptografi, sebuah checksum sederhana menolak salah ketik secara instan. Checksum bukan keamanan — ia mudah dihitung ulang oleh siapa pun — melainkan kualitas hidup: pengguna yang salah mengetik satu huruf langsung diberi tahu, tanpa menunggu jaringan.
Pola yang dipakai alat di Bab 14: hitung hash dari 12 simbol payload, ambil 4 simbol sebagai grup checksum. Saat validasi, hitung ulang dan bandingkan. Berikut versi ringkasnya (identik dengan yang berjalan di halaman ini):
const ALPHA = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; // tanpa I,L,O,U
function toGroup(n) { // 20 bit -> 4 simbol
let s = "";
for (let i = 0; i < 4; i++) s += ALPHA[(n >>> (i*5)) & 31];
return s;
}
function checksumGroup(body) { // 12 simbol -> 1 grup checksum
let sum = 0;
for (let i = 0; i < body.length; i++)
sum = (Math.imul(sum, 31) + body.charCodeAt(i)) >>> 0;
return toGroup(sum);
}
// valid jika: checksumGroup(12 simbol pertama) === grup ke-4
Checksum menolak salah ketik (offline, murah). Signature menolak pemalsuan (butuh secret/private key). Kunci yang serius memakai keduanya: checksum untuk UX, signature untuk keamanan.
Aktivasi: Online vs Offline
Aktivasi adalah momen sebuah kunci "dipasangkan" ke sebuah instalasi. Dua jalur:
Aktivasi online
- App mengirim kunci + sidik jari perangkat ke server lisensi.
- Server memeriksa: kunci ada? belum melebihi jumlah seat? belum dicabut?
- Server mencatat aktivasi dan mengembalikan token akses (berumur pendek) atau berkas lisensi bertanda tangan.
- App menyimpan token dan bekerja; sesekali "menelepon pulang" untuk memperbarui.
Aktivasi offline
Untuk lingkungan tanpa internet (pabrik, instansi tertutup): pengguna mengirim request code (berisi sidik jari perangkat) lewat kanal lain; Anda membalas dengan berkas lisensi bertanda tangan yang mengikat ke perangkat itu. App memverifikasi tanda tangan dengan public key — tak perlu jaringan. Dibahas tuntas di Bab 11.
Device Binding, Trial & Grace Period
Device binding mengikat lisensi ke perangkat tertentu agar satu kunci tak dipakai seribu orang. Sidik jari perangkat dirakit dari kombinasi atribut stabil — bukan satu pun yang sempurna, maka gabungkan beberapa dan toleransi perubahan kecil.
// Contoh menyusun device fingerprint (jangan terlalu ketat!)
function deviceFingerprint(os) {
const parts = [
os.machineId, // id mesin stabil
os.cpuModel, // model CPU
os.primaryMac // MAC adaptor utama
].filter(Boolean);
return crypto.createHash('sha256').update(parts.join('|')).digest('hex').slice(0,32);
}
Mengikat ke MAC address saja membuat lisensi mati saat pengguna ganti Wi-Fi. Ganti hard drive, upgrade RAM — semua bisa memicu "device baru". Beri toleransi: cocok jika mayoritas atribut sama, dan izinkan sejumlah reactivation mandiri per tahun.
Trial memberi akses penuh berbatas waktu. Simpan tanggal mulai di tempat yang sulit direset (server, atau berkas bertanda tangan) — bukan sekadar file lokal yang bisa dihapus. Grace period adalah kebaikan hati yang menyelamatkan reputasi: saat langganan gagal ditagih atau server lisensi sedang down, jangan langsung mematikan aplikasi. Beri, misalnya, 3–14 hari tenggang agar pelanggan yang jujur tak terkunci karena kartu kedaluwarsa.
"Butuh 20 tahun membangun reputasi dan lima menit menghancurkannya."
— Warren Buffett
Saat menghasilkan kode lisensi, selalu default ke fail-open dengan grace period untuk kegagalan jaringan, dan fail-closed hanya untuk signature yang benar-benar tidak valid. Membedakan "tak bisa memverifikasi" dari "terbukti palsu" adalah inti keputusan yang benar.
Server Lisensi
Jika kunci lisensi adalah tiket, server lisensi adalah petugas di gerbang — yang tahu tiket mana yang masih berlaku, mana yang sudah dicabut, dan berapa kali sebuah tiket boleh dipakai. Bab ini merancang otak terpusat itu: sederhana, tetapi dengan tiga kemampuan yang tak boleh absen — validasi, pencabutan, dan kedaluwarsa.
Skema Data Minimal
Anda tidak butuh arsitektur mikroservis untuk memulai. Sebuah tabel lisensi dan sebuah tabel aktivasi sudah cukup untuk ribuan pelanggan pertama.
-- Tabel inti (PostgreSQL)
CREATE TABLE licenses (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
key text UNIQUE NOT NULL, -- XXXX-XXXX-XXXX-XXXX
sku text NOT NULL, -- produk/tier
email text NOT NULL,
seats int NOT NULL DEFAULT 1, -- jumlah perangkat
status text NOT NULL DEFAULT 'active', -- active|revoked|expired
expires_at timestamptz, -- NULL = perpetual
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE activations (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
license_id uuid REFERENCES licenses(id) ON DELETE CASCADE,
device_hash text NOT NULL,
last_seen timestamptz NOT NULL DEFAULT now(),
UNIQUE(license_id, device_hash)
);
Endpoint Validasi
Endpoint validasi harus cepat, aman, dan jujur soal kegagalan. Ia menggabungkan checksum (murah), pencarian database, cek seat, cek revoke, dan cek expiry.
// POST /v1/activate { key, deviceHash }
app.post('/v1/activate', async (req, res) => {
const { key, deviceHash } = req.body;
// 1) Checksum lokal — tolak sampah sebelum menyentuh DB
if (!checksumValid(key))
return res.status(400).json({ ok:false, code:'BAD_FORMAT' });
const lic = await db.licenseByKey(key);
if (!lic) return res.status(404).json({ ok:false, code:'NOT_FOUND' });
if (lic.status==='revoked') return res.status(403).json({ ok:false, code:'REVOKED' });
if (lic.expires_at && new Date() > lic.expires_at)
return res.status(403).json({ ok:false, code:'EXPIRED' });
// 2) Cek seat: perangkat ini sudah terdaftar? kalau belum, masih ada kuota?
const seatsUsed = await db.countActivations(lic.id);
const known = await db.hasActivation(lic.id, deviceHash);
if (!known && seatsUsed >= lic.seats)
return res.status(409).json({ ok:false, code:'SEAT_LIMIT' });
// 3) Catat/segarkan aktivasi, kembalikan token berumur pendek
await db.upsertActivation(lic.id, deviceHash);
const token = signLicense(
{ sku:lic.sku, exp: Date.now() + 7*864e5 }, // 7 hari
process.env.LICENSE_SECRET
);
res.json({ ok:true, token, sku:lic.sku, expires_at:lic.expires_at });
});
Server tak mengembalikan "izin selamanya", melainkan token yang kedaluwarsa dalam beberapa hari. App bekerja offline selama token valid, lalu memperbaruinya saat online. Ini menyeimbangkan kenyamanan offline dengan kemampuan mencabut akses.
Revoke & Expiry
Revoke (pencabutan) diperlukan saat: chargeback/refund, penyalahgunaan, atau kunci bocor ke publik. Karena app memegang token berumur pendek, pencabutan menjadi efektif dalam hitungan hari — cukup ubah status menjadi revoked, dan penyegaran token berikutnya akan ditolak.
// Mencabut sebuah lisensi (mis. dipicu webhook refund)
async function revokeLicense(key, reason) {
await db.update('licenses', { key }, {
status: 'revoked', revoked_reason: reason, revoked_at: new Date()
});
audit('license.revoked', { key, reason }); // jejak audit penting
}
Expiry berlaku otomatis lewat kolom expires_at. Untuk langganan, kolom ini diperpanjang setiap pembayaran berhasil (lewat webhook, Bab 8). Jadwalkan sebuah cron harian untuk menandai lisensi kedaluwarsa dan mengirim email pengingat perpanjangan beberapa hari sebelumnya.
Heartbeat & Fail-Open
App yang sudah aktif sesekali mengirim heartbeat untuk memperbarui last_seen dan token. Filosofi kegagalan harus ditetapkan sadar:
| Situasi | Keputusan | Alasan |
|---|---|---|
| Signature terbukti palsu | Fail-closed (tolak) | Bukti kuat penyalahgunaan |
| Server tak dapat dihubungi | Fail-open + grace | Jangan hukum pelanggan jujur atas masalah jaringan |
| Token kedaluwarsa & offline | Grace period, lalu mode terbatas | Beri waktu untuk online kembali |
Billing Langganan
Langganan adalah janji yang ditagih berulang. Di baliknya ada mesin waktu kecil: menghitung tanggal jatuh tempo, menagih ulang otomatis, membagi biaya secara adil saat pelanggan pindah paket di tengah jalan, dan menangani kartu yang gagal dengan sabar. Bab ini membedah mesin itu.
Siklus Penagihan
Setiap langganan punya anchor — tanggal jangkar — yang menentukan kapan siklus berikutnya dimulai. Dua pola:
- Anniversary billing: tiap pelanggan ditagih pada tanggal ia mendaftar (mendaftar 7 Juli → ditagih tiap tanggal 7). Umum di Stripe.
- Calendar billing: semua pelanggan ditagih di tanggal sama (mis. tanggal 1), dengan proration untuk yang mendaftar di tengah bulan.
| Status langganan | Arti | Akses |
|---|---|---|
trialing | Masa coba, belum ditagih | Penuh |
active | Pembayaran lancar | Penuh |
past_due | Tagihan gagal, sedang dicoba ulang | Penuh (grace) |
canceled | Dibatalkan, tak diperpanjang | Sampai akhir periode |
unpaid | Semua percobaan gagal | Ditangguhkan |
Proration — Berbagi Biaya dengan Adil
Ketika pelanggan upgrade dari Pro ($29) ke Business ($99) di tengah bulan, tidak adil menagih $99 penuh — mereka sudah membayar sisa bulan di Pro. Proration menghitung kredit atas waktu yang belum terpakai dan hanya menagih selisihnya.
// Proration sederhana saat upgrade di tengah siklus
function prorate({ oldPrice, newPrice, daysLeft, daysInCycle }) {
const unusedCredit = oldPrice * (daysLeft / daysInCycle); // sisa paket lama
const newCharge = newPrice * (daysLeft / daysInCycle); // paket baru, prorata
const dueNow = Math.max(0, +(newCharge - unusedCredit).toFixed(2));
return { dueNow, unusedCredit, newCharge };
}
// Upgrade Pro->Business, 20 hari tersisa dari 30:
prorate({ oldPrice:29, newPrice:99, daysLeft:20, daysInCycle:30 });
// { dueNow: 46.67, unusedCredit: 19.33, newCharge: 66.00 }
Stripe dan Midtrans (via fitur langganan) bisa menghitung proration untuk Anda. Pahami mekanismenya seperti di atas, tetapi di produksi biarkan gateway yang menjadi sumber kebenaran angka — agar cocok dengan invoice yang mereka terbitkan.
Upgrade & Downgrade
Kebijakan yang lazim dan ramah pelanggan:
- Upgrade → berlaku segera, dengan proration ditagih saat itu juga. Pelanggan mau fitur lebih, beri sekarang.
- Downgrade → berlaku di akhir periode. Pelanggan sudah membayar untuk tier lebih tinggi sampai akhir siklus; turunkan saat periode berikutnya agar tak perlu mengembalikan uang.
Dunning — Seni Menagih dengan Sabar
Kartu kedaluwarsa. Saldo tak cukup. Bank menolak. Ini normal — sekitar 5–10% penagihan berulang gagal di percobaan pertama, dan mayoritas bisa diselamatkan. Dunning adalah proses percobaan ulang yang terjadwal plus komunikasi yang sopan.
// Jadwal dunning umum (hari setelah kegagalan pertama)
const DUNNING = [
{ day: 0, action: 'retry + email: pembayaran gagal, kami coba lagi' },
{ day: 3, action: 'retry + email: mohon perbarui metode bayar' },
{ day: 5, action: 'retry + email: peringatan penangguhan' },
{ day: 7, action: 'retry terakhir' },
{ day: 10, action: 'suspend akses + email: langganan ditangguhkan' },
];
Memutus akses di kegagalan pertama membuang pelanggan yang hanya lupa memperbarui kartu. Involuntary churn (churn karena kegagalan teknis, bukan keinginan berhenti) bisa mencapai sepertiga total churn — dunning yang baik mengubahnya kembali menjadi pendapatan.
"Aturan No. 1: jangan pernah kehilangan uang. Aturan No. 2: jangan pernah lupa aturan No. 1."
— Warren Buffett
Saat merancang alur langganan, selalu sertakan penanganan status past_due dan sebuah jadwal dunning. Kode yang hanya menangani jalur bahagia (pembayaran selalu sukses) tidak siap produksi.
Integrasi Midtrans
Untuk menjual ke pasar Indonesia, Midtrans adalah jalur yang paling natural: ia berbicara bahasa pembayaran lokal — GoPay, QRIS, transfer bank, kartu, gerai ritel — dalam satu integrasi. Bab ini merangkai alur lengkap: dari tombol beli, ke halaman pembayaran Snap, hingga notifikasi yang mengubah "pending" menjadi "lisensi aktif".
Peta Alur Midtrans
Midtrans menawarkan beberapa produk; yang paling cepat untuk memulai adalah Snap — halaman/pop-up pembayaran siap pakai. Alur inti selalu sama:
- Server Anda membuat transaction ke Midtrans dan menerima sebuah
snap_token. - Front-end membuka Snap memakai token itu; pelanggan membayar dengan metode pilihannya.
- Midtrans memproses pembayaran dan mengirim HTTP Notification (webhook) ke server Anda.
- Server memverifikasi signature notifikasi, lalu memutakhirkan status pesanan & menerbitkan lisensi.
Membuat Transaksi Snap
Dari server, Anda memanggil Snap API dengan detail pesanan. order_id harus unik dan Anda simpan untuk mencocokkan notifikasi nanti. Otentikasi memakai Server Key yang di-encode Base64 sebagai Basic Auth.
// server: buat Snap transaction (tanpa SDK, cukup fetch)
const SERVER_KEY = process.env.MIDTRANS_SERVER_KEY; // SB-Mid-server-xxxx
const BASE = 'https://app.sandbox.midtrans.com/snap/v1/transactions'; // prod: app.midtrans.com
async function createSnapTx(order) {
const auth = Buffer.from(SERVER_KEY + ':').toString('base64');
const body = {
transaction_details: { order_id: order.id, gross_amount: order.amount },
customer_details: { email: order.email, first_name: order.name },
item_details: [{ id: order.sku, price: order.amount, quantity: 1, name: order.title }],
// callbacks & expiry opsional
expiry: { unit: 'minutes', duration: 60 }
};
const r = await fetch(BASE, {
method: 'POST',
headers: { 'Content-Type':'application/json', 'Accept':'application/json',
'Authorization': 'Basic ' + auth },
body: JSON.stringify(body)
});
const data = await r.json(); // { token, redirect_url }
return data.token; // snap_token untuk front-end
}
Front-end — membuka Snap dengan token
// muat snap.js dari Midtrans, lalu:
window.snap.pay(snapToken, {
onSuccess: (res) => { // tampilkan "menunggu konfirmasi"; JANGAN aktifkan di sini },
onPending: (res) => { // mis. VA belum dibayar },
onError: (res) => { // gagal },
onClose: () => { // pengguna menutup pop-up }
});
onSuccess di browser tidak boleh menjadi pemicu penerbitan lisensi — mudah dipalsukan oleh siapa pun yang membuka konsol. Sumber kebenaran satu-satunya adalah HTTP Notification yang diverifikasi di server (Bab 8). Front-end hanya menampilkan status, tidak memberi akses.
Menerima & Memverifikasi Notifikasi
Midtrans mem-POST notifikasi ke URL yang Anda daftarkan di dashboard. Verifikasinya memakai signature_key — SHA-512 dari gabungan beberapa field ditambah Server Key.
const crypto = require('crypto');
app.post('/webhook/midtrans', async (req, res) => {
const n = req.body; // notifikasi JSON dari Midtrans
// 1) Verifikasi signature: sha512(order_id + status_code + gross_amount + ServerKey)
const raw = n.order_id + n.status_code + n.gross_amount + SERVER_KEY;
const expected = crypto.createHash('sha512').update(raw).digest('hex');
if (expected !== n.signature_key)
return res.status(403).send('bad signature');
// 2) Tentukan hasil dari transaction_status + fraud_status
const s = n.transaction_status, fraud = n.fraud_status;
let paid = false;
if ((s==='capture' && fraud==='accept') || s==='settlement') paid = true;
else if (['deny','cancel','expire'].includes(s)) paid = false;
else if (s==='pending') { /* tunggu; belum aktifkan */ }
// 3) Idempoten: proses order_id ini sekali saja (lihat Bab 8)
if (paid) await fulfillOrder(n.order_id); // terbitkan lisensi + invoice
res.status(200).send('OK'); // SELALU 200 agar Midtrans berhenti retry
});
Status Transaksi Midtrans
transaction_status | Arti | Tindakan |
|---|---|---|
capture | Kartu ter-otorisasi (cek fraud_status) | Jika accept → penuhi |
settlement | Dana benar-benar masuk | Penuhi pesanan |
pending | Menunggu (mis. VA belum dibayar) | Tunggu, jangan aktifkan |
deny | Ditolak | Gagalkan |
expire | Kadaluarsa | Gagalkan |
cancel | Dibatalkan | Gagalkan / batalkan lisensi |
refund | Dana dikembalikan | Cabut lisensi (Bab 4) |
Midtrans menyediakan lingkungan Sandbox dengan kartu & nomor uji. Selesaikan seluruh alur — termasuk notifikasi refund dan expire — di Sandbox sebelum menyentuh Server Key produksi. Gunakan tombol "Send test notification" di dashboard untuk memverifikasi endpoint webhook.
Integrasi Stripe
Untuk pasar global dan langganan yang rapi, Stripe adalah standar de facto. Ia menyembunyikan kerumitan kartu internasional, pajak lintas negara, proration, dan dunning di balik API yang bersih. Bab ini menautkan tiga primitif Stripe yang paling sering Anda pakai: Checkout untuk pembayaran, Billing untuk langganan, dan Customer Portal agar pelanggan mengurus dirinya sendiri.
Stripe Checkout — Jalur Tercepat
Alih-alih membangun form kartu (dan menanggung beban PCI), Anda mengarahkan pelanggan ke halaman Checkout yang di-host Stripe. Untuk langganan, Anda cukup merujuk sebuah Price yang telah dibuat di dashboard.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// Buat sesi Checkout untuk langganan bulanan
app.post('/checkout', async (req, res) => {
const session = await stripe.checkout.sessions.create({
mode: 'subscription', // 'payment' untuk sekali bayar
line_items: [{ price: 'price_1PxProMonthly', quantity: 1 }],
customer_email: req.body.email,
success_url: 'https://app.contoh.com/ok?s={CHECKOUT_SESSION_ID}',
cancel_url: 'https://app.contoh.com/batal',
// simpan referensi internal agar mudah dicocokkan
client_reference_id: req.body.userId,
metadata: { sku: 'aurora-pro' }
});
res.json({ url: session.url }); // arahkan browser ke sini
});
Stripe Billing — Model Berlangganan
Di Stripe, langganan tersusun dari objek yang saling terkait. Memahami hierarki ini membuat sisa integrasi jelas:
| Objek | Peran |
|---|---|
Product | Apa yang dijual ("Aurora Pro") |
Price | Berapa & seberapa sering ($29/bln, $290/thn) |
Customer | Pelanggan & metode pembayarannya |
Subscription | Ikatan Customer ↔ Price yang aktif |
Invoice | Tagihan yang dibuat tiap siklus |
Upgrade/downgrade cukup dengan memutakhirkan item langganan; Stripe menghitung proration otomatis (lihat Bab 5).
// Upgrade: ganti price pada item langganan, biarkan Stripe prorate
await stripe.subscriptions.update(subId, {
items: [{ id: itemId, price: 'price_1PxBusinessMonthly' }],
proration_behavior: 'create_prorations' // tagih selisih prorata
});
Customer Portal — Biarkan Mereka Mengurus Diri
Alih-alih membangun halaman "ubah kartu / batalkan langganan / unduh invoice", arahkan pelanggan ke Portal bawaan Stripe. Satu panggilan API, dan Anda menghemat berhari-hari pekerjaan.
const portal = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: 'https://app.contoh.com/akun'
});
// arahkan pelanggan ke portal.url
Webhook Stripe
Seperti Midtrans, kebenaran datang dari webhook, bukan dari success_url. Event yang wajib Anda dengarkan:
| Event | Makna & tindakan |
|---|---|
checkout.session.completed | Checkout selesai → provisi awal akses |
invoice.paid | Siklus dibayar → perpanjang expires_at |
invoice.payment_failed | Gagal → masuk alur dunning |
customer.subscription.updated | Ganti tier/status → sinkronkan entitlement |
customer.subscription.deleted | Berhenti → cabut akses di akhir periode |
Verifikasi signature-nya dibahas bersama Midtrans di bab berikutnya, karena polanya sama: selalu verifikasi, selalu idempoten.
"Waktu adalah teman dari bisnis yang hebat, dan musuh dari bisnis yang biasa-biasa saja."
— Warren Buffett
Ketika ditanya "Midtrans atau Stripe?", pertimbangkan pasar: Midtrans unggul untuk metode pembayaran lokal Indonesia (QRIS, VA, e-wallet); Stripe unggul untuk pelanggan global & langganan kompleks. Banyak produk memakai keduanya — Midtrans untuk pembeli Indonesia, Stripe untuk luar negeri.
Webhook Pembayaran
Webhook adalah tempat uang berubah menjadi akses. Ia juga tempat paling rawan di seluruh sistem: sebuah endpoint publik yang, jika salah tangani, bisa memberi lisensi gratis ke penipu atau menerbitkan seribu kunci untuk satu pembayaran. Dua disiplin menyelamatkannya — verifikasi signature dan idempotensi. Kuasai keduanya dan seluruh sistem uang Anda menjadi kokoh.
Aturan Emas Webhook
- Jangan percaya isi tubuh permintaan sebelum signature-nya terverifikasi.
- Proses setiap event tepat satu kali, meski dikirim berkali-kali (idempoten).
- Balas 2xx secepatnya; kerjakan tugas berat di latar belakang.
- Anggap event bisa datang tak berurutan dan bisa duplikat.
Verifikasi Signature
Setiap gateway menandatangani payload-nya sehingga Anda yakin ia benar-benar berasal dari mereka dan tak diubah di jalan. Perhatikan: Anda butuh raw body (byte mentah), bukan JSON yang sudah di-parse, karena parsing bisa mengubah representasi.
// Stripe: verifikasi memakai raw body + secret webhook
app.post('/webhook/stripe',
express.raw({ type: 'application/json' }), // WAJIB raw, bukan json()
(req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.body, // raw Buffer
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET // whsec_xxx
);
} catch (err) {
return res.status(400).send('signature gagal: ' + err.message);
}
enqueue(event); // simpan & proses async (idempoten)
res.json({ received: true });
});
Untuk Midtrans, signature-nya adalah SHA-512 seperti di Bab 6. Prinsipnya identik: hitung ulang dari field yang ditentukan + Server Key, bandingkan konstan-waktu.
Jika middleware express.json() berjalan lebih dulu, ia sudah mem-parse dan membuang raw body — verifikasi Stripe akan selalu gagal. Pasang rute webhook sebelum parser global, atau gunakan express.raw() khusus di rute itu.
Idempotensi — Memproses Sekali Saja
Gateway menjamin at-least-once delivery: mereka akan mengirim ulang notifikasi sampai menerima 2xx. Jaringan yang lambat bisa membuat Anda menerima event yang sama dua kali. Tanpa perlindungan, satu pembayaran bisa menerbitkan dua lisensi. Solusinya: catat ID event yang sudah diproses, dan tolak duplikat secara atomik.
// Kunci idempotensi: gunakan ID unik dari gateway
// Stripe: event.id | Midtrans: transaction_id / order_id+status
async function processOnce(eventId, handler) {
// INSERT akan gagal jika eventId sudah ada -> sudah diproses
try {
await db.query(
'INSERT INTO processed_events(id, at) VALUES ($1, now())', [eventId]);
} catch (e) {
if (e.code === '23505') return { skipped: true }; // unique violation
throw e;
}
await handler(); // jalankan efek sekali saja
return { processed: true };
}
Cara paling andal bukan "cek lalu tulis" (rawan balapan), melainkan menyerahkannya ke UNIQUE constraint. Biarkan database menolak duplikat secara atomik. Transaksi kedua akan gagal insert dan Anda tahu untuk melewatinya.
Urutan & Rekonsiliasi
Event bisa tiba tak berurutan (subscription.updated mendahului checkout.completed). Rancang handler agar idempoten sekaligus komutatif sebisa mungkin — turunkan status akhir dari data, bukan dari asumsi urutan. Sebagai jaring pengaman, jalankan rekonsiliasi berkala: tarik daftar transaksi dari gateway dan cocokkan dengan database Anda, untuk menangkap webhook yang hilang.
// Cron harian: tangkap pembayaran yang webhook-nya gagal terkirim
async function reconcile(sinceISO) {
const remote = await gateway.listSettled(sinceISO); // dari Stripe/Midtrans
for (const tx of remote) {
const local = await db.orderByExternalId(tx.id);
if (!local || local.status !== 'fulfilled')
await fulfillOrder(tx.order_id); // idempoten, jadi aman diulang
}
}
Invoice & Kwitansi
Sebuah pembayaran yang tak berdokumen adalah masalah yang menunggu waktu — bagi pembeli yang butuh reimburse, bagi Anda yang butuh lapor pajak, bagi keduanya saat terjadi sengketa. Invoice dan kwitansi mengubah transaksi menjadi catatan yang sah dan bisa diaudit. Bab ini merapikan anatomi dan penomorannya.
Invoice vs Kwitansi
- Invoice (faktur): tagihan — permintaan pembayaran. Diterbitkan sebelum/saat penagihan. Memuat rincian, jatuh tempo, dan pajak.
- Kwitansi (receipt): bukti bayar — konfirmasi bahwa uang telah diterima. Diterbitkan setelah pembayaran berhasil.
Penomoran yang Benar
Nomor invoice harus unik, berurutan, dan tanpa celah — persyaratan akuntansi di banyak yurisdiksi. Jangan memakai ID acak atau UUID sebagai nomor invoice resmi.
// Format: INV/YYYY/MM/000123 — sekuensial, atomik lewat DB
async function nextInvoiceNo(db) {
// gunakan sequence/row-lock agar tak ada nomor kembar/loncat
const { seq } = await db.one(
"UPDATE counters SET val = val + 1 WHERE name='invoice' RETURNING val AS seq");
const now = new Date();
const yyyy = now.getFullYear();
const mm = String(now.getMonth()+1).padStart(2,'0');
return `INV/${yyyy}/${mm}/${String(seq).padStart(6,'0')}`;
}
Komponen Wajib Sebuah Invoice
| Bagian | Isi |
|---|---|
| Identitas penjual | Nama badan/usaha, alamat, NPWP (jika PKP) |
| Identitas pembeli | Nama, email/alamat, NPWP (untuk B2B) |
| Nomor & tanggal | Nomor sekuensial + tanggal terbit + jatuh tempo |
| Rincian item | Deskripsi, kuantitas, harga satuan, subtotal |
| Pajak | Dasar Pengenaan Pajak, tarif PPN, nilai PPN (Bab 13) |
| Total | Subtotal + pajak − diskon = total dibayar |
| Instruksi bayar | Metode, rekening/link, referensi |
Terbitkan kwitansi otomatis begitu event pembayaran berhasil tiba (Bab 8). Simpan PDF, kirim via email, dan tautkan ke akun pelanggan. Stripe bahkan bisa menerbitkan & mengirim invoice sendiri jika Anda mengaktifkan Stripe Invoicing.
Metering & Kuota
Ketika Anda menagih berdasarkan pemakaian, akurasi pencatatan menjadi soal kepercayaan dan pendapatan sekaligus. Metering yang benar mencatat setiap unit yang dipakai, menegakkan kuota tanpa mengejutkan, dan mengubah pemakaian menjadi angka tagihan yang bisa dipertanggungjawabkan.
Merekam Peristiwa Pemakaian
Setiap unit yang dapat ditagih — panggilan API, dokumen diproses, GB tersimpan — menjadi sebuah usage event. Catat mentah dulu, agregasi belakangan; ini membuat penagihan bisa diaudit dan dikoreksi.
// Catat peristiwa pemakaian secara ringan (idempoten via requestId)
async function recordUsage({ accountId, metric, qty, requestId }) {
await db.query(
`INSERT INTO usage_events(account_id, metric, qty, req_id, ts)
VALUES ($1,$2,$3,$4, now())
ON CONFLICT (req_id) DO NOTHING`, // hindari hitung ganda
[accountId, metric, qty, requestId]);
}
// Agregasi periodik untuk tagihan
async function usageThisCycle(accountId, metric, since) {
const { sum } = await db.one(
`SELECT COALESCE(SUM(qty),0) AS sum FROM usage_events
WHERE account_id=$1 AND metric=$2 AND ts >= $3`, [accountId, metric, since]);
return Number(sum);
}
Menegakkan Kuota
Kuota mengubah pemakaian menjadi batas. Tiga sikap terhadap kuota yang habis:
| Kebijakan | Perilaku saat kuota habis | Cocok untuk |
|---|---|---|
| Hard cap | Tolak permintaan berikutnya (HTTP 429) | Tier gratis, cegah penyalahgunaan |
| Overage | Izinkan lanjut, tagih kelebihan | Tier berbayar, pertumbuhan mulus |
| Soft cap | Izinkan sementara + peringatan | Menjaga hubungan, cegah kejutan |
// Penjaga kuota di depan operasi mahal
async function guardQuota(account, metric) {
const used = await usageThisCycle(account.id, metric, account.cycleStart);
const limit = account.plan.limits[metric];
if (used >= limit) {
if (account.plan.allowOverage) return { allow:true, overage:true };
return { allow:false, code:'QUOTA_EXCEEDED', used, limit };
}
return { allow:true, remaining: limit - used };
}
Kirim notifikasi di 80% dan 100% kuota. Pelanggan yang tiba-tiba diblokir tanpa peringatan akan menyalahkan Anda; pelanggan yang sudah diperingatkan akan menyalahkan diri sendiri — atau lebih baik, meng-upgrade.
"Anda tidak akan pernah kaya dengan mengikuti keramaian."
— Warren Buffett
Untuk sistem metering, selalu sertakan kunci idempotensi (requestId) pada pencatatan pemakaian. Menghitung ganda unit yang ditagih adalah bug yang langsung merusak kepercayaan dan tagihan pelanggan.
Lisensi On-Premise
Ada dunia di mana "cukup telepon server lisensi kami" adalah jawaban yang mustahil: bank, rumah sakit, pabrik, instansi pemerintah yang jaringannya terisolasi (air-gapped). Menjual ke sana menuntut lisensi yang membuktikan dirinya sendiri, tanpa internet. Bab ini membangun berkas lisensi bertanda tangan yang bisa diverifikasi sepenuhnya offline.
Prinsip: Bukti Ada di Berkas
Karena tak ada server untuk ditanyai, seluruh hak harus terkandung dalam license file itu sendiri, dilindungi tanda tangan asimetris (Bab 3). Aplikasi hanya memegang public key; ia bisa memverifikasi keaslian dan integritas berkas, tetapi tak bisa memalsukannya — karena private key hanya ada pada Anda.
// Struktur license.lic — payload + signature (base64)
{
"v": 1,
"payload": {
"customer": "PT Bank Sentosa",
"sku": "aurora-enterprise",
"seats": 50,
"machine_ids": ["a1b2c3..."], // ikat ke perangkat (opsional)
"issued_at": "2026-07-12",
"expires_at": "2027-07-12", // null = perpetual
"features": ["export", "sso", "audit"]
},
"sig": "MEUCIQD...Ed25519-base64" // tanda tangan atas payload terkanonikal
}
Alur Aktivasi Offline
- Aplikasi menghasilkan request file berisi sidik jari perangkat (dan jumlah seat yang diminta).
- Pelanggan mengirimnya via kanal apa pun (USB, email dari komputer lain, portal).
- Anda menandatangani license file yang mengikat ke perangkat itu dan mengembalikannya.
- Aplikasi memuat berkas, memverifikasi tanda tangan dengan public key bawaan, dan membuka fitur sesuai payload.
// Verifikasi offline di dalam aplikasi (hanya butuh PUBLIC key)
function loadLicense(fileText, PUBLIC_KEY) {
const lic = JSON.parse(fileText);
const canonical = JSON.stringify(lic.payload); // samakan kanonikalisasi dgn saat menandatangani
const ok = crypto.verify(null, Buffer.from(canonical),
PUBLIC_KEY, Buffer.from(lic.sig, 'base64'));
if (!ok) throw new Error('Lisensi tidak sah (tanda tangan gagal)');
if (lic.payload.expires_at && new Date() > new Date(lic.payload.expires_at))
throw new Error('Lisensi kedaluwarsa');
return lic.payload; // fitur & batas untuk aplikasi
}
Tanda tangan dihitung atas byte tertentu. Jika saat menandatangani Anda memakai urutan field/spasi yang berbeda dengan saat memverifikasi, verifikasi akan selalu gagal. Bekukan satu bentuk kanonikal (mis. JSON tanpa spasi, kunci terurut) di kedua sisi.
Untuk lisensi tahunan, terbitkan berkas dengan expires_at dan kirim yang baru saat perpanjangan. Untuk pencabutan di lingkungan air-gapped, gunakan masa berlaku pendek yang harus diperbarui — itulah satu-satunya "revoke" yang mungkin tanpa jaringan.
Anti-piracy & Proteksi
Mari jujur sejak awal: tidak ada perangkat lunak yang bisa dijalankan di komputer pengguna dan sekaligus mustahil dibajak. Tujuan anti-piracy bukan kesempurnaan mutlak, melainkan membuat membajak lebih merepotkan daripada membayar bagi mayoritas pengguna jujur, sambil tak menghukum pelanggan yang sah.
Model Ancaman yang Realistis
| Ancaman | Pertahanan proporsional |
|---|---|
| Berbagi kunci ("kirim aku kuncimu dong") | Device binding + batas seat (Bab 3, 4) |
| Kunci bocor ke publik | Revoke + heartbeat berumur pendek (Bab 4) |
| Kunci palsu buatan sendiri | Tanda tangan asimetris (Bab 3) |
| Trial di-reset berulang | Simpan status trial di server, bukan file lokal |
| Cracker profesional (patch biner) | Sulit dihentikan total; fokuskan energi ke tempat lain |
Lapisan Pertahanan
- Server-side value. Pertahanan terkuat: pindahkan bagian bernilai ke server Anda (fitur SaaS, pemrosesan berat). Yang tak ada di mesin pengguna tak bisa dibajak.
- Signed entitlement. Kunci & berkas bertanda tangan menutup pemalsuan buatan sendiri.
- Aktivasi & heartbeat. Membatasi berbagi dan memungkinkan pencabutan.
- Obfuscation secukupnya. Menyulitkan pembongkaran biner memperlambat cracker amatir — tetapi jangan berlebihan; ini perlombaan yang tak bisa dimenangkan.
- Watermarking. Menyisipkan identitas pembeli (tak kasat mata) ke berkas/ekspor, agar kebocoran bisa dilacak asalnya.
"Risiko datang dari tidak tahu apa yang sedang Anda lakukan."
— Warren Buffett
DRM yang agresif — memaksa online terus-menerus, mematikan app saat server Anda down, false-positive device binding — merusak pengalaman pembeli yang sah jauh lebih sering daripada menghentikan bajakan. Setiap lapisan proteksi harus ditimbang terhadap gesekan yang ia timbulkan bagi pelanggan yang membayar.
Banyak bisnis perangkat lunak sukses memperlakukan sebagian pembajakan sebagai biaya pemasaran — pengguna yang tak akan pernah membayar tetap menyebarkan produk Anda. Fokuskan proteksi pada segmen yang mampu & mau membayar (korporat, profesional), bukan pada memburu setiap salinan.
Pajak & Legal Ringkas
Bab ini adalah orientasi, bukan nasihat hukum. Aturan pajak berubah dan berbeda per yurisdiksi; untuk keputusan nyata, konsultasikan dengan konsultan pajak atau notaris. Tujuannya di sini: memberi Anda peta agar tahu pertanyaan apa yang harus diajukan.
PPN atas Produk Digital (Indonesia)
Di Indonesia, penjualan perangkat lunak dan layanan digital umumnya objek PPN. Jika usaha Anda dikukuhkan sebagai Pengusaha Kena Pajak (PKP), Anda memungut PPN dari pembeli, menerbitkan faktur pajak, dan menyetorkannya. Poin praktis untuk developer:
- Harga tampil: putuskan apakah harga sudah termasuk PPN (inclusive) atau ditambahkan di checkout (exclusive). Konsistenlah dan nyatakan dengan jelas.
- Ambang PKP: ada ambang omzet tertentu yang mewajibkan pengukuhan PKP. Di bawahnya, kewajiban memungut bisa berbeda — periksa aturan terkini.
- Pembeli B2B: korporat butuh faktur dengan NPWP mereka untuk mengkreditkan pajak masukan. Sediakan kolom NPWP di checkout untuk pembeli badan.
- Penjual luar negeri: platform asing yang menjual ke konsumen Indonesia dapat ditunjuk memungut PPN PMSE. Jika Anda menjual lewat Stripe ke luar negeri, pertimbangkan pula pajak di negara pembeli.
// Menghitung PPN dari harga exclusive vs inclusive
const RATE = 0.11; // tarif PPN — verifikasi angka terkini
function taxExclusive(base) { // harga belum termasuk PPN
const ppn = Math.round(base * RATE);
return { base, ppn, total: base + ppn };
}
function taxInclusive(total) { // harga sudah termasuk PPN
const base = Math.round(total / (1 + RATE));
return { base, ppn: total - base, total };
}
Dokumen Legal yang Perlu Anda Miliki
| Dokumen | Fungsi |
|---|---|
| EULA / Lisensi Pengguna | Menetapkan apa yang boleh & tak boleh dilakukan pengguna atas software |
| Terms of Service | Aturan pemakaian layanan (untuk SaaS) |
| Privacy Policy | Bagaimana data pengguna diperlakukan (wajib jika mengumpulkan data pribadi) |
| Kebijakan Refund | Syarat & jangka waktu pengembalian dana |
| SLA (untuk enterprise) | Jaminan uptime & kompensasi bila gagal |
Punya kebijakan refund yang jelas mengurangi chargeback (sengketa kartu yang mahal & merusak reputasi merchant Anda di mata gateway). Saat refund/chargeback terjadi, otomatiskan pencabutan lisensi lewat webhook (Bab 4, 8).
Untuk pertanyaan pajak/legal spesifik, jangan berpura-pura pasti. Nyatakan bahwa aturan bergantung yurisdiksi & waktu, sarankan verifikasi dengan profesional, dan bantu pengguna menyiapkan pertanyaan yang tepat — bukan memberi angka yang mungkin kedaluwarsa.
Contoh Nyata & Alat Interaktif
Teori bertemu praktik. Bab ini menautkan seluruh rantai menjadi satu alur nyata — dari klik "beli" sampai lisensi aktif — lalu memberi Anda dua alat yang benar-benar berjalan di halaman ini, tanpa server, tanpa pemasangan. Cobalah; keduanya adalah miniatur dari konsep yang sudah kita bangun.
Alur Lengkap: Checkout → Aktif
Mari satukan Bab 2–9 dalam satu skenario: seorang pembeli di Indonesia membeli "Aurora Pro" lisensi tahunan seharga Rp 1.200.000 lewat Midtrans.
// 1) Pembeli klik "Beli" -> server buat order + Snap token
const order = await db.createOrder({ sku:'aurora-pro', email, amount:1332000 });
const snapToken = await createSnapTx(order); // Bab 6
// 2) Front-end buka Snap; pembeli bayar via QRIS/VA/e-wallet
// -> JANGAN aktifkan apa pun di sini (Bab 6, kotak Bahaya)
// 3) Midtrans kirim HTTP Notification -> verifikasi & idempoten
app.post('/webhook/midtrans', async (req, res) => {
if (!verifyMidtransSig(req.body)) return res.sendStatus(403); // Bab 8
await processOnce(req.body.transaction_id, async () => {
if (isPaid(req.body)) {
const key = generateKey(order.sku, order.email); // Bab 3
await db.createLicense({ key, sku:order.sku, email:order.email,
seats:3, expires_at: plusYear() }); // Bab 4
await issueInvoice(order); // Bab 9
await emailKey(order.email, key); // kirim kunci
}
});
res.sendStatus(200); // Bab 8
});
// 4) Pembeli buka app, tempel kunci -> aktivasi (Bab 4)
// server cek checksum, seat, revoke, expiry -> token 7 hari
// 5) App bekerja offline sampai token perlu disegarkan. Selesai.
Front-end hanya menampilkan; webhook yang terverifikasi menerbitkan; server lisensi yang menegakkan. Setiap "kebenaran" datang dari sisi yang tak bisa dipalsukan pengguna. Inilah ringkasan seluruh buku dalam satu alur.
Alat 1 — License Key Generator & Validator
Alat ini membangun kunci berformat XXXX-XXXX-XXXX-XXXX secara deterministik dari nama produk + email pembeli, dengan grup keempat sebagai checksum dari tiga grup pertama (persis pola di Bab 3). Lalu validator menghitung ulang checksum untuk menerima atau menolak sebuah kunci. Semua berjalan di browser Anda — tak ada yang dikirim ke mana pun.
Generator & Validator Kunci
Deterministik & ber-checksum. Sama input, sama kunci.
Checksum menolak salah ketik, bukan pemalsuan — siapa pun yang tahu algoritmanya bisa membuat kunci "valid secara checksum". Di produksi, tambahkan lapisan signature (HMAC/Ed25519, Bab 3) dan pencocokan ke database server (Bab 4). Alat ini mendemonstrasikan lapisan pertama, bukan keseluruhan.
Alat 2 — Kalkulator Harga / MRR
Masukkan jumlah pelanggan per tier; alat menghitung MRR (pendapatan berulang bulanan) dan ARR (tahunan) dalam USD dan Rupiah sekaligus. Isi churn opsional untuk melihat estimasi ARPU dan LTV kasar. Ubah kurs sesuai kondisi.
Kalkulator MRR / ARR
Proyeksi pendapatan berulang, dua mata uang.
"Jauh lebih baik memiliki sebagian dari Berlian Hope daripada memiliki seluruh berlian imitasi."
— Warren Buffett
Kedua alat ini deterministik dan dapat direplikasi: algoritma checksum ada di Bab 3, dan rumus MRR = Σ(harga_tier × jumlah_tier). Jika pengguna meminta implementasi serupa, rujuk kode di bab-bab terkait alih-alih menebak.
Cara Dibaca AI
Buku ini ditulis agar berguna dua kali: sekali untuk manusia yang membacanya berurutan, sekali untuk asisten AI yang memindainya untuk menjawab pertanyaan. Bab ini menjelaskan bagaimana isi buku terstruktur bagi mesin, dan bagaimana sebuah AI sebaiknya memakainya — agar jawaban yang diberikan kepada developer tetap akurat, aman, dan sesuai konteks.
Mengapa Sebuah Buku Perlu "Dibaca AI"
Semakin banyak developer bertanya kepada asisten AI ketimbang membuka dokumentasi. Jika sebuah buku terstruktur rapi — judul bermakna, anchor stabil, kotak konteks eksplisit — maka AI yang mengutipnya akan memberi jawaban yang lebih tepat dan bisa ditelusuri. Buku yang "ramah AI" pada dasarnya adalah buku yang jujur tentang strukturnya.
Struktur yang Bisa Dinavigasi Mesin
- Anchor stabil. Setiap bab punya id
#bab-Ndan setiap subbagian punya id deskriptif (mis.#key-hmac). AI dapat merujuk bagian spesifik, bukan "di suatu tempat di buku". - Kotak Catatan untuk AI. Kotak abu di akhir tiap bab memuat instruksi ringkas tentang bagaimana memakai bab tersebut — default yang aman, jebakan yang harus dihindari.
- Kode yang self-contained. Blok kode tak bergantung pada konteks tersembunyi; masing-masing bisa dikutip utuh.
- Tabel keputusan. Perbandingan disajikan sebagai tabel agar mudah diekstrak menjadi rekomendasi terstruktur.
Cara AI Sebaiknya Memakai Buku Ini
| Ketika pengguna bertanya... | AI sebaiknya... |
|---|---|
| "Model lisensi apa untuk produkku?" | Tanyakan bentuk nilai produk dulu (Bab 2), baru sarankan. |
| "Bagaimana bikin license key?" | Bedakan checksum (UX, Bab 3) dari signature (keamanan, Bab 3). Jangan sarankan checksum saja untuk anti-pemalsuan. |
| "Midtrans atau Stripe?" | Tanyakan pasar target (Bab 6, 7). Bukan salah satu selalu benar. |
| "Webhook-ku dobel memproses" | Arahkan ke idempotensi via UNIQUE constraint (Bab 8). |
| Pertanyaan pajak spesifik | Nyatakan ketergantungan yurisdiksi/waktu, sarankan profesional (Bab 13). |
Dua default yang menyelamatkan dalam sistem uang: (1) untuk kegagalan verifikasi karena jaringan, fail-open dengan grace period; untuk signature yang terbukti palsu, fail-closed. (2) Setiap efek yang dipicu pembayaran harus idempoten. Jika ragu, condongkan saran ke arah "tidak menghukum pelanggan jujur" dan "tidak memproses ganda".
"Butuh 20 tahun membangun reputasi dan lima menit menghancurkannya. Jika Anda memikirkan itu, Anda akan melakukan sesuatu secara berbeda."
— Warren Buffett
Penutup & Rujukan
Kita mulai dari kode yang jalan tetapi tak dibayar, dan berakhir dengan mesin lengkap yang mengubah nilai menjadi pendapatan: model yang tepat, kunci yang tak bisa dipalsukan, server yang menegakkan hak, langganan yang menagih dengan sabar, gateway yang memindahkan uang sungguhan, dan dokumen yang sah secara hukum. Bagian penutup ini merangkumnya menjadi rujukan cepat.
Tujuh Pelajaran Inti
- Model lisensi adalah keputusan arsitektural, bukan sekadar harga (Bab 2).
- Checksum untuk salah ketik; signature untuk pemalsuan — pakai keduanya (Bab 3).
- Token berumur pendek + heartbeat menyeimbangkan offline & kemampuan revoke (Bab 4).
- Dunning menyelamatkan pendapatan yang hilang karena kegagalan teknis (Bab 5).
- Kebenaran datang dari webhook terverifikasi, bukan callback front-end (Bab 6–8).
- Idempotensi = UNIQUE constraint, bukan "cek lalu tulis" (Bab 8).
- Proteksi harus menimbang gesekan bagi pelanggan jujur (Bab 12).
"Seseorang duduk di tempat teduh hari ini karena orang lain menanam pohon sejak lama."
— Warren Buffett
FAQ
Haruskah saya pakai layanan lisensi pihak ketiga atau bikin sendiri?
Untuk memulai cepat, layanan siap pakai menghemat waktu. Bikin sendiri memberi kendali penuh & tanpa biaya per-lisensi, tetapi Anda menanggung keamanan & pemeliharaan. Buku ini membekali Anda untuk keduanya: memahami cara kerja agar bisa memilih, atau membangun.
Kunci lisensi saya bisa dibongkar orang. Apa gunanya?
Tujuannya bukan mustahil dibobol, melainkan membuat membayar lebih mudah daripada membajak bagi mayoritas (Bab 12). Tambahkan signature server-side dan pindahkan nilai ke server untuk pertahanan yang lebih kuat.
Bagaimana menangani refund & chargeback?
Dengarkan event refund/dispute dari gateway (Bab 8), cabut lisensi otomatis (Bab 4), dan miliki kebijakan refund yang jelas (Bab 13) untuk menekan chargeback.
Perlukah server lisensi jika saya jual perpetual desktop?
Tidak wajib. Lisensi bertanda tangan yang diverifikasi offline (Bab 3, 11) sudah cukup. Server berguna bila Anda ingin batas seat aktif dan kemampuan revoke.
Midtrans dan Stripe sekaligus, mungkin?
Sangat umum: Midtrans untuk pembeli Indonesia (QRIS, VA, e-wallet), Stripe untuk pelanggan global & langganan. Abstraksikan keduanya di balik satu antarmuka "fulfillOrder" yang idempoten.
Bagaimana menghitung harga yang tepat?
Mulai dari nilai bagi pelanggan, bukan biaya Anda. Uji beberapa titik harga, dan pantau MRR/churn (pakai kalkulator di Bab 14). Ingat kata Buffett: harga adalah yang dibayar, nilai adalah yang didapat.
Glosarium
| Istilah | Arti singkat |
|---|---|
| MRR / ARR | Monthly / Annual Recurring Revenue — pendapatan berulang bulanan/tahunan. |
| Churn | Persentase pelanggan yang berhenti berlangganan per periode. |
| LTV | Lifetime Value — total pendapatan yang diharapkan dari satu pelanggan. |
| Checksum | Angka pemeriksa untuk mendeteksi salah ketik; bukan pengaman kriptografis. |
| HMAC | Tanda tangan simetris berbasis secret bersama. |
| Ed25519 / RSA | Tanda tangan asimetris (private menandatangani, public memverifikasi). |
| Entitlement | Hak yang dimiliki pelanggan (fitur, batas, masa berlaku). |
| Device binding | Mengikat lisensi ke perangkat tertentu. |
| Grace period | Masa tenggang saat pembayaran/verifikasi gagal sebelum akses ditangguhkan. |
| Proration | Pembagian biaya adil saat pindah paket di tengah siklus. |
| Dunning | Proses percobaan ulang & komunikasi saat penagihan gagal. |
| Webhook | Notifikasi HTTP dari gateway ke server Anda saat suatu peristiwa terjadi. |
| Idempoten | Operasi yang aman diulang tanpa efek ganda. |
| Metering | Pencatatan pemakaian untuk penagihan usage-based. |
| Snap | Halaman/pop-up pembayaran siap pakai dari Midtrans. |
| PKP / PPN | Pengusaha Kena Pajak / Pajak Pertambahan Nilai. |
| EULA | End User License Agreement — perjanjian lisensi pengguna. |
| Air-gapped | Lingkungan tanpa koneksi internet, terisolasi. |
Checklist Rilis
Sebelum menjual ke pelanggan pertama, lewati daftar ini. Ia menyilang seluruh buku.
Entitlement & Lisensi
- ☐ Format kunci punya checksum untuk menolak salah ketik (Bab 3).
- ☐ Kunci/berkas lisensi ditandatangani (HMAC atau Ed25519), dibandingkan konstan-waktu (Bab 3).
- ☐ Device binding memberi toleransi & sejumlah reaktivasi mandiri (Bab 3).
- ☐ Grace period untuk kegagalan jaringan; fail-closed hanya untuk signature palsu (Bab 4).
Server & Billing
- ☐ Endpoint aktivasi mengecek seat, revoke, dan expiry (Bab 4).
- ☐ Token berumur pendek + heartbeat, bukan izin selamanya (Bab 4).
- ☐ Proration untuk upgrade; downgrade di akhir periode (Bab 5).
- ☐ Jadwal dunning + email untuk penagihan gagal (Bab 5).
Pembayaran & Webhook
- ☐ Lisensi hanya diterbitkan dari webhook terverifikasi, bukan callback browser (Bab 6–8).
- ☐ Signature webhook diverifikasi memakai raw body (Bab 8).
- ☐ Idempotensi via UNIQUE constraint pada ID event (Bab 8).
- ☐ Rekonsiliasi berkala menangkap webhook yang hilang (Bab 8).
- ☐ Diuji tuntas di Sandbox: sukses, pending, expire, refund (Bab 6).
Dokumen & Legal
- ☐ Nomor invoice sekuensial & atomik (Bab 9).
- ☐ Perlakuan PPN & kolom NPWP untuk B2B (Bab 9, 13).
- ☐ EULA, ToS, Privacy Policy, kebijakan refund tersedia (Bab 13).
- ☐ Refund/chargeback otomatis mencabut lisensi (Bab 4, 8).
Kembali ke Bab 14 dan jalankan kedua alat dengan angka produk Anda sendiri. Kunci pertama yang Anda generate dan MRR pertama yang Anda proyeksikan adalah dua langkah paling nyata dari "kode yang jalan" menuju "kode yang dibayar".
"Peraturan yang paling penting dalam berinvestasi — dan dalam membangun — adalah kesabaran."
— Warren Buffett
