| Layanan | Kegunaan | Status |
|---|---|---|
| Google Account | Login ke Firebase Console | Wajib Gratis |
| Cloudinary | Hosting foto bukti transfer | Wajib Gratis |
| Cloudflare | Worker untuk hapus foto otomatis | Wajib Gratis |
| GitHub | Hosting (Opsi A) / simpan kode | Opsional Gratis |
| Netlify / Vercel | Hosting alternatif (Opsi B/C) | Opsional Gratis |
nodejs.org.index.html โ file utama aplikasi (sudah Anda punya).sw.js โ file service worker untuk PWA (akan dibuat di Bagian 6).Total instalasi dari nol sampai online: 45โ90 menit. Jika sudah familiar dengan Firebase & hosting, bisa selesai dalam 30 menit.
Firebase adalah layanan dari Google yang akan menyimpan seluruh data aplikasi (warga, tagihan, kas, pengaduan, dll). Kita akan menggunakan dua fitur utama:
Buka browser dan kunjungi:
https://console.firebase.google.com
Login menggunakan akun Google Anda.
rt9bc2 atau siwarta-rt.
Di sidebar kiri Firebase Console, klik:
Build โ Firestore Database
asia-southeast1 (Singapore) โ Direkomendasikan untuk Indonesia.asia-southeast2 (Jakarta) โ jika tersedia, paling cepat untuk pengguna Indonesia.Setelah database dibuat, lokasi server tidak dapat dipindahkan. Pilih dengan hati-hati. Untuk warga Indonesia, Singapore atau Jakarta adalah pilihan terbaik.
apps โ klik Next.rtdigital โ klik Next.initializedbooleantrueAplikasi dirancang untuk membaca & menulis ke dokumen apps/rtdigital. Jika dokumen ini belum ada, saat aplikasi pertama kali dijalankan, ia akan otomatis membuat seed data di sana. Membuat manual memastikan struktur awal terbentuk.
Build โ Authentication
Klik tombol "Get started".
Aplikasi memakai login manual sendiri (username + password yang disimpan di Firestore). Firebase Authentication hanya dipakai untuk memenuhi security rules, sehingga siapapun yang mengakses aplikasi harus terautentikasi (dalam hal ini anonymous) sebelum bisa read/write database.
Masih di Firestore Database, klik tab Rules di bagian atas.
Hapus semua isi default, lalu paste kode di bawah ini:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Dokumen utama aplikasi
match /apps/{docId} {
allow read, write: if request.auth != null;
}
// Role user (untuk multi-role: ketua/bendahara/keamanan)
match /userRoles/{uid} {
allow read, write: if request.auth != null
&& request.auth.uid == uid;
}
// Default: tolak semua akses lain
match /{document=**} {
allow read, write: if false;
}
}
}
Klik tombol Publish di kanan atas. Tunggu sampai muncul notifikasi "Rules published".
Test mode membuka akses baca/tulis ke semua orang di internet tanpa autentikasi. Rules di atas sudah cukup aman untuk aplikasi RT dengan sistem login internal.
Klik logo Firebase / Project Overview di kiri atas, atau:
Project settings โ General
SIWARTA Web.Firebase akan menampilkan kode seperti ini โ salin dan simpan:
const firebaseConfig = {
apiKey: "AIzaSyAgH9Zz8ORfIFAmvSahAMOJBJld3_yxK_U",
authDomain: "rt9bc2.firebaseapp.com",
projectId: "rt9bc2",
storageBucket: "rt9bc2.firebasestorage.app",
messagingSenderId: "480773365990",
appId: "1:480773365990:web:aa1acf0bbdc43a7c4c9b27"
};
Anda akan membutuhkannya di Bagian 5 saat mengedit index.html. Simpan di notepad sementara.
Selesai! Firebase sudah siap.
Cloudinary dipakai untuk menyimpan foto bukti transfer, selfie absen jaga, lampiran pengaduan, dan foto bank sampah. Free tier Cloudinary memberikan 25 GB storage + 25 GB bandwidth/bulan โ lebih dari cukup untuk RT.
https://cloudinary.com/users/register_free
Klik Create Account. Verifikasi email Anda.
Setelah login, Anda akan diarahkan ke Dashboard. Cari bagian "Account Details":
Cloud name: e0sfugt2
API Key: 123456789012345
API Secret: xxxxxxxxxxxxxxxxxxxx
Cloud Name bersifat unik dan publik โ aman disimpan di kode frontend. Contoh di atas (e0sfugt2) hanya ilustrasi, milik Anda akan berbeda.
Upload Preset adalah "profil" upload yang menentukan folder, format, dan ukuran file yang diizinkan. Kita pakai unsigned preset supaya bisa upload langsung dari browser tanpa perlu signature server-side.
Di sidebar kiri, klik ikon gerigi (โ๏ธ) โ Upload.
Atau langsung ke:
https://console.cloudinary.com/settings/upload
Klik "Add upload preset".
| Field | Nilai |
|---|---|
| Preset name | rt_bukti (harus persis sama!) |
| Signing Mode | Unsigned |
| Folder | rt-bukti |
| Access mode | Public (default) |
| Use filename | Off |
| Unique filename | On โ |
| Overwrite | Off |
| Incoming transformation | Kosongkan (biar kode yang handle kompresi) |
Klik Save di kanan atas.
Preset rt_bukti sekarang bisa dipakai untuk upload dari browser tanpa login. Ini yang akan dipakai index.html.
Folder rt-bukti akan otomatis dibuat saat upload pertama. Tidak perlu dibuat manual.
Signed mode butuh signature valid dari server. Karena aplikasi kita murni frontend (tanpa server), WAJIB pakai Unsigned. Preset salah = semua upload akan gagal dengan error 400.
Cloudflare Worker di sini berfungsi sebagai "penghapus foto otomatis". Aplikasi akan memanggilnya untuk menghapus bukti transfer yang sudah lebih dari 30 hari, agar storage Cloudinary tidak penuh.
Cloudinary melarang proses delete dari browser langsung (karena butuh API Secret). Solusinya: buat "jembatan" berupa Cloudflare Worker yang menyimpan API Secret secara aman dan menerima perintah hapus dari aplikasi.
https://dash.cloudflare.com/sign-up
Daftar dengan email, verifikasi, lalu login.
Di sidebar kiri: Workers & Pages โ Create application โ Create Worker.
Ketik nama, contoh: rt-cloudinary-delete. Klik Deploy.
Setelah deploy sukses, klik Edit code (atau "Quick edit"). Hapus kode default, ganti dengan kode berikut:
// Cloudflare Worker โ Cloudinary Delete Proxy
export default {
async fetch(request, env, ctx) {
// CORS headers
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
// Preflight
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
if (request.method !== 'POST') {
return new Response(
JSON.stringify({ success: false, error: 'Method not allowed' }),
{ status: 405, headers: { 'Content-Type': 'application/json', ...corsHeaders } }
);
}
try {
const { publicIds } = await request.json();
if (!Array.isArray(publicIds) || publicIds.length === 0) {
return new Response(
JSON.stringify({ success: false, error: 'publicIds kosong' }),
{ status: 400, headers: { 'Content-Type': 'application/json', ...corsHeaders } }
);
}
const CLOUD_NAME = env.CLOUDINARY_CLOUD_NAME;
const API_KEY = env.CLOUDINARY_API_KEY;
const API_SECRET = env.CLOUDINARY_API_SECRET;
if (!CLOUD_NAME || !API_KEY || !API_SECRET) {
return new Response(
JSON.stringify({ success: false, error: 'Env variable belum diset' }),
{ status: 500, headers: { 'Content-Type': 'application/json', ...corsHeaders } }
);
}
const results = [];
for (const publicId of publicIds) {
try {
const timestamp = Math.floor(Date.now() / 1000);
const toSign = `public_id=${publicId}×tamp=${timestamp}${API_SECRET}`;
const signature = await sha1(toSign);
const formData = new FormData();
formData.append('public_id', publicId);
formData.append('api_key', API_KEY);
formData.append('timestamp', timestamp);
formData.append('signature', signature);
const res = await fetch(
`https://api.cloudinary.com/v1_1/${CLOUD_NAME}/image/destroy`,
{ method: 'POST', body: formData }
);
const data = await res.json();
results.push({ publicId, result: data.result, error: data.error });
} catch (err) {
results.push({ publicId, result: 'error', error: err.message });
}
}
const sukses = results.filter(r => r.result === 'ok' || r.result === 'not found').length;
return new Response(
JSON.stringify({ success: true, deleted: sukses, total: publicIds.length, results }),
{ headers: { 'Content-Type': 'application/json', ...corsHeaders } }
);
} catch (e) {
return new Response(
JSON.stringify({ success: false, error: e.message }),
{ status: 500, headers: { 'Content-Type': 'application/json', ...corsHeaders } }
);
}
}
};
// Helper SHA-1
async function sha1(str) {
const buf = new TextEncoder().encode(str);
const hash = await crypto.subtle.digest('SHA-1', buf);
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
Klik Save and Deploy di kanan atas.
Di halaman Worker, klik tab Settings โ Variables and Secrets.
Klik Add variable tiga kali, isi sebagai berikut (ambil dari Dashboard Cloudinary di Bagian 3.2):
| Variable name | Value | Type |
|---|---|---|
CLOUDINARY_CLOUD_NAME | Cloud Name Anda (contoh: e0sfugt2) | Text |
CLOUDINARY_API_KEY | API Key dari Cloudinary | Secret |
CLOUDINARY_API_SECRET | API Secret dari Cloudinary | Secret |
Kalau diset sebagai "Text", siapa pun yang bisa akses dashboard Worker bisa lihat nilainya. Untuk keamanan, gunakan tipe Secret.
Klik Save and Deploy lagi supaya variable aktif.
Kembali ke halaman utama Worker. URL akan tampak seperti ini:
https://rt-cloudinary-delete.username.workers.dev
Salin URL ini โ akan dipakai di Bagian 5.
Untuk memastikan Worker bekerja, buka Terminal / Command Prompt, jalankan:
curl -X POST https://rt-cloudinary-delete.username.workers.dev ^
-H "Content-Type: application/json" ^
-d "{\"publicIds\":[\"dummy-test-id\"]}"
Respons yang benar (meski public_id tidak ada):
{"success":true,"deleted":1,"total":1,"results":[{"publicId":"dummy-test-id","result":"not found"}]}
Sekarang saatnya menempelkan konfigurasi dari langkah-langkah sebelumnya ke dalam file index.html.
Klik kanan file index.html โ Open with โ VS Code / Notepad++.
Gunakan Ctrl+F, cari kata kunci:
firebaseConfig
Anda akan menemukan kode seperti ini di baris ~30:
const firebaseConfig = {
apiKey: "AIzaSyAgH9Zz8ORfIFAmvSahAMOJBJld3_yxK_U",
authDomain: "rt9bc2.firebaseapp.com",
projectId: "rt9bc2",
storageBucket: "rt9bc2.firebasestorage.app",
messagingSenderId: "480773365990",
appId: "1:480773365990:web:aa1acf0bbdc43a7c4c9b27"
};
Ganti semua nilai dengan config dari Firebase Console Anda (yang disalin di Bagian 2.5):
const firebaseConfig = {
apiKey: "PASTE_API_KEY_ANDA_DI_SINI",
authDomain: "NAMA_PROJECT.firebaseapp.com",
projectId: "NAMA_PROJECT",
storageBucket: "NAMA_PROJECT.firebasestorage.app",
messagingSenderId: "PASTE_SENDER_ID",
appId: "PASTE_APP_ID"
};
Satu karakter salah = login gagal. Copy-paste langsung dari Firebase Console, jangan ketik ulang manual.
Masih dengan Ctrl+F, cari:
CLOUDINARY_CLOUD_NAME
Anda akan menemukan 3 baris ini (baris ~44):
const CLOUDINARY_CLOUD_NAME = 'e0sfugt2';
const CLOUDINARY_UPLOAD_PRESET = 'rt_bukti';
const CLOUDINARY_UPLOAD_URL = 'https://api.cloudinary.com/v1_1/' + CLOUDINARY_CLOUD_NAME + '/auto/upload';
const CLOUDINARY_DELETE_WORKER_URL = 'https://rt-cloudinary-delete.helmy-bagus-saputra.workers.dev';
Ganti 2 nilai berikut:
const CLOUDINARY_CLOUD_NAME = 'CLOUD_NAME_ANDA'; // โ dari Bagian 3.2
const CLOUDINARY_UPLOAD_PRESET = 'rt_bukti'; // โ JANGAN DIUBAH
const CLOUDINARY_UPLOAD_URL = 'https://api.cloudinary.com/v1_1/' + CLOUDINARY_CLOUD_NAME + '/auto/upload';
const CLOUDINARY_DELETE_WORKER_URL = 'URL_WORKER_ANDA'; // โ dari Bagian 4.4
Kalau Anda mengubah nama preset di Cloudinary, ubah juga nilai CLOUDINARY_UPLOAD_PRESET. Tapi disarankan tetap rt_bukti supaya konsisten.
Periksa sekali lagi dengan Ctrl+F, cari kata-kata ini, pastikan tidak ada lagi:
AIzaSyAgH9Zz8ORfIFAmvSahAMOJBJld3_yxK_U (harus sudah diganti)rt9bc2 (harus sudah diganti dengan project ID Anda)e0sfugt2 (harus sudah diganti dengan Cloud Name Anda)rt-cloudinary-delete.helmy-bagus-saputra.workers.dev (harus sudah diganti)Simpan file dengan Ctrl+S.
File sw.js membuat aplikasi bisa dibuka saat offline dan di-install ke home screen HP (PWA). Ini opsional, tapi sangat direkomendasikan.
Di folder yang sama dengan index.html, buat file baru bernama:
sw.js
// sw.js โ Service Worker SIWARTA PWA
const CACHE_NAME = 'siwarta-v1';
const PRECACHE = [
'./',
'./index.html'
];
// Install: cache aset dasar
self.addEventListener('install', (e) => {
e.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(PRECACHE))
.catch(err => console.warn('[SW] Precache gagal:', err))
);
self.skipWaiting();
});
// Activate: bersihkan cache lama
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then(keys => Promise.all(
keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k))
))
);
self.clients.claim();
});
// Fetch: network-first untuk API, cache-first untuk aset statis
self.addEventListener('fetch', (e) => {
const url = e.request.url;
// Skip non-GET
if (e.request.method !== 'GET') return;
// Skip Firebase, Cloudinary, Google APIs โ selalu live
if (url.includes('firestore') ||
url.includes('firebase') ||
url.includes('cloudinary') ||
url.includes('googleapis') ||
url.includes('gstatic') ||
url.includes('workers.dev')) {
return;
}
e.respondWith(
fetch(e.request)
.then(res => {
// Simpan clone ke cache untuk offline
if (res && res.status === 200) {
const clone = res.clone();
caches.open(CACHE_NAME)
.then(c => c.put(e.request, clone))
.catch(() => {});
}
return res;
})
.catch(() => caches.match(e.request).then(cached => cached || caches.match('./index.html')))
);
});
Ctrl+S. Sekarang folder Anda punya 2 file:
๐ folder-anda/
โโโ index.html
โโโ sw.js
Hapus saja file sw.js. Aplikasi tetap jalan normal, hanya saja tidak bisa dibuka offline dan tidak muncul tombol "Install Aplikasi". Service worker akan di-skip otomatis oleh kode.
Kalau dibuka dengan file:///C:/Users/.../index.html, maka:
Buka Terminal / Command Prompt di folder tempat index.html berada:
Windows (Command Prompt):
cd C:\path\ke\folder\anda
python -m http.server 8000
macOS / Linux:
cd /path/ke/folder/anda
python3 -m http.server 8000
Buka browser, akses:
http://localhost:8000
npx serve
Install ekstensi "Live Server" di VS Code. Klik kanan index.html โ "Open with Live Server".
firestore.googleapis.com yang statusnya 200.Di Console browser, ketik:
console.log('Firebase ready?', fbReady);
console.log('Project:', firebaseConfig.projectId);
Harus keluar: Firebase ready? true. Kalau false, berarti config Anda salah.
Pilih salah satu dari 4 opsi di bawah. Untuk pemula, saya rekomendasikan Netlify Drop (paling cepat) atau GitHub Pages (paling fleksibel).
https://app.netlify.com/drop
Seret folder yang berisi index.html dan sw.js ke area upload. Netlify otomatis deploy dalam 10โ30 detik.
Setelah deploy, Anda akan dapat URL random seperti:
https://spontaneous-cat-a1b2c3.netlify.app
Klik Site settings โ Change site name. Ganti jadi:
https://siwarta-rt09.netlify.app
Tanpa akun Netlify, site akan hilang dalam beberapa jam. Klik Claim your site untuk daftar gratis.
https://github.com/signup
siwarta-rtindex.html dan sw.js ke area upload.Deploy from a branch.main, folder / (root).https://username-anda.github.io/siwarta-rt/
Setiap kali Anda edit index.html, upload ulang ke GitHub. GitHub Pages otomatis rebuild dalam 1 menit.
https://vercel.com/signup
Klik Add New... โ Project โ Continue with GitHub (import dari repo GitHub) atau upload folder manual.
Klik Deploy. Tunggu 30 detik. URL akan muncul:
https://siwarta-rt.vercel.app
Buka Terminal / Command Prompt, ketik:
npm install -g firebase-tools
firebase login
Browser akan terbuka. Login dengan akun Google yang dipakai di Firebase.
Masuk ke folder Anda, lalu:
cd path/ke/folder/anda
firebase init hosting
Jawab pertanyaan:
Use an existing project โ pilih project Firebase Anda.. (titik, artinya current folder).NoNoNofirebase deploy
Setelah selesai, URL akan muncul:
https://projectid-anda.web.app
https://projectid-anda.firebaseapp.com
Setelah aplikasi online, lakukan langkah-langkah berikut untuk mengisi data awal:
A-01admin atau nama AndaKarena belum ada Ketua RT yang bisa approve, Anda harus meng-approve diri sendiri lewat Firebase Console.
apps โ dokumen rtdigital.warga (array). Klik untuk expand.status: dari "Pending" โ "Aktif"roles: dari ["warga"] โ ["warga","ketua","bendahara","keamanan"]Login sebagai Ketua RT, lalu isi data berikut secara berurutan:
RT 9 BC 2.Login sebagai Bendahara (atau dari akun Ketua, buka Pengaturan):
Login sebagai Bendahara/Ketua โ Pengaturan โ Data & Backup โ Backup Sekarang. File JSON akan terunduh. Simpan di Google Drive / email.
Penyebab: Config Firebase salah atau project belum aktif.
Solusi:
firebaseConfig di index.html โ pastikan tidak ada typo.Penyebab: Firestore Rules terlalu ketat, atau Anonymous Auth belum aktif.
Solusi:
allow read, write: if request.auth != null; untuk apps/{docId}.Penyebab: Preset name salah, atau preset bukan Unsigned.
Solusi:
rt_bukti ada.CLOUDINARY_CLOUD_NAME di index.html benar.Penyebab: Env variables di Cloudflare belum tersimpan.
Solusi:
CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET.Penyebab: Izin lokasi ditolak, atau dibuka via file://.
Solusi:
https:// atau http://localhost.Penyebab: Sinkronisasi Firestore lambat, atau offline.
Solusi:
Penyebab: Cache lokal browser.
Solusi:
Penyebab: PWA hanya bisa diinstall via HTTPS.
Solusi:
Penyebab: JavaScript error.
Solusi:
Unexpected token = ada karakter aneh di kode. Copy ulang index.html.fbReady is not defined = file rusak. Redownload.Penyebab: Data seharusnya tersimpan di Firestore, bukan lokal.
Solusi:
apps โ dokumen rtdigital. Kalau kosong, ada masalah autentikasi.Kalau masalah Anda tidak ada di daftar di atas:
https://console.firebase.google.comhttps://console.cloudinary.comhttps://dash.cloudflare.comhttps://firebase.google.com/docs/firestorehttps://cloudinary.com/documentationSIWARTA โ Sistem Informasi Warga Terpadu
Panduan Instalasi Versi 1.0 ยท 2026
Dokumen ini bebas dibagikan untuk keperluan lingkungan RT/RW.