MVP 2 — Supabase Connection Deploy Guide
MVP 2 — Supabase Connection Deploy Guide
Implementasi nyata Supabase Auth + Postgres + RLS. Frontend tetap di GitHub Pages.
1. Pre-flight (Anda sudah lakukan)
- Buat Supabase project.
- Jalankan SQL schema (lihat
docs/SUPABASE_MIGRATION.md§5). - Enable RLS untuk table sensitif (§6).
Belum dilakukan tapi wajib sebelum login:
- Tambah
user_profilesrow untuk Anda sendiri dengan roleadmin:-- Run di Supabase SQL Editor SETELAH magic-link login pertama Anda berhasil. insert into user_profiles (user_id, role, full_name, affiliation) values ( (select id from auth.users where email = 'rezapramaaa@gmail.com'), 'admin', 'Reza Prama Arviandi', 'Universitas Indonesia' ) on conflict (user_id) do update set role = excluded.role; - Authentication → URL Configuration di Supabase dashboard:
- Site URL:
https://rezaprama.github.io/infraimpact/admin.html - Redirect URLs (allow list):
https://rezaprama.github.io/infraimpact/admin.htmlhttps://rezaprama.github.io/infraimpact/http://localhost:*(untuk dev lokal kalau perlu)
- Site URL:
- Authentication → Email Templates → Magic Link: confirm template berbahasa yang Anda mau.
- Tambah RLS INSERT policy untuk
audit_logs(kalau belum):alter table audit_logs enable row level security; create policy "audit_insert_authenticated" on audit_logs for insert to authenticated with check (auth.uid() is not null); create policy "audit_select_admin" on audit_logs for select to authenticated using (get_role(auth.uid()) in ('researcher','supervisor','admin')); - Tambah RLS untuk
user_profiles:alter table user_profiles enable row level security; create policy "self_read_profile" on user_profiles for select to authenticated using (user_id = auth.uid()); create policy "admin_manage_profiles" on user_profiles for all to authenticated using (get_role(auth.uid()) = 'admin') with check (get_role(auth.uid()) = 'admin'); - Tambah RLS untuk
settings:alter table settings enable row level security; create policy "public_read_settings" on settings for select to anon, authenticated using (true); create policy "admin_write_settings" on settings for insert, update, delete to authenticated using (get_role(auth.uid()) in ('researcher','admin'));
2. File baru yang ditambahkan ke repo
| File | Peran |
|---|---|
assets/js/admin/supabase-client.js | Init Supabase client + auth wrappers + RLS error decoder |
assets/js/admin/db-remote.js | Supabase implementation dari API db (mirror IndexedDB API) |
assets/js/admin/db-adapter.js | Switch backend berdasarkan STORAGE_MODE |
assets/js/admin/migrate-supabase.js | Migrate IndexedDB → Supabase (dan reverse pull) |
docs/MVP2_SUPABASE_DEPLOY.md | File ini |
| File yang dimodifikasi | Perubahan | |
|---|---|---|
admin.html | + CDN @supabase/supabase-js@2; + dual login form (passcode | email); script order baru |
assets/js/admin/auth.js | Dual-mode (local pseudo-login + Supabase magic link), API surface sama | |
assets/js/admin/studio-app.js | Dual login wiring + role-based menu hiding + auth-state listener | |
assets/js/admin/settings.js | + Storage Mode card + Migration card |
Modul yang tidak diubah (karena memakai window.IFI.studio.db abstrak): audit.js, raw-viewer.js, bwm-studio.js, calc-studio.js, quality.js, export-center.js, forms.js, form-schemas.js, csv-io.js, pdf-export.js.
3. Konfigurasi (3 baris di DevTools console)
Buka https://rezaprama.github.io/infraimpact/admin.html, F12 → Console:
localStorage.setItem('IFI_SUPABASE_URL', 'https://YOUR-PROJECT.supabase.co');
localStorage.setItem('IFI_SUPABASE_ANON', 'eyJhbGci...YOUR-ANON-KEY');
localStorage.setItem('IFI_STORAGE_MODE', 'supabase');
location.reload();
Atau lebih permanen — edit assets/js/admin/supabase-client.js baris 22-25:
const DEFAULTS = {
url: 'https://YOUR-PROJECT.supabase.co',
anon: 'eyJhbGci...YOUR-ANON-KEY'
};
JANGAN commit service_role key. Hanya anon.
4. Smoke test alur (15 menit)
| # | Step | Expected |
|---|---|---|
| 1 | Reload admin.html | Login form ber-mode email magic-link |
| 2 | Masukkan email Anda → klik Kirim Magic Link | Banner info “Magic link dikirim ke …” |
| 3 | Cek inbox → klik link | Redirect kembali ke admin.html dan otomatis logged in |
| 4 | Run di SQL Editor: insert into user_profiles (user_id, role) values ((select id from auth.users where email = '<your email>'), 'admin') on conflict do update set role='admin'; | row tersimpan |
| 5 | Reload admin.html | Badge sidebar menampilkan admin · <email> |
| 6 | Buka view Settings & Method → card Storage Mode | STORAGE_MODE=supabase, Client ready=yes |
| 7 | Klik Migrate (live) → konfirmasi | Log menunjukkan tiap table di-upsert. Total = ~571 + 1442 + 22 + … |
| 8 | Cek di Supabase dashboard → Table editor → assets | ≥571 rows |
| 9 | Reload admin.html → buka Raw Asset Database | Tabel populated dari Supabase (bukan IndexedDB) |
| 10 | Klik salah satu asset → drawer terbuka | Detail muncul (Supabase query sukses) |
| 11 | Buka Quality view | Audit log baru tampil (mencatat login, seed, migrate_local_to_supabase) |
| 12 | Logout | Kembali ke login form |
| 13 | Klik magic link lama → sukses | Auto-login lagi |
5. Pemetaan tabel & primary key
STORES key (frontend) | Postgres table | Primary key |
|---|---|---|
assets | assets | asset_id |
yearly_observations | yearly_observations | observation_id |
interviews | interviews | interview_id |
fgds | fgds | fgd_id |
field_observations | field_observations | observation_id |
documents | documents | document_id |
bwm_experts | bwm_experts | expert_id |
bwm_inputs | bwm_inputs | input_id |
bwm_weights | bwm_weights | weight_id |
indicators | indicators | indicator_id |
scores | scores | score_id |
audit_logs | audit_logs | audit_id |
settings | settings | key |
Jika nama table di Supabase Anda berbeda (mis. document_evidence vs documents), edit STORES di db-remote.js baris 13-25.
6. Role → menu visibility
| Role | Bisa lihat menu |
|---|---|
public_viewer | Raw Asset Database (read-only) |
enumerator | Raw Asset Database, Interview/FGD/Field/Document Forms |
supervisor | Raw Asset Database, Quality, Export Center |
researcher | Semua kecuali admin-only |
admin | Semua |
Dikontrol di studio-app.js baris 11-21 (VIEWS[*].roles).
7. RLS error handling
Setiap error dari Supabase diparse oleh explainError() di supabase-client.js. Kode umum:
| Code | Arti | Yang harus dilakukan |
|---|---|---|
42501 | RLS deny | Cek user_profiles.role, cek policy USING/WITH CHECK |
PGRST301 | JWT expired | Logout + login ulang |
23505 | Duplicate PK | Record sudah ada — pakai update bukan insert |
23503 | FK violation | Parent row belum ada (mis. insert observation sebelum asset) |
23514 | CHECK constraint | Nilai di luar enum (asset_type, funding_source, role) |
PGRST204 | Schema cache | Restart project, atau cek nama column |
Banner error di view body akan menampilkan kode + penjelasan. Detail lengkap ada di console.
8. Rollback ke MVP 1a
Kalau Supabase down atau cost issue:
localStorage.setItem('IFI_STORAGE_MODE', 'local');
location.reload();
Atau via Settings view → Switch ke LOCAL. IndexedDB tetap utuh — tidak ada yang hilang.
9. Security boundary (penting)
- anon key boleh di repo publik. Itu memang token untuk klien anonim. Semua keamanan via RLS.
- service_role key TIDAK BOLEH ada di frontend. Pakai hanya di SQL Editor atau backend yang Anda kendalikan.
- Magic link dikirim Supabase, bukan dari frontend Anda. Anda tidak butuh SMTP.
- Email rate limit Supabase Free: 4 emails/jam. Untuk produksi: setup custom SMTP di Project Settings → Auth.
- Magic link expires 1 jam default. Bisa di-extend di Auth settings.
- JWT auto-refresh sudah aktif (60 menit default).
10. Audit trail di MVP 2
audit.js tidak berubah — masih panggil db.put('audit_logs', record). Dalam mode supabase, ini insert ke table audit_logs. Setiap audit log otomatis dapat created_by = auth.uid() via db-remote.put().
Untuk tamper-evident audit (Q1 publication requirement): tambahkan trigger Postgres:
create or replace function audit_no_update() returns trigger as $$
begin
raise exception 'audit_logs immutable — cannot UPDATE/DELETE';
end;
$$ language plpgsql;
create trigger trg_audit_immutable
before update or delete on audit_logs
for each row execute function audit_no_update();
11. Known limitations
db.count()di Supabase bisa diblok RLS — fallback return 0 dengan log warning.db.clear()di Supabase mode =DELETE WHERE pk <> '__never__'. Untuk wipe massal lewat dashboard saja (lebih cepat).seedIfEmpty()di Supabase mode hanya jalan kalau tabel kosong. Idempotent. Tapi butuh role write.- Postgrest default limit 1000 rows per request —
getAll()sudah pagination otomatis sampai habis.
12. Verifikasi cepat — copy-paste di console
// 1. cek mode aktif
console.log('mode:', IFI.studio.STORAGE_MODE);
console.log('supa ready:', !!IFI.studio.supa.client);
// 2. cek session
await IFI.studio.supa.getSession();
// 3. cek role
await IFI.studio.auth.fetchProfile();
// 4. count semua table (dari Supabase)
for (const t of Object.keys(IFI.studio.db.STORES)) {
console.log(t.padEnd(22), await IFI.studio.db.count(t));
}
// 5. test insert audit (RLS check)
await IFI.studio.audit.logEvent('smoke_test', { ts: Date.now() });
// 6. dry-run migration
await IFI.studio.migrate.migrate(console.log, { dryRun: true });
End of MVP 2 deploy guide. Bug/issue → log di console + paste output ke researcher.
