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_profiles row untuk Anda sendiri dengan role admin:
    -- 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.html
      • https://rezaprama.github.io/infraimpact/
      • http://localhost:* (untuk dev lokal kalau perlu)
  • 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

FilePeran
assets/js/admin/supabase-client.jsInit Supabase client + auth wrappers + RLS error decoder
assets/js/admin/db-remote.jsSupabase implementation dari API db (mirror IndexedDB API)
assets/js/admin/db-adapter.jsSwitch backend berdasarkan STORAGE_MODE
assets/js/admin/migrate-supabase.jsMigrate IndexedDB → Supabase (dan reverse pull)
docs/MVP2_SUPABASE_DEPLOY.mdFile ini
File yang dimodifikasiPerubahan 
admin.html+ CDN @supabase/supabase-js@2; + dual login form (passcodeemail); script order baru
assets/js/admin/auth.jsDual-mode (local pseudo-login + Supabase magic link), API surface sama 
assets/js/admin/studio-app.jsDual 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)

#StepExpected
1Reload admin.htmlLogin form ber-mode email magic-link
2Masukkan email Anda → klik Kirim Magic LinkBanner info “Magic link dikirim ke …”
3Cek inbox → klik linkRedirect kembali ke admin.html dan otomatis logged in
4Run 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
5Reload admin.htmlBadge sidebar menampilkan admin · <email>
6Buka view Settings & Method → card Storage ModeSTORAGE_MODE=supabase, Client ready=yes
7Klik Migrate (live) → konfirmasiLog menunjukkan tiap table di-upsert. Total = ~571 + 1442 + 22 + …
8Cek di Supabase dashboard → Table editor → assets≥571 rows
9Reload admin.html → buka Raw Asset DatabaseTabel populated dari Supabase (bukan IndexedDB)
10Klik salah satu asset → drawer terbukaDetail muncul (Supabase query sukses)
11Buka Quality viewAudit log baru tampil (mencatat login, seed, migrate_local_to_supabase)
12LogoutKembali ke login form
13Klik magic link lama → suksesAuto-login lagi

5. Pemetaan tabel & primary key

STORES key (frontend)Postgres tablePrimary key
assetsassetsasset_id
yearly_observationsyearly_observationsobservation_id
interviewsinterviewsinterview_id
fgdsfgdsfgd_id
field_observationsfield_observationsobservation_id
documentsdocumentsdocument_id
bwm_expertsbwm_expertsexpert_id
bwm_inputsbwm_inputsinput_id
bwm_weightsbwm_weightsweight_id
indicatorsindicatorsindicator_id
scoresscoresscore_id
audit_logsaudit_logsaudit_id
settingssettingskey

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

RoleBisa lihat menu
public_viewerRaw Asset Database (read-only)
enumeratorRaw Asset Database, Interview/FGD/Field/Document Forms
supervisorRaw Asset Database, Quality, Export Center
researcherSemua kecuali admin-only
adminSemua

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:

CodeArtiYang harus dilakukan
42501RLS denyCek user_profiles.role, cek policy USING/WITH CHECK
PGRST301JWT expiredLogout + login ulang
23505Duplicate PKRecord sudah ada — pakai update bukan insert
23503FK violationParent row belum ada (mis. insert observation sebelum asset)
23514CHECK constraintNilai di luar enum (asset_type, funding_source, role)
PGRST204Schema cacheRestart 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.