Ease

Nöbetçi Eczane · on-duty pharmacies in Turkey

How it is built

Ease answers one question: which pharmacy is on duty near me right now, and how far is it? This page covers the few engineering decisions behind that answer, and how each one is checked.

The iPhone app calls an API on the app's own domain, a Cloudflare Worker, which answers from a KV cache. A scheduled refresh, twice a day, fills the cache from a paid pharmacy data service. User requests never reach the paid service. For driving times the Worker asks a public routing service.
The app only ever talks to the API. Only the scheduled refresh talks to the paid data service.

The shape

The app is written in TypeScript with Expo (React Native). It talks to one small API on the app's own domain, running as a Cloudflare Worker. The Worker answers every request from a cache in Cloudflare KV. A scheduled job fills that cache from a paid pharmacy data service. Neither the app nor any user request ever calls that service.

A cache sized to a budget

The pharmacy data service charges for each call. So the design starts from the bill rather than from traffic: user requests only read the cache, and only the scheduled job pays for fresh data. The data bill stays the same however many people open the app on a busy night.

The on-duty roster is refreshed twice a day, morning and evening Turkey time, a few minutes after the service's own update times so that its data has settled. An earlier schedule refreshed three times a day. Once the arithmetic was written down, it would have used up the monthly plan before the month was over, so the midday refresh was dropped. That calculation now sits next to the schedule in the configuration, together with the rule that a third daily refresh needs a bigger plan first.

The list of all pharmacies, not only those on duty, changes slowly, so it is not refetched on a schedule. It is loaded by hand when needed, and a weekly job adds only recently added pharmacies, which costs little.

For distances, the Worker takes the nearest pharmacies by straight-line distance and asks a public routing service for driving distance and time. That service is free and sometimes slow, so the call has a time limit. If it fails, the list still comes back, ordered by straight-line distance.

Never show yesterday's list as today's

The failure that matters most here is quiet: a plausible-looking list of the wrong pharmacies at 3am, sending someone to a closed door. So the app has one firm rule. An on-duty list that is not current is not shown at all. Instead, the screen says current duty data could not be fetched and points to the “All Pharmacies” tab, which still has addresses and phone numbers.

The rule is enforced in two places.

On the server, every response says whether the roster is stale, based on the time since the last successful refresh. The limit is set above the longest gap between scheduled refreshes, the overnight one. A normal night never hides the list, but a missed refresh does.

On the phone, the last list you loaded is saved so it can be shown when you lose connection. An earlier version of this code had two flaws. The saved copy carried the “stale” flag from the moment it was written, so last night's list read back as fresh the next morning. And the screen's guard against stale lists made an exception for offline mode, which is the one case that produces old lists. Both flaws had the same symptom: a list of the wrong pharmacies, with no warning.

The fix stamps each saved copy with the time it was written and judges it by duty day. Duty rosters hand over in the morning, so a duty day runs from 08:00 to 08:00 Turkey time. A list saved at 22:00 is still good at 03:00, and it is hidden at 10:00 the next day. A saved copy with no timestamp, from before the fix, counts as stale: unknown age means unusable. The all-pharmacies list is exempt, because a pharmacy that exists today existed yesterday.

Checks that fail when the bug comes back

These rules are guarded by a small check suite that runs with one command, together with the TypeScript type check. The staleness checks pin the duty-day boundary (07:59 and 08:01), a list saved at night and read at 03:00, the same list read the next morning, a copy with no timestamp, and the all-pharmacies exemption. Two of the checks read the source code itself. One fails if the screen's guard ever exempts offline mode again. The other fails if the cache write loses its timestamp.

A check is only trusted once it has been seen to fail. While writing this page, the old offline exception was put back into a copy of the screen, and one check failed. Then the duty-day comparison was taken out of a copy of the staleness logic, and another check failed. With the real code, all ten pass.

The medicine list has its own seventeen checks. They cover refill timing, what a shared list does and does not carry, and that background location permission is declared only while the feature that needs it is switched on.

Privacy by construction

Your location is sent with each request so the Worker can find the nearest pharmacies, and passed to the routing service for driving times. The Worker does not store it, and there is no account to tie it to. The app has no ads, no ad SDKs and no analytics.

The medicine list and any photos of medicine boxes stay on the phone. Photos are kept in the app's own storage instead of the camera roll, because the camera roll is backed up to the cloud automatically. Refill reminders are scheduled on the device, with no push token and no server involved. A list is shared only when you tap the share button, as a text message through WhatsApp on your phone.


Nasıl yapıldı

Ease tek bir soruya cevap verir: şu an yakınımda hangi eczane nöbetçi ve ne kadar uzakta? Bu bölüm o cevabın arkasındaki birkaç mühendislik kararını ve her birinin nasıl denetlendiğini anlatır.

Genel yapı

Uygulama Expo (React Native) ile TypeScript'te yazıldı. Uygulamanın kendi alan adındaki tek bir küçük API ile konuşur; bu API bir Cloudflare Worker'dır. Worker her isteği Cloudflare KV'deki önbellekten yanıtlar. Önbelleği, planlı bir görev ücretli bir eczane veri servisinden doldurur. Ne uygulama ne de herhangi bir kullanıcı isteği o servise hiç gitmez.

Bütçeye göre boyutlanmış önbellek

Eczane veri servisi her çağrı için ücret alır. Bu yüzden tasarım trafikten değil faturadan başlar: kullanıcı istekleri yalnızca önbelleği okur, taze veri için yalnızca planlı görev ödeme yapar. Yoğun bir gecede uygulamayı kaç kişi açarsa açsın veri faturası aynı kalır.

Nöbetçi listesi günde iki kez, Türkiye saatiyle sabah ve akşam, verinin oturması için servisin kendi güncelleme saatlerinden birkaç dakika sonra yenilenir. Önceki takvimde günde üç yenileme vardı. Hesap yazıya dökülünce bunun aylık planı ay bitmeden tüketeceği görüldü ve öğlen yenilemesi kaldırıldı. O hesap artık ayarlarda takvimin hemen yanında duruyor; günde üçüncü bir yenileme için önce daha büyük bir plan gerektiği kuralıyla birlikte.

Yalnızca nöbetçileri değil tüm eczaneleri içeren liste yavaş değişir, bu yüzden takvimle yeniden çekilmez. Gerektiğinde elle yüklenir; haftalık bir görev yalnızca yeni eklenen eczaneleri ekler; bunun maliyeti düşüktür.

Mesafe için Worker, kuş uçuşu en yakın eczaneleri alır ve herkese açık bir yol tarifi servisinden araçla mesafe ve süreyi ister. Bu servis ücretsizdir ve bazen yavaştır; bu yüzden çağrının bir süre sınırı vardır. Başarısız olursa liste yine gelir, kuş uçuşu mesafeye göre sıralanır.

Dünün listesi asla bugününmüş gibi gösterilmez

Burada en önemli hata sessiz olandır: gece 3'te makul görünen ama yanlış eczanelerden oluşan bir liste, birini kapalı bir kapıya gönderir. Bu yüzden uygulamanın tek bir kesin kuralı var: güncel olmayan nöbetçi listesi hiç gösterilmez. Ekran bunun yerine güncel nöbetçi verisinin alınamadığını söyler ve adres ile telefonların hâlâ bulunduğu “Tüm Eczaneler” sekmesine yönlendirir.

Kural iki yerde uygulanır.

Sunucuda her yanıt, son başarılı yenilemeden bu yana geçen süreye bakarak listenin bayat olup olmadığını söyler. Sınır, planlı yenilemeler arasındaki en uzun boşluğun, yani gece boşluğunun üstüne konmuştur. Olağan bir gece listeyi gizlemez, kaçırılmış bir yenileme gizler.

Telefonda, bağlantı koptuğunda gösterilebilmesi için en son yüklenen liste kaydedilir. Bu kodun önceki bir sürümünde iki kusur vardı. Kayıtlı kopya, yazıldığı andaki “bayat” işaretini taşıyordu; böylece dün geceki liste ertesi sabah taze görünüyordu. Ekranın bayat listeye karşı koruması da çevrimdışı durumu istisna tutuyordu; oysa eski listeyi üreten tek durum oydu. İki kusurun belirtisi aynıydı: hiçbir uyarı olmadan yanlış eczanelerin listesi.

Düzeltme, her kayıtlı kopyaya yazıldığı zamanı damgalar ve onu nöbet gününe göre değerlendirir. Nöbet listeleri sabah devredilir; bu yüzden bir nöbet günü Türkiye saatiyle 08:00'den 08:00'e kadar sürer. 22:00'de kaydedilen liste 03:00'te hâlâ geçerlidir, ertesi gün 10:00'da gizlenir. Düzeltmeden önceye ait, zaman damgası olmayan bir kopya bayat sayılır: yaşı bilinmeyen liste kullanılamaz. Tüm eczaneler listesi bu kuralın dışındadır, çünkü bugün var olan bir eczane dün de vardı.

Hata geri gelince düşen denetimler

Bu kuralları, TypeScript tür denetimiyle birlikte tek komutla çalışan küçük bir denetim takımı korur. Bayatlık denetimleri nöbet günü sınırını (07:59 ve 08:01), gece kaydedilip 03:00'te okunan listeyi, aynı listenin ertesi sabah okunmasını, zaman damgası olmayan kopyayı ve tüm eczaneler istisnasını sabitler. Denetimlerden ikisi kaynak kodun kendisini okur. Biri, ekranın koruması çevrimdışı durumu yeniden istisna tutarsa düşer. Diğeri, önbelleğe yazma zaman damgasını kaybederse düşer.

Bir denetime ancak düştüğü görüldükten sonra güvenilir. Bu sayfa yazılırken eski çevrimdışı istisnası ekranın bir kopyasına geri konuldu ve bir denetim düştü. Sonra nöbet günü karşılaştırması bayatlık mantığının bir kopyasından çıkarıldı ve başka bir denetim düştü. Gerçek kodla on denetimin hepsi geçer.

İlaç listesinin kendi on yedi denetimi var. Bunlar yeniden alım zamanlamasını, paylaşılan listenin neyi taşıyıp neyi taşımadığını ve arka plan konum izninin yalnızca onu gerektiren özellik açıkken bildirildiğini kapsar.

Yapısı gereği gizlilik

Worker en yakın eczaneleri bulabilsin diye konumun her istekle gönderilir, araçla süre için de yol tarifi servisine iletilir. Worker konumu saklamaz ve onu bağlayacak bir hesap da yoktur. Uygulamada reklam, reklam SDK'sı ve analiz aracı yoktur.

İlaç listesi ve ilaç kutusu fotoğrafları telefonda kalır. Fotoğraflar galeride değil uygulamanın kendi alanında tutulur, çünkü galeri otomatik olarak buluta yedeklenir. Yeniden alım hatırlatmaları cihazda kurulur; ne bildirim jetonu ne sunucu vardır. Liste yalnızca paylaş düğmesine dokunduğunda, telefonundaki WhatsApp üzerinden metin olarak paylaşılır.