VIC — Valora Intelligence Center
WhatsApp üzerinden çalışan, danışman-öncelikli gayrimenkul CRM'i. Müşteri chatbot'u değil — danışmanın cebindeki ofis.
01 Genel Bakış
Emlak danışmanları bilgisayar başına geçmeden tüm iş süreçlerini WhatsApp'tan halletsin: portföy ekle/güncelle, talep eşleştir, gösterim ayarla, teklif/pazarlık yönet, komisyon hesapla.
Tasarım Felsefesi
02 Teknoloji Stack
WhatsApp Cloud API
Business Solution Provider, system user token (kalıcı). Webhook → Edge Function. Phone Number ID + WABA ID üzerinden tüm template + freeform mesaj akışı.
Supabase (Postgres + Edge)
10 Edge Function (Deno runtime), 21 tablo, RLS aktif, Storage bucket (portfolio-media, documents), Vault (secrets), pg_cron + pg_net extensions.
Vertex AI fine-tuned Gemini
Gemini 2.5 Flash 5 round eğitildi (R5: 482 train + 54 val + 237 DPO pair). OAuth refresh token + token cache. Birincil model.
Gemini 2.5 Flash (fallback)
Vertex 5xx/timeout durumunda devreye girer. Aynı sistem prompt, daha agresif post-processing.
pg_cron + pg_net
Postgres içinden HTTP POST ile Edge Function tetikleme. 4 farklı cron job (1dk, 1dk, 15dk, haftalık).
GCP OAuth + Vault
Refresh token ile Vertex erişimi. Service role key Vault'ta. pg_cron job'ları içinden çağrıda secret çekiliyor.
Hesap Haritası
03 Mimari — Data Flow
Webhook ultra-hızlı row INSERT eder, gerçek iş cron-driven batch processor'da olur. Tüm fonksiyonlar service role key ile DB'ye yazar.
363 LOC] PM[process-messages
1000 LOC] VR[vic-respond
2348 LOC
Vertex + Flash] PFM[portfolio-manage
874 LOC] DM[deal-manage
1933 LOC] MN[match-notify
674 LOC] PC[proactive-check
756 LOC] SE[session-extract
477 LOC] SM[send-message
340 LOC] AWT[admin-wa-templates
417 LOC] DB[(Postgres
21 tables)] CRON{{pg_cron
1dk · 1dk · 15dk · weekly}} WA -->|inbound webhook| WH WH -->|INSERT row| DB WH -.->|200 OK <1s| WA CRON -->|1dk| PM CRON -->|1dk| SE CRON -->|15dk| PC CRON -->|weekly Sat| PC PM -->|batch per contact| VR PM -->|portfolio intent| PFM PM -->|deal intent| DM PM -->|interactive_button| DM PM -->|interactive_button| PFM DB -.->|trigger: portfolio/request new| MN DB -.->|trigger: deal status change| DM DB -.->|trigger: portfolio deactivate| DM VR --> SM PFM --> SM DM --> SM MN --> SM PC --> SM SM -->|outbound API call| WA SM -->|INSERT outbound| DB AWT -.->|external admin| WA classDef func fill:#1c2128,stroke:#58a6ff,color:#e6edf3 classDef store fill:#1c2128,stroke:#bc8cff,color:#e6edf3 classDef ext fill:#1c2128,stroke:#3fb950,color:#e6edf3 class WH,PM,VR,PFM,DM,MN,PC,SE,SM,AWT func class DB,CRON store class WA ext
Async Batch Pattern (kritik)
Naif yaklaşım: her webhook'ta LLM çağır → kullanıcı 5 mesaj atınca 5 cevap döner. VIC'in çözümü:
processed_at pipeline metadata değil — claim column.
UPDATE ... WHERE claimed_at IS NULL RETURNING race condition'sız batch effect üretir.
Bu pattern her kuyruk akışında tekrarlanır (notification nudge dedupe, session extraction, vs).
04 ERD — Entity Relationship Diagram
21 tablo, ~38 foreign key constraint. Live Supabase schema'dan üretildi. Tam çözünürlük + pan/zoom için yeni sekmede aç.
Mantıksal Gruplar
👤 Identity
contacts agent_profiles interaction_tracker
Kullanıcı kimliği + agent yetki hiyerarşisi + WhatsApp 24h window state.
🏢 Portföy
portfolios portfolio_confidential portfolio_media portfolio_history
İlan ana tablosu + gizli alanlar (dip fiyat, sahip) + medya + audit log.
🎯 Talep & Eşleştirme
requests portfolio_request_matches market_signals
Müşteri talepleri + deterministic SQL match sonuçları + boşa düşen sorgular.
🤝 Deal Pipeline
deals deal_confirmations deal_negotiations showings
State machine + dual confirmation + ping-pong pazarlık + gösterim akışı.
💰 Komisyon
platform_commissions agent_visibility
Per-party komisyon split + deal kapanınca agent görünürlük artırımı.
💬 Conversation
message_log sessions pending_notifications
Tüm mesaj kaydı + 90sn-grouped session + 24h window queue.
📋 Operations
tasks documents
VIC'in kendine çıkardığı task'lar + sözleşme/sertifika dosyaları.
🔐 Onboarding
contacts kolonları: onboarding_status, terms_accepted_at, consent_transfer, consent_training, consent_marketing
KVKK Md. 9 açık rıza state machine. Cross-border (Gemini abroad) zorunlu, training/marketing opsiyonel.
05 Tablolar — Tam Schema
Her tablo card'ına tıkla, kolon detayları açılır. Live Supabase'den çekildi (****).
06 Edge Functions
Hepsi Deno runtime, hepsi service role key ile DB'ye yazar. verify_jwt internal olanlarda kapalı (production scar #1, bkz. §11).
whatsapp-webhook
WhatsApp Cloud API webhook endpoint. INSERT message_log + upsert contacts (BSUID → phone fallback). 200 OK in <1s zorunlu (Meta retry policy).
- Status update handling (delivered/read)
- Reaction filter (LLM'e gönderilmez)
- Voice → media_url, transcript flow'a girer
process-messages
Async batch processor. Pending mesajları atomic claim, per-contact batch, intent router.
- 5sn batch window
- Interactive button dispatch (stateful)
- Slash command routing (/davet, /yardim, vs.)
- Portfolio / deal / conversation flow ayrıştırması
vic-respond
LLM yanıt motoru. Vertex AI fine-tuned (birincil) + Gemini 2.5 Flash (fallback). 3 mod: agent / customer / unknown.
- FIRST vs ONGOING prompt ayrımı (caching dostu)
- İngilizce system prompt (tokenizer optimal)
- Portfolio context injection
- Truncation guard (cümle sonuna keser)
- Post-processing: intro strip, isim seyrekleştirme
portfolio-manage
Portföy CRUD: create / update / deactivate / reactivate / list / detail / summary. Gemini ile structured data extraction.
- Insan dostu ID:
2026-001(SQL function) - CREATE: full listing data extraction
- UPDATE: diff extraction
- portfolio_history audit log her CRUD'da
- portfolio_confidential broker+ modda yazılabilir
deal-manage
Deal state machine: matched → showing_scheduled → shown → offer → closed_won/lost. Ping-pong pazarlık. Cross-agent notification.
- handleSetOffer (her teklif counterpart'a iletilir)
- handleStatusChanged trigger
- handleShowingRequest / Confirm / Reschedule
- parseDateTimeText (gün adı, dönem, buçuk, voice)
- Duplicate showing guard
match-notify
Yeni portföy / talep → SQL match → bildirim. DB trigger'dan tetiklenir.
- find_matching_portfolios RPC
- find_matching_requests RPC
- property_type hard filter (F7 fix)
- 24h window dışıysa pending_notifications kuyruğu
proactive-check
5 nudge sinyali + haftalık broker raporu. Her sinyal kendine ait RPC.
- 3-day stale followup
- Showing reminder customer (T-2h)
- Showing reminder agent (T-1h)
- Unactioned matches (24h+)
- Stale deal check (7d+)
session-extract
90sn inaktif session'ı kapatır, Gemini Flash ile intent + entities çıkartır.
- close_stale_sessions() SQL function
- sessions → top-level indexed (intent, district, max_price)
- Yeni request INSERT (criteria JSONB)
- market_signals (boşa düşen sorgu)
send-message
Tek outbound noktası. WA Cloud API çağrısı + INSERT outbound row.
- 24h window kontrolü
- Template fallback (window kapalıysa)
- Interactive buttons / lists / CTA
- Body sanitization (\n, \t guard)
admin-wa-templates
Template yönetimi: create / list / delete. Meta WhatsApp Graph API wrapper. Admin-only.
- conversational_automation config (welcome msg + ice breakers + commands)
- Template body validation (newline/tab guard)
- allow_category_change: false
07 Cron Jobs & Triggers
Postgres içinden pg_cron + pg_net ile HTTP POST. Authentication için vault.decrypted_secrets'tan service_role_key çekilir.
Scheduled Jobs
-- F4b: Proactive checks
SELECT cron.schedule('proactive-check', '*/15 * * * *',
$$ SELECT trigger_proactive_check('regular'); $$
);
-- Weekly broker report (Saturday 10:00 TR)
SELECT cron.schedule('weekly-report', '0 7 * * 6',
$$ SELECT trigger_proactive_check('weekly_report'); $$
);
-- F7b: Session extraction (90sn idle threshold)
SELECT cron.schedule('extract-closed-sessions', '*/1 * * * *',
$$ SELECT trigger_session_extraction(); $$
);
-- F2: Process inbound messages
SELECT cron.schedule('process-messages', '*/1 * * * *',
$$ SELECT trigger_process_messages(); $$
);
DB Triggers
-- Yeni / reactive portföy → match-notify
CREATE TRIGGER trg_match_on_portfolio
AFTER INSERT OR UPDATE ON portfolios
FOR EACH ROW EXECUTE FUNCTION trigger_match_portfolio();
-- Yeni talep → match-notify
CREATE TRIGGER trg_match_on_request
AFTER INSERT ON requests
FOR EACH ROW EXECUTE FUNCTION trigger_match_request();
-- Deal status değişti → deal-manage notification
CREATE TRIGGER trg_deal_status_change
BEFORE UPDATE ON deals
FOR EACH ROW EXECUTE FUNCTION trigger_deal_status_change();
-- Portföy deaktive ediliyor + aktif deal var → deal-manage uyarısı
CREATE TRIGGER trg_portfolio_deal_check
BEFORE UPDATE ON portfolios
FOR EACH ROW EXECUTE FUNCTION trigger_portfolio_deal_check();
SECURITY DEFINER RPC'ler
find_matching_portfolios(district, ptype, type, rooms, min, max, currency)— talep → ilanfind_matching_requests(district, ptype, listing_type, rooms, price, currency)— ilan → talepfind_stale_followups()— 3 gün cevapsız müşterifind_showing_reminders_customer/agent()— T-2h / T-1hfind_unactioned_matches()— 24h+ aksiyon alınmamış matchfind_stale_deals()— 7 gün inaktif aktif dealfind_unconfirmed_deals()— 48h+ confirmation bekleyenclose_stale_sessions()— 90sn idle session kapatırgenerate_portfolio_id()—2026-001formatı
08 LLM Mimarisi
İki katmanlı model — fine-tuned Vertex AI birincil, base Flash fallback. Prompt İngilizce, code-level enforcement zorunlu.
İki Katmanlı LLM
Fine-Tuning Round'ları
R1 + R2
Prompt tuning baseline. SFT 209 konuşma, 1426 turn.
R3
261 train + 29 val. Identity leak fix, thinking token sızıntısı, refusal nuance.
R4
278 train + 31 val. Gerçek WA konuşmalarından düzeltmeler. Agent portfolio access. tunedModelDisplayName API değişikliği.
R5 (production)
482 train + 54 val. Claude+ChatGPT+Gemini sentetik. Anti-fabrication, persona çeşitleme, çoklu dil, 237 DPO pair (henüz SFT-only).
Prompt Yapısı
- İngilizce system prompt — tokenizer + IF performansı için optimal
- FIRST vs ONGOING ayrı prompt — LLM "Merhaba ben VIC" demesin diye kod-seviyesi tanıtma kontrolü
- Implicit caching uyumlu — sabit kısım önce, dynamic context sonra
- 3 mod:
agent— full portföy + komisyon + diğer agent'ların ilanlarıcustomer— filtered, qualifying flowunknown— onboarding / KVKK flow
- Truncation guard — sadece MAX_TOKENS finish reason'da tetiklenir, son
.!?karakterine keser - Refusal kuralları — legal/tax + competitor → kod reddi; genel şirket bilgisi → LLM cevap
- Refusal nuance — kişiye özel bilgi → danışmana yönlendir; genel kamu bilgisi → VIC cevap verebilir
Code-Level Enforcement (LLM'e güvenmek yasak)
- Post-processing: intro strip ("Merhaba!" temizliği), filler temizliği, isim seyrekleştirme
- Reaction filter: emoji-only reaction mesajları LLM'e gitmez
- Interactive button routing: button click'ler regex match → dedicated handler
- Telefon numarası uydurma engeli (regex check)
- Duplicate showing guard
09 İş Akışları
İki örnek: portföy ekleme (LLM extraction) ve cross-agent deal pazarlığı (state machine).
Akış 1 — Portföy Ekleme (Agent)
(processed_at=NULL) WH-->>A: 200 OK Note over PM: pg_cron (1dk later) PM->>DB: atomic claim PM->>PM: detectPortfolioIntent() ✓ PM->>PFM: route(action: create) PFM->>G: extract listing JSON G-->>PFM: {type, district, rooms, price...} PFM->>DB: INSERT portfolios
+ portfolio_history PFM->>DB: portfolio_id = "2026-021" Note over DB: TRIGGER fires DB->>DB: trg_match_on_portfolio DB->>+match-notify: HTTP POST (pg_net) PFM->>send-message: "Portföy eklendi: 2026-021" send-message->>A: WA reply Note over match-notify: bg
find_matching_requests
→ notify matching agents
Akış 2 — Cross-Agent Deal Pazarlığı (Ping-Pong)
(amount: 12M, status: pending) DM->>SM: notify listing_agent SM->>LA: [Kabul] [Reddet] [Karşı Teklif] LA->>DM: tap [Karşı Teklif]
"13M" DM->>DB: deal_negotiations
(amount: 13M, status: countered) DM->>SM: notify buying_agent SM->>BA: [Kabul] [Reddet] [Karşı Teklif] BA->>DM: tap [Kabul] DM->>DB: UPDATE deals
status: closed_won Note over DB: TRIGGER fires DB->>DM: trg_deal_status_change DM->>DB: INSERT platform_commissions DM->>DB: INSERT agent_visibility
(unlock full info) DM->>SM: notify both agents SM->>BA: "Teklif kabul edildi" SM->>LA: "Anlaşma kapandı"
Akış 3 — 24h Window Closed: Nudge Pattern
\n / \t kabul etmiyor. Çok satırlı match notification'lar template ile gönderilemez.
Çözüm: rich body pending_notifications kuyruğa, single-line nudge template gönder, kullanıcı cevap verince 24h window açılır, kuyruk drain edilir.
10 Yetki Modeli — 5 Katmanlı RLS
Identity (kim olduğun) ve authority (ne yapabildiğin) ayrı tablolarda. F11 migration ile schema temizlendi.
Identity vs Authority Ayrımı (F11)
-- Before: çakışan değerler iki tabloda
contacts.contact_type IN (customer, agent, manager, broker, owner, ...)
agent_profiles.role IN (agent, senior_agent, broker, admin, owner)
-- Permission check'ler ambiguous, kod identity ile authority'yi conflate ediyordu
-- After: tek source of truth per concept
contacts.contact_type IN (customer, agent, external_agent, lead, unknown)
-- Identity / category
agent_profiles.role IN (agent, senior_agent, broker, admin, owner)
-- Hierarchy within agent network
5 Katmanlı Görünürlük
1. Herkese Açık
Fotoğraf, m², ilan başlığı. Public website + portföy paylaşımı.
2. Müşteri
+ Adres yaklaşımı, oda planı, semt. Onaylı müşteri.
3. Valora-İçi (Agent)
+ Tam adres, müsait saatler, sahip iletişimi (filtered).
4. Sahip-Özel
+ Sahip notları, özel talimatlar. portfolio_confidential.
5. Broker / Admin / Owner
+ Dip fiyat, komisyon, gerçek pazarlık marjı, tüm finansal veri.
mode = customer | agent sadece. resolveAgentTier(contact) ayrı bir adım, prompt variant ve görünürlük seviyesini belirler. Mode ile tier ayrı.
11 Production Scar'lar
Live'da yanan ve fix'lenen, dokümante edilmiş hatalar. Her biri tekrarlama riskine karşı kodla + memory ile kilitli.
#1 — Supabase verify_jwt + yeni sb_secret format çatışması
Supabase yeni service role key formatını sb_secret_... (JWT değil, ~41 char) yaptı. CLI default --verify-jwt ON deploy ediyor. Internal function-to-function çağrıları "Invalid JWT" (HTTP 401) ile silently break oluyordu — process-messages → vic-respond, vic-respond → send-message, hepsi koptu.
Fix: config.toml içinde her internal function için [functions.X] verify_jwt = false. admin-wa-templates verified kalır (extra layer, dış JWT-format key ile invoke ediliyor). Probe: Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")?.startsWith("sb_secret_").
#2 — WhatsApp template body newline / tab guard
(#100) Param text cannot have new-line/tab characters or more than 4 consecutive spaces runtime error. Çok satırlı match/proactive bildirimler template ile gönderilemez (24h window kapalı kullanıcılar için kritik).
Fix: "Nudge template + freeform-on-reply" pattern. Rich body pending_notifications kuyruğa, single-line nudge template gönder, kullanıcı cevap verince vic-respond kuyruğu drain eder. Atomic claim UPDATE interaction_tracker SET nudge_sent_at = now() WHERE contact_id = $1 AND nudge_sent_at IS NULL RETURNING dedupe.
#3 — Inbound message_type routing footgun
"Text dışında her şey fallback" early-return blanket-block ediyordu interactive_button + interactive_list tiplerini → buton tıklamaları çalışmıyordu (onboarding role selection, KVKK consent, deal action). Belirti: button taps "Mesajınızı aldım" generic reply, DB state değişmez. Grandfathered kullanıcılarda fark edilmiyordu çünkü onboarding tamamlanmıştı.
Fix: Whitelist gerçek media tiplerine (image | audio | video | document | location), interactive_* flow through handler chain'e.
#4 — Identity vs authority schema collision (F11)
İki tabloda aynı role değerleri (broker, owner) permission check'leri ambiguous yapıyordu. contact_type = 'broker' mı agent_profiles.role = 'broker' mı? Kod conflate ediyordu, tier-based prompt seçimi yanlış sonuçlar üretiyordu.
Fix: F11 migration — contacts.contact_type sadece identity (customer/agent/external_agent/lead/unknown), agent_profiles.role sadece hiyerarşi. PostgREST embed (contact, agent_profiles(role)) ile tier ayrı resolve edilir.
#5 — BSUID'yi PK olarak kullanma
WhatsApp Business Solution UID numara değişince yenileniyor (kullanıcı SIM değiştirirse). 30 gün kuralı telefon görünürlüğünü etkiler, BSUID'yi değil. WABA portfolio transfer = potansiyel BSUID reset riski.
Fix: UUID PK + BSUID secondary unique index. Lookup chain: BSUID → phone fallback. Haziran 2026'da BSUID ile mesaj gönderme aktif olacak ama PK olmaya hâlâ uygun değil.
#6 — Matcher property_type hard filter eksikti (F7)
find_matching_portfolios / find_matching_requests RPC'lerinde property_type sadece scoring'deydi, WHERE'de değildi. Cross-category match leaked through — daire talebi dükkân ilanıyla match'liyordu (district aligned olunca). Yanlış agent bildirimleri.
Fix: F7 migration — property_type hard WHERE filter olarak iki RPC'ye eklendi. Mevcut yanlış 2 row pruned.
#7 — Session extraction cadence güvensizdi (F7b)
3 dakika idle threshold + 2 dakika cron = worst-case agent visibility latency ~5 dakika. Event-driven trigger missed olursa (non-portfolio reply, heuristic edge case), session geç kapanıyor → request geç INSERT ediliyor → match geç tetikleniyor.
Fix: F7b migration — close_stale_sessions threshold 3dk → 90sn, cron 2dk → 1dk. Worst-case ~150sn.
#8 — Meta template name lifecycle locks
DELETE /{waba}/message_templates?name=X sonrasında Meta name+language pair'i saatlerce locked tutuyor ("Message template language is being deleted", subcode 2388023). Yeniden aynı isimle deploy edilemez.
Fix: Reuse etme — _v2 / _v3 bump. Body design kuralları: {{1}} tek başına olamaz (subcode 2388047), variable başta/sonda olamaz (subcode 2388299), allow_category_change: false zorunlu (Meta'nın UTILITY → MARKETING silently flip etmesini engeller — KVKK + per-message cost).
12 Roadmap — Yapılacaklar
Tamamlanan Faz'lar
F1-F5c DONE
Webhook + session-based intent extraction + portfolio CRUD + matching trigger + proactive checks + deal pipeline + ping-pong negotiation + cross-agent flow.
F6 Interactive + Commission DONE
Buttons, lists, CTA + deal_negotiations + platform_commissions + agent_visibility tabloları.
F7 / F7b DONE
Matcher property_type hard filter + session extraction cadence tightening.
F8 Onboarding + KVKK DONE
Onboarding state machine (pending_role → pending_terms → pending_transfer → completed). KVKK Md. 9 cross-border açık rıza zorunlu, training + marketing opsiyonel.
F10 Pending Notifications DONE
24h window queue + nudge dedupe pattern.
F11 Role Consolidation DONE
Identity vs authority schema ayrımı. CHECK constraint tightening.
F12 Conversational Automation DONE
Meta WhatsApp commands + ice breakers (slash command handler).
F13 /davet DONE
Admin WhatsApp'tan invite link generation (agent onboarding temeli).
F14 Role Authority DONE
contact_type identity + agent_profiles.role authority consolidation kodda aktif.
Bekleyen
F6/F7 Notification Detayı PLAN
Owner notification (teklif forward, kapanış) + customer notification (gösterim onayı, arama sonucu, teklif durumu) tam akışı.
F8 Commission Automation PLAN
Komisyon hesaplama + platform_commissions otomatik populate + invoice flow.
F9 Public Listings PLAN
valoragayrimenkul.com → Supabase → Next.js ISR SEO sayfaları. Public portföy görünümü.
F10 MCP Server PLAN
Can + VAEL'e direkt CRM erişimi (Cloud Run, Supabase Edge wrap).
Agent Onboarding Flow WIP
Admin WA'dan /davet **** → otomatik invite link → yeni agent self-onboarding. F13 ile temeli kuruldu, end-to-end flow eksik.