πŸ—„οΈ Supabase β€” Backend Setup ← Hub Blueprint
βš™οΈ Konfigurasi Project Supabase
πŸ”„ Mengecek koneksi otomatis ke server (config.php / /api/config)...
Project URL
Anon Key (aman untuk frontend)
Project Ref (ID singkat)
Service Role Key (JANGAN expose ke frontend)
Disimpan lokal β€” tidak terkirim ke mana pun
πŸš€ Setup Awal Supabase Project
πŸ’‘ Supabase = backend open-source (PostgreSQL + Edge Functions + Storage + Auth) yang bisa dipakai gratis untuk project awal. Kamu tidak perlu server sendiri.
1
Buat Akun & Project Baru
1. Buka supabase.com β†’ klik Start your project
2. Sign up dengan GitHub (paling cepat)
3. Klik New Project β†’ pilih organisasi β†’ isi nama project & region
4. Pilih region: Southeast Asia (Singapore) untuk latensi terbaik dari Indonesia
5. Tunggu ~2 menit sampai project siap
2
Ambil API Keys
Di Supabase dashboard: Settings (βš™οΈ) β†’ API

Yang perlu dicatat:
β€’ Project URL β†’ format: https://[ref].supabase.co
β€’ anon / public key β†’ aman dipakai di frontend HTML/JS
β€’ service_role key β†’ HANYA untuk edge functions & server, JANGAN expose
β€’ Project Ref β†’ ID singkat di URL dashboard (misal: iyatwaubpyqbjwrguuna)
3
Install Supabase CLI
Dibutuhkan untuk deploy Edge Functions dan manage project via terminal.
# Opsi 1: via npm (paling umum) npm install -g supabase # Opsi 2: via Homebrew (macOS) brew install supabase/tap/supabase # Verifikasi instalasi supabase --version # Login dengan akun Supabase supabase login # Link ke project kamu supabase link --project-ref [PROJECT_REF] # Contoh supabase link --project-ref iyatwaubpyqbjwrguuna
4
Init Struktur Folder Lokal
# Dari folder project kamu supabase init # Struktur yang dibuat: # supabase/ # β”œβ”€β”€ config.toml ← konfigurasi project # β”œβ”€β”€ migrations/ ← file SQL migration # └── functions/ ← folder edge functions # β”œβ”€β”€ save-lead/ # β”œβ”€β”€ payment-webhook/ # β”œβ”€β”€ send-email/ # └── quiz-result/
5
Test Koneksi
// Test koneksi dari browser console atau file HTML const SUPABASE_URL = '[PROJECT_URL]' const SUPABASE_ANON_KEY = '[ANON_KEY]' // Test query sederhana (setelah tabel leads dibuat) const res = await fetch(`${SUPABASE_URL}/rest/v1/leads?limit=1`, { headers: { 'apikey': SUPABASE_ANON_KEY, 'Authorization': `Bearer ${SUPABASE_ANON_KEY}` } }) const data = await res.json() console.log('Koneksi OK:', data)
πŸ—„οΈ Database Schema β€” Semua Tabel
πŸ’‘ Jalankan semua SQL ini di Supabase Dashboard β†’ SQL Editor. Urutan: jalankan satu per satu, atau semuanya sekaligus.
β‘ 
Tabel: leads β€” Untuk Landing Page & Lead Magnet
-- Tabel utama untuk simpan semua leads dari landing page, lead magnet, quiz, dll CREATE TABLE IF NOT EXISTS leads ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, email TEXT NOT NULL, name TEXT, phone TEXT, source TEXT, -- 'landing_page' | 'lead_magnet' | 'quiz' | 'cold_wa' | 'organic' utm_source TEXT, utm_medium TEXT, utm_campaign TEXT, utm_content TEXT, fbc TEXT, -- Meta click ID (fbclid β†’ diformat jadi fbc) fbp TEXT, -- Meta browser pixel ID (cookie _fbp) user_agent TEXT, ip TEXT, status TEXT DEFAULT 'lead', -- 'lead' | 'nurture' | 'buyer' | 'refund' | 'unsubscribed' tags TEXT[], -- array tag: ['quiz_taker', 'email_subscriber', ...] notes TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Index untuk query cepat CREATE INDEX IF NOT EXISTS leads_email_idx ON leads(email); CREATE INDEX IF NOT EXISTS leads_status_idx ON leads(status); CREATE INDEX IF NOT EXISTS leads_source_idx ON leads(source); CREATE INDEX IF NOT EXISTS leads_created_at_idx ON leads(created_at DESC); -- Auto-update timestamp CREATE OR REPLACE FUNCTION update_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER leads_updated_at BEFORE UPDATE ON leads FOR EACH ROW EXECUTE FUNCTION update_updated_at(); -- Row Level Security ALTER TABLE leads ENABLE ROW LEVEL SECURITY;
β‘‘
Tabel: purchases β€” Untuk Tracking Transaksi
-- Tabel transaksi dari payment gateway (Mayar.id, Xendit, dll) CREATE TABLE IF NOT EXISTS purchases ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, lead_id UUID REFERENCES leads(id) ON DELETE SET NULL, order_id TEXT UNIQUE NOT NULL, product TEXT NOT NULL, -- nama produk product_type TEXT, -- 'ebook' | 'bundle' | 'workbook' amount INTEGER NOT NULL, -- dalam Rupiah (tanpa desimal) currency TEXT DEFAULT 'IDR', status TEXT DEFAULT 'pending', -- 'pending' | 'paid' | 'refund' | 'failed' gateway TEXT, -- 'mayar' | 'xendit' | 'midtrans' gateway_ref TEXT, -- ID transaksi dari gateway paid_at TIMESTAMPTZ, buyer_email TEXT, buyer_name TEXT, buyer_phone TEXT, fbc TEXT, fbp TEXT, utm_source TEXT, utm_campaign TEXT, metadata JSONB DEFAULT '{}', -- data tambahan dari webhook created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS purchases_lead_id_idx ON purchases(lead_id); CREATE INDEX IF NOT EXISTS purchases_status_idx ON purchases(status); CREATE INDEX IF NOT EXISTS purchases_order_id_idx ON purchases(order_id); ALTER TABLE purchases ENABLE ROW LEVEL SECURITY;
β‘’
Tabel: quiz_results β€” Untuk Modul Quiz
-- Simpan hasil quiz per user CREATE TABLE IF NOT EXISTS quiz_results ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, lead_id UUID REFERENCES leads(id) ON DELETE CASCADE, email TEXT, quiz_name TEXT NOT NULL, -- nama quiz (misal: 'tes-jalur-penghasilan') result_type TEXT, -- tipe hasil: 'arsitek' | 'mercusuar' | 'navigator' | 'patron' scores JSONB DEFAULT '{}', -- { arsitek: 12, mercusuar: 8, navigator: 6, patron: 10 } answers JSONB DEFAULT '[]', -- array jawaban per pertanyaan completed_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS quiz_lead_idx ON quiz_results(lead_id); CREATE INDEX IF NOT EXISTS quiz_type_idx ON quiz_results(result_type); ALTER TABLE quiz_results ENABLE ROW LEVEL SECURITY;
β‘£
Tabel: ebook_downloads β€” Untuk Delivery eBook
-- Tracking unduhan ebook (lead magnet & produk berbayar) CREATE TABLE IF NOT EXISTS ebook_downloads ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, lead_id UUID REFERENCES leads(id) ON DELETE SET NULL, email TEXT NOT NULL, ebook_name TEXT NOT NULL, -- 'lead-magnet' | 'produk-utama' | 'tutorial' is_paid BOOLEAN DEFAULT FALSE, -- false = lead magnet, true = produk berbayar purchase_id UUID REFERENCES purchases(id) ON DELETE SET NULL, download_token TEXT UNIQUE, -- token unik untuk link download (expire setelah X jam) token_expires_at TIMESTAMPTZ, download_count INTEGER DEFAULT 0, last_downloaded_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE ebook_downloads ENABLE ROW LEVEL SECURITY;
β‘€
Tabel: email_log β€” Untuk Email Marketing
-- Log email yang dikirim (untuk audit & de-duplicate) CREATE TABLE IF NOT EXISTS email_log ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, lead_id UUID REFERENCES leads(id) ON DELETE SET NULL, email TEXT NOT NULL, template TEXT NOT NULL, -- 'welcome' | 'nurture_d1' | 'nurture_d3' | 'purchase_confirm' subject TEXT, status TEXT DEFAULT 'sent', -- 'sent' | 'failed' | 'bounced' provider TEXT DEFAULT 'brevo', sent_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS email_log_lead_idx ON email_log(lead_id); CREATE INDEX IF NOT EXISTS email_log_template_idx ON email_log(template);
β‘₯
RLS Policies β€” Akses yang Aman
Policy ini memungkinkan edge functions (pakai service_role) menulis ke tabel, tapi frontend (pakai anon) tidak bisa baca data sensitif.
-- Izinkan service_role (edge functions) melakukan semua operasi -- Frontend hanya bisa INSERT (bukan SELECT data orang lain) -- leads: frontend bisa insert, tapi tidak bisa baca semua rows CREATE POLICY "leads_insert" ON leads FOR INSERT WITH CHECK (true); -- Untuk baca data sendiri (by email) β€” opsional jika ada auth -- CREATE POLICY "leads_select_own" ON leads FOR SELECT -- USING (email = auth.jwt() ->> 'email'); -- purchases: hanya service_role yang bisa insert (via webhook) -- quiz_results: service_role insert, user bisa baca hasil sendiri CREATE POLICY "quiz_insert" ON quiz_results FOR INSERT WITH CHECK (true); -- ebook_downloads: frontend bisa insert (untuk tracking), baca via token CREATE POLICY "ebook_dl_insert" ON ebook_downloads FOR INSERT WITH CHECK (true);
☁️ Tabel di bawah ini khusus untuk fitur Simpan/Muat ke Cloud di tiap 18 halaman modul blueprint (bukan untuk website produksi). Setiap modul dapat 1 tabel sendiri, struktur generik: id, label, data JSONB, timestamps.
β‘₯
18 Tabel bp_* β€” Cloud Sync per Modul Blueprint
create extension if not exists "pgcrypto"; create or replace function bp_update_updated_at() returns trigger language plpgsql as $$ begin new.updated_at = now(); return new; end; $$; -- Template generik untuk 18 tabel modul blueprint: -- bp_content_calendar, bp_workbook, bp_hooks_copywriting, bp_followup_wa, -- bp_roadmap, bp_rencana_pemasaran, bp_quiz, bp_pseo_planner, bp_paid_ads, -- bp_landing_page, bp_email_marketing, bp_ebook_builder, bp_ebook_tutorial, -- bp_crm, bp_cold_wa, bp_ebook_produk, bp_ebook_lead_magnet, bp_kpi_tracker -- -- create table if not exists bp_NAMA_TABEL ( -- id uuid primary key default gen_random_uuid(), -- label text, -- data jsonb not null default '{}', -- created_at timestamptz default now(), -- updated_at timestamptz default now() -- ); -- -- Lihat file lengkap: blueprint/assets/blueprint_tables.sql -- (berisi CREATE TABLE + trigger + RLS untuk ke-18 tabel sekaligus, -- tinggal copy-paste seluruh isi file itu ke SQL Editor Supabase)
πŸ“„ File lengkap siap-jalan: blueprint/assets/blueprint_tables.sql
⚑ Edge Functions β€” Semua Modul
πŸ’‘ Edge Functions berjalan di server Supabase (Deno runtime). Aman untuk simpan API keys dan memanggil layanan eksternal seperti Brevo, Meta CAPI, Mayar.id, dll.
1
save-lead β€” Landing Page & Lead Magnet
Dipanggil saat user submit form di landing page atau unduh lead magnet.
// supabase/functions/save-lead/index.ts import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', } serve(async (req) => { if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders }) try { const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! ) const body = await req.json() const { email, name, phone, source, fbc, fbp, utm_source, utm_medium, utm_campaign } = body // Simpan atau update lead const { data, error } = await supabase .from('leads') .upsert({ email, name, phone, source, fbc, fbp, utm_source, utm_medium, utm_campaign }, { onConflict: 'email', ignoreDuplicates: false }) .select().single() if (error) throw error // [Opsional] Daftarkan ke Brevo // const brevoKey = Deno.env.get('BREVO_API_KEY') // await fetch('https://api.brevo.com/v3/contacts', { method: 'POST', ... }) // [Opsional] Kirim Meta CAPI Lead event // await sendMetaCAPI('Lead', email, fbc, fbp) return new Response(JSON.stringify({ ok: true, id: data.id }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } catch(e) { return new Response(JSON.stringify({ error: e.message }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } })
2
payment-webhook β€” Untuk Mayar.id / Xendit / Midtrans
Dipanggil otomatis oleh payment gateway saat ada transaksi masuk.
// supabase/functions/payment-webhook/index.ts import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' serve(async (req) => { const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! ) const payload = await req.json() // Sesuaikan dengan format webhook gateway kamu // Contoh untuk Mayar.id: const { order_id, status, amount, buyer_email, buyer_name } = payload if (status === 'PAID' || status === 'paid') { // Update atau insert purchase await supabase.from('purchases').upsert({ order_id, status: 'paid', amount: parseInt(amount), buyer_email, buyer_name, paid_at: new Date().toISOString() }, { onConflict: 'order_id' }) // Update lead status await supabase.from('leads') .update({ status: 'buyer' }) .eq('email', buyer_email) // Kirim email konfirmasi + link download // await generateDownloadToken(buyer_email, order_id) // Kirim Meta CAPI Purchase event // await sendMetaCAPI('Purchase', buyer_email, fbc, amount) } return new Response(JSON.stringify({ received: true }), { headers: { 'Content-Type': 'application/json' } }) })
3
quiz-result β€” Untuk Modul Quiz
Dipanggil setelah user menyelesaikan quiz dan submit email untuk lihat hasil.
// supabase/functions/quiz-result/index.ts import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' serve(async (req) => { if (req.method === 'OPTIONS') return new Response('ok', { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*' } }) const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! ) const { email, name, quiz_name, result_type, scores, answers } = await req.json() // Simpan atau update lead const { data: lead } = await supabase .from('leads').upsert({ email, name, source: 'quiz', tags: ['quiz_taker'] }, { onConflict: 'email' }).select().single() // Simpan hasil quiz await supabase.from('quiz_results').insert({ lead_id: lead?.id, email, quiz_name, result_type, scores, answers }) // Kirim email dengan hasil quiz (via Brevo template) // await sendQuizResultEmail(email, name, result_type) return new Response(JSON.stringify({ ok: true, result_type }), { headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' } }) })
4
generate-download β€” Untuk Delivery eBook
// supabase/functions/generate-download/index.ts import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' serve(async (req) => { const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! ) const { token } = await req.json() // Verifikasi token const { data: dl } = await supabase .from('ebook_downloads') .select('*').eq('download_token', token) .gt('token_expires_at', new Date().toISOString()) .single() if (!dl) return new Response(JSON.stringify({ error: 'Link expired atau tidak valid' }), { status: 403, headers: { 'Content-Type': 'application/json' } }) // Increment counter await supabase.from('ebook_downloads') .update({ download_count: dl.download_count + 1, last_downloaded_at: new Date().toISOString() }) .eq('id', dl.id) // Generate signed URL dari Supabase Storage const { data: signedUrl } = await supabase.storage .from('ebooks') .createSignedUrl(`${dl.ebook_name}.pdf`, 300) // expire 5 menit return new Response(JSON.stringify({ url: signedUrl?.signedUrl }), { headers: { 'Content-Type': 'application/json' } }) })
5
Deploy Semua Edge Functions
# Deploy semua functions sekaligus supabase functions deploy --no-verify-jwt # Set secrets (WAJIB sebelum deploy ke production) supabase secrets set META_ACCESS_TOKEN=xxx supabase secrets set BREVO_API_KEY=xxx supabase secrets set MAYAR_SECRET_KEY=xxx # Cek semua secrets yang sudah diset supabase secrets list # URL endpoint setelah deploy: # https://[PROJECT_REF].supabase.co/functions/v1/save-lead # https://[PROJECT_REF].supabase.co/functions/v1/payment-webhook # https://[PROJECT_REF].supabase.co/functions/v1/quiz-result # https://[PROJECT_REF].supabase.co/functions/v1/generate-download
πŸ“ Storage β€” Delivery File eBook
⚑ Supabase Storage untuk simpan file PDF ebook. File tidak bisa diakses langsung β€” harus via signed URL yang expire dalam beberapa menit. Aman dari download ilegal.
1
Buat Bucket Storage
Dashboard Supabase β†’ Storage β†’ New Bucket
-- Atau via SQL Editor: INSERT INTO storage.buckets (id, name, public) VALUES ('ebooks', 'ebooks', false); -- false = private, harus pakai signed URL -- Bucket 'ebooks' untuk: -- ebooks/lead-magnet.pdf ← ebook gratis (lead magnet) -- ebooks/produk-utama.pdf ← ebook berbayar -- ebooks/tutorial.pdf ← ebook tutorial -- ebooks/workbook.pdf ← workbook PDF
2
Storage RLS Policies
-- Hanya service_role (edge functions) yang bisa akses bucket 'ebooks' -- User tidak bisa download langsung tanpa signed URL CREATE POLICY "ebooks_service_only" ON storage.objects FOR SELECT USING ( bucket_id = 'ebooks' AND auth.role() = 'service_role' ); -- Upload hanya bisa via dashboard atau CLI CREATE POLICY "ebooks_upload_auth" ON storage.objects FOR INSERT WITH CHECK ( bucket_id = 'ebooks' AND auth.role() = 'service_role' );
3
Upload File via CLI
-- Via Supabase Dashboard: Storage β†’ Bucket 'ebooks' β†’ Upload Files -- Via CLI (belum stabil, lebih baik pakai dashboard) -- supabase storage cp ./ebook.pdf ss:///ebooks/produk-utama.pdf -- Cara alternatif: upload via script Node.js import { createClient } from '@supabase/supabase-js' import fs from 'fs' const supabase = createClient(SUPABASE_URL, SERVICE_ROLE_KEY) const file = fs.readFileSync('./produk-utama.pdf') const { error } = await supabase.storage .from('ebooks') .upload('produk-utama.pdf', file, { contentType: 'application/pdf', upsert: true }) if (!error) console.log('Upload berhasil!')
πŸ”— Integrasi Supabase per Modul Blueprint
Setiap modul blueprint bisa membaca config Supabase dari localStorage.getItem('bp_supabase') yang kamu simpan di halaman ini. Sekarang ke-18 modul di bawah sudah terhubung langsung β€” tiap halaman punya badge status koneksi + tombol ⬆️ Simpan Cloud / ⬇️ Muat Cloud di topbar, tersambung ke tabelnya masing-masing.
Terhubung Terhubung Terhubung Terhubung Terhubung Terhubung Terhubung Terhubung Terhubung Terhubung Terhubung Terhubung Terhubung Terhubung Terhubung Terhubung Terhubung Terhubung
πŸ“‹ Cara Kerja Cloud Sync di Tiap Modul (assets/supabase-connector.js)
Setiap modul cukup include 1 script dan panggil BPCloud.initCloudSync(table, DATA_KEY) β€” badge status & tombol Simpan/Muat otomatis aktif.
<!-- 1. Tambahkan di <head> --> <script src="assets/supabase-connector.js"></script> <!-- 2. Tambahkan di topbar --> <span class="cloud-badge" id="cloud-badge">☁️ ...</span> <button class="btn-cloud" onclick="doCloudSave()">⬆️ Simpan Cloud</button> <button class="btn-cloud" onclick="doCloudLoad()">⬇️ Muat Cloud</button> // 3. Panggil di akhir <script> modul (setelah load()) BPCloud.initCloudSync('bp_nama_tabel', DATA_KEY) // DATA_KEY = localStorage key yang sudah dipakai modul tsb (mis. 'bp_crm') // Save akan meng-upsert seluruh isi localStorage[DATA_KEY] sebagai JSONB // ke kolom `data` di tabelnya. Load akan mengambil baris terakhir // (atau baris yang sama jika sudah pernah disimpan) dan replace localStorage, // lalu memanggil ulang function load() milik modul.
βœ… Checklist Setup Supabase