Blindy
Üniversite öğrencileri için anonim bir tanışma uygulaması. Yüz görmeden yedi dakika konuşuyorsunuz, isimler yalnızca karşılıklı evetle açılıyor.



Özet
- Sorun
- Telefonlar kararsız ağlarda istekleri yeniden gönderiyor, bağlantıları kopuyor; yine de bir sohbet mesajı ne kaybolmalı ne de iki kez kaydedilmeli. Eşleşme yalnızca akşam penceresinde çalışıyor ve iki işçi aynı kişiyi asla aynı anda eşleştirmemeli.
- Benim payım
- Tek geliştirici: Java ve Spring WebFlux backend, Flutter uygulaması, React yönetim paneli ve hepsinin çalıştığı sunucu.
- Sonuç ve durum
- Tekrar gönderilen mesaj bir kez kaydediliyor, yeniden bağlanan telefon kaçırdıklarını istiyor. Kapalı beta, git push ile güncellenen ve her gece yedeklenen tek bir kiralık sunucuda çalışıyor.
Beş ekranda bir akşam
Yanan mumdan karşılıklı evete kadar. Her ekran gerçek uygulama, yalnızca kişiler uydurma.
Ekran 1, toplam 5
Akşam
1Mum yandı
Akşam açık: kullanıcı masayı ve kalan süreyi görür, günün sorularını cevaplar.
Eşleşme
2Eşleşme bulundu
İki mum yanar, iki rumuz dağıtılır ve ortak ilgi alanları görünür.
Sohbet
31. tur: Cevaplar
İki tarafın cevapları kart olarak yan yana açılır, kullanıcı yorum yazar.
Karar
4Karar
Sohbet biter; kullanıcı 30 saniye içinde evet demek için butona basılı tutar.
5Açılış
İkisi de evet dedi: rumuzun yerini gerçek ad alır, kullanıcı ilk mesajı yazar.
Ürünün tüm akışı31 ekran
Kayıttan hesap silmeye kadar bütün ekranlar, kullanıcının karşılaştığı sırayla.
Ekran 1, toplam 31
Kayıt
1Hoş geldin
Kullanıcı mumu görür, bir akşamın nasıl işlediğini okur ve Google ile devam eder.
2Üniversite e-postası
Kullanıcı öğrenci olduğunu doğrulamak için .edu.tr adresini yazar.
3Doğrulama kodu
Kullanıcı üniversite e-postasına gelen 6 haneli kodu girer.
4Ad ve doğum tarihi
Kullanıcı adını ve doğum tarihini girer; ad karşılıklı evete kadar gizli kalır.
5Fotoğraflar
Kullanıcı anonim sohbette kimsenin görmediği en az iki fotoğraf ekler.
6İlgi alanları
Kullanıcı ilgi alanlarını seçer; en fazla üçü anonim sohbette ipucu olur.
Akşam
7İlk akşam
İlk girişte masadaki not bu akşam ne olacağını anlatır.
8Mum yandı
Akşam açık: kullanıcı masayı ve kalan süreyi görür, günün sorularını cevaplar.
9Bu akşam 21:00'de
Akşamdan önce mum sönüktür; kullanıcı geri sayımı görür, hatırlatma kurabilir.
10Özel gün masası
Bayram ve özel günlerde masa o güne göre süslenir.
11Günün soruları
Kullanıcı sohbette kart kart açılacak kısa günlük soruları cevaplar.
12Günün destesi
Günün destesi kısa bir törenle açılır, kartlar yelpaze olur.
Eşleşme
13Masa hazırlanıyor
Kullanıcı masaya oturur, karşısına oturacak kişi aranırken bekler.
14Mum yakılıyor
Kullanıcı sıraya girerken bir kibrit mumu yakar.
15Eşleşme bulundu
İki mum yanar, iki rumuz dağıtılır ve ortak ilgi alanları görünür.
Sohbet
161. tur: Cevaplar
İki tarafın cevapları kart olarak yan yana açılır, kullanıcı yorum yazar.
172. tur: Seçim
Kullanıcı gizlice bir şık seçer; aynı seçim kıvılcım çıkarır.
183. tur: Serbest
Serbest sohbet açılır; kullanıcı serbestçe yazar, elindeki kartı oynayabilir.
193 dakika daha
Süre biterken karşı taraf 3 dakika daha ister, kullanıcı kabul edebilir.
Karar
20Karar
Sohbet biter; kullanıcı 30 saniye içinde evet demek için butona basılı tutar.
21Karşı taraf bekleniyor
Kullanıcı evet dedi ve bekler; iki karar aynı anda görünür.
22Açılış
İkisi de evet dedi: rumuzun yerini gerçek ad alır, kullanıcı ilk mesajı yazar.
Sonrası
23Açık sohbet
Açılıştan sonra ikisi adlarıyla, süre sınırı olmadan konuşmaya devam eder.
24Bu sefer olmadı
Karşılıklı evet çıkmazsa kullanıcıya nazikçe söylenir, sonraki akşam yeniden oturabilir.
25Akşam zarfı
Kullanıcı mührü basılı tutar, mühür çatlayarak açılır.
26Zarfın içi
Zarftan yeni soru kartları ve bir kart arka yüzü çıkar.
27Koleksiyon
Kullanıcı topladığı kartlara, geçmiş akşamlarına ve rozetlerine bakar.
Hesabın
28Profil
Kullanıcı kendi profilini görür: hakkında, ilgi alanları ve masa süsleri.
29Ayarlar
Kullanıcı eşleşme tercihlerini, yaş aralığını ve bildirimleri ayarlar.
30Gizlilik ve hesap
Kullanıcı gizlilik metinlerini okur, rızasını yönetir ve hesap silmeyi bulur.
31Hesabı sil
Kullanıcı hesabını silmeyi onaylar; pencere neyin ne zaman silineceğini anlatır.
Nasıl çalışıyor
Backend yazanlar için okumaya değer kısımlar: sistemin yerleşimi, bir mesajın yolculuğu, iki kişinin nasıl eşleştiği ve kapalı betanın tek sunucuda nasıl çalıştığı.
Sisteme bir bakış
core-api, kimlikten moderasyona on dört modülden oluşan modüler bir monolit. chat-gateway yalnızca WebSocket bağlantılarını tutuyor: bağlanırken Firebase token'ını doğruluyor, Redis'i dinliyor ve veritabanına hiç dokunmuyor. Fotoğraflar imzalı URL'lerle doğrudan telefon ile Cloudflare R2 arasında gidip geliyor.
Sistem metin olarak
Her satır bir kutuyu ve oklarının gösterdiği kutuları sıralıyor.
- Flutter uygulaması (iOS ve Android) → Cloudflare (DNS, TLS, proxy), Cloudflare R2 (fotoğraf, yedek)
- Yönetim paneli (React, tarayıcıda) → Cloudflare
- Cloudflare → Caddy (tek giriş, dört alan adı)
- Caddy → core-api (modüler monolit, 14 modül), chat-gateway (yalnız soket, veritabanı yok), panel + site (statik dosyalar)
- core-api → PostgreSQL 15, Redis 7 (kuyruk, pub/sub, kilit), Cloudflare R2, Firebase, FCM, Resend (giriş, bildirim, e-posta)
- chat-gateway → Redis 7
- Flutter uygulamasıiOS ve AndroidYönetim paneliReact, tarayıcıda
- CloudflareDNS, TLS, proxy
- Caddytek giriş, dört alan adı
- core-apimodüler monolit, 14 modülchat-gatewayyalnız soket, veritabanı yokpanel + sitestatik dosyalar
- PostgreSQL 15Redis 7kuyruk, pub/sub, kilitCloudflare R2fotoğraf, yedekFirebase, FCM, Resendgiriş, bildirim, e-posta
Bir mesaj gönderdiğinizde ne oluyor
Adım adım kendiniz ilerlettiğiniz açıklayıcı bir model. Gerçek backend'e istek göndermiyor.
Yukarıdan ne olacağını seçin ya da sırayla ilerlemek için Sonraki'ye basın.
- Telefonunuz → core-api
POST /messages + clientMsgIdMesajı gönderiyorsunuz. Uygulama mesaja kendi kimliğini (clientMsgId) ekliyor, böylece tekrar deneme asla kopya oluşturmuyor. - core-api → Postgres
bir kez kaydetcore-api mesajı kaydediyor. Aynı kimlikle gelen ikinci istek, kayıtlı satırdan yanıtlanıyor. - core-api → Redis
publish user:{uid}Olay, bir sunucunun değil alıcının adını taşıyan Redis kanalına gidiyor. - Redis → Gateway
message.createdKarşı tarafın WebSocket'ini hangi gateway tutuyorsa o kanala abone ve olayı o alıyor. - Gateway → Karşı telefon
WebSocket üzerinden iletGateway mesajı karşı telefona iletiyor. Çevrimdışıysa bir Firebase bildirimi telefonu uyandırıyor. - Karşı telefon → core-api
kaçırılan mesajları GET ile alYeniden bağlanınca uygulama kaçırdıklarını core-api'den istiyor. Kalıcı kayıt için esas kaynak soket değil, REST.
İki kişi nasıl eşleşiyor
Eşleşme yalnızca akşam penceresinde çalışıyor. Kuyruk Redis'te duruyor; dağıtık bir kilit, iki işçinin aynı anda aynı kişiyi eşleştirememesini sağlıyor.
- Aynı iki kişi bir daha asla eşleşmiyor.
- Bir hayır, karşı tarafa hiçbir zaman gösterilmiyor.
Eşleşme
- Günde 5 soruherkese aynı sorular
- 3'ünü yanıtlakuyruğu açar
- Akşam kuyruğuRedis, yalnız pencere içinde
- Çift yönlü kontroliki tercih de uymalı
- İki kişiyi kilitledağıtık kilit
- match.foundiki telefona iletilir
Kapalı betayı çalıştırmak
Kiralık tek bir sunucu her şeyi Docker Compose ile çalıştırıyor. Kod sunucuya git push ile ulaşıyor; veritabanı her gece şifrelenip sunucudan çıkıyor.
- 80 ve 443 portları yalnızca Cloudflare'in IP aralıklarını kabul ediyor.
- Veri ağı iç ağ; veritabanına yalnızca API container'ları erişebiliyor.
- Servisler root olmayan bir kullanıcıyla çalışıyor; uygulamanın veritabanı rolü süper kullanıcı değil.
- Bir geri yükleme provasında yedek geri açılıp çalışan veritabanıyla karşılaştırıldı.
Dağıtım
- git push prodsunucudaki çıplak depoya
- post-receive kancasıyeni commit'i açar
- docker compose buildimajlar sunucuda derlenir
- Yeniden başlatCaddy arkasında
Gece yedeği
- pg_dumpher gece 04:30'da
- Şifreleage ile
- Cloudflare R2sunucu dışında
- 14 gün saklasonra dönüşümlü
Rakamlarla
- 962
- backend testi
- 3.200+
- Flutter testi, 256'sı golden görüntü
- 237
- yönetim paneli testi
- 77
- OpenAPI sözleşmesinde API yolu
- 13
- WebSocket olay türü
- 21
- veritabanı migration'ı
Yine aynı şekilde vereceğim kararlar
Esas kaynak REST, soket değil
Soket yalnızca kapı zilini çalıyor; gönderilenlerin kaydını o tutmuyor. Bedeli, her yeniden bağlanmada uygulamanın kaçırdıklarını core-api'ye sorduğu bir istek daha. Karşılığında kopan bağlantı hiçbir şey kaybettirmiyor.
Tekrar denemek tasarım gereği güvenli
Her mesaj bir istemci kimliği taşıyor. Kararsız bir ağ onu iki kez gönderebilir, veritabanı bir kez kaydeder.
Gizlilik ürünün parçası
KVKK kapsamında açık rıza, her durumdan hesap silme ve yöneticiler yalnızca şikâyet edilen sohbetleri okuyabiliyor.

