Berkan Türkoğlu
Projeler
Django · FastAPI · LangGraph

PaceUp

AI destekli koşu koçu (iOS) · kişisel proje

Repositorydurum: yayında

Problem

Kendim de yapay zekadan koşu programı istiyordum. Her seferinde yaşımı, kilomu, mevcut tempomu, haftada kaç gün müsait olduğumu baştan anlatmam gerekiyordu. Karşılığında aldığım şey de bir listeydi. Sorun da oradaydı: program sohbetin içinde metin olarak kalıyor, bir süre sonra geri dönüp bakmıyorsun ve kopuyorsun. PaceUp'ta profilini bir kez giriyorsun; chatbot seni zaten tanıdığı için sonrasında sadece hedefini söylüyorsun — “üç ay sonra yarı maraton koşmak istiyorum” gibi. Onay verdiğin anda program uygulamanın takvimine gün gün yerleşiyor. Antrenmanları oradan işaretliyorsun, uygulama da bir gün öncesinden bildirim atıp hatırlatıyor.

Teknik detaylar

Tek kayıt, dört bağımsız etki

Kullanıcı bir antrenmanı tamamlayıp kaydettiğinde tek bir WorkoutResult oluşuyor, gerisini Django signal'ları hallediyor: antrenman tamamlandı olarak işaretleniyor, programın sayacı ilerliyor, kullanıcının istatistikleri ve streak'i yeniden hesaplanıyor, achievement kontrolü tetikleniyor ve gerekiyorsa bildirim gönderiliyor. Her etki kendi Django app'inde, birbirinden bağımsız duruyor. Kullanıcı kaydı sildiğinde aynı zincir ters yönde çalışıyor, böylece sayaçlar ve streak'ler tutarlı kalıyor.

Aynı anda gelen isteklerde şaşmayan sayaçlar

Program ve kullanıcı sayaçlarını F() expression'ları ile artırıyorum. Yani toplama işlemi Python tarafında oku-değiştir-yaz şeklinde değil, doğrudan veritabanının içinde yapılıyor. Aynı saniyede tamamlanan iki antrenman birbirinin sayacını ezmiyor.

Abonelik doğrulaması iki kez çalışsa da sorun olmuyor

RevenueCat doğrulamasının idempotent olması şart, çünkü aynı satın almayı hem webhook hem uygulamanın kendisi aynı anda onaylamaya çalışabiliyor. Eşleştirmeyi RevenueCat'in app user id'si ve store'un hiç değişmeyen original transaction id'si üzerinden yapıyorum; bu id sonradan bir iade ya da itiraz olduğunda izi sürmek için de kullanılıyor. Son doğrulamanın zaman damgasını da hata ayıklamak için saklıyorum.

Yapmadığı işi yaptım diyemeyen LLM

Agent'ın elindeki tool'ları ikiye ayırdım. UI tool'ları serbest metin döndürmek yerine uygulamada tipi belli bir form açıyor. Gerçekten veritabanına yazan tek bir backend tool'u var: create_workout_plan. Model “programını hazırladım” yazsa bile bu tek başına bir şey ifade etmiyor; program ancak o backend tool'u gerçekten çalıştıysa ortaya çıkıyor.

İki servis, tek auth sistemi

FastAPI servisi, Django'nun ürettiği JWT'leri aynı signing key ile HS256 üzerinden doğruluyor ve aynı PostgreSQL veritabanını okuyor. Böylece ikinci bir login ekranı, token değiş tokuşu ya da senkron tutulması gereken ikinci bir kullanıcı tablosu gerekmiyor. LangGraph'ın checkpointer'ı da sohbetleri servis yeniden başlasa veya deploy alsa bile kaldığı yerden devam ettiriyor.

Teknoloji
Backend
Python 3.12 · Django 6 · DRF · SimpleJWT · django-q2
AI servisi
FastAPI · LangGraph · LangChain · AWS Bedrock · SSE
Veri
Aiven managed PostgreSQL · S3
Altyapı
Hetzner · Docker Compose · Caddy · GitHub Actions
Mobil
TypeScript · Expo · React Native · RevenueCat
Sayılarla
3
servis, tek monorepo
6
Django app'i
18+
REST endpoint
4
signal receiver

Mimari

PaceUp — üç servis, tek monorepo
Django 6 · FastAPI · LangGraph · Hetzner
mobile — Expo / React NativeTypeScript · Expo Router · RevenueCatcalendar plans · AI coach chat · remindersREST · api.paceupSSE stream · chatbot.HETZNER · paceup-prod-1 — docker compose -p paceup (moved off AWS ECS Fargate)Caddy — automatic HTTPScerts issued + renewed automaticallyTLS volume survives deploys via -p paceupdjango:8000graph-api:8001BACKEND/ — DJANGO 6 + DRF6 appsusers · programs · activitygamification · notifications · analyticsSimpleJWT · Google / Apple sign-indjango-q2 async tasks · S3 media4 signal receivers — the event-driven coreCHATAPI/ — FASTAPI + LANGGRAPHAI running coachPOST /chat-stream (SSE) · GET /healthcheckpointer — survives redeployUI TOOLSopens a typed formin the app, not textBACKEND TOOLScreate_workout_planthe only side effectsame DJANGO_SECRET_KEYHS256 — one auth systemAWS BedrockLLM inferencevia LangChainAiven managed PostgreSQLexternal — no database container on the boxshared by django and graph-apiCI/CDGitHub Actionspush to main→ paths-filter→ ssh to host→ rebuild only the changed service
Tek kayıt, dört bağımsız etki
Django signal zinciri
POST_SAVEWorkoutResult savedthe user finishes one runWorkout marked completedis_completed = True · status = completedProgram counter incrementedF() expression — atomic, race-proofUser stats recalculateddistance · duration · current + longest streakAchievement checkpost_save on User · gamification appNotificationpost_save on AchievementPushto devicePOST_DELETEDeleting the result unwinds the same chain in reverse — counters, stats and streaks stay honest.

Uygulama

Günün antrenmanı; mesafe ve streak bilgisi signal'lardan geliyor
Oluşturulan program, gün gün takvime dağıtılmış halde
Aktif program — 12 hafta boyunca 48 antrenman
Profil ve premium üyelik durumu, RevenueCat ile doğrulanıyor