Skip to content
← Back to portfolio

Project case study · Mobile system

Duty Pharmacies: Mobile

A React Native companion app with a live Mapbox map of on-duty pharmacies, backed by Cloudflare Workers and D1.

React NativeExpoTypeScriptCloudflare WorkersCloudflare D1tRPCMapbox
Duty Pharmacies: Mobile

The challenge

The Duty Pharmacies web app works well in a browser, but people often need to find an open pharmacy from their phone. GPS is usually the quickest answer. I built the mobile version around the same government-sourced data, with a live map and location-aware list designed for that moment.

The solution

The Expo and React Native app centers on a Mapbox map that shows on-duty pharmacies relative to the user’s GPS position. Results are filtered by district and date. Language selection is a UI locale switcher in Settings, not a filter for individual pharmacies.

The app talks to a dedicated Hono and tRPC API on Cloudflare Workers, backed by Cloudflare D1 through Drizzle ORM. It uses the same government-sourced pharmacy dataset as the web app, including its daily sync and geocoding backfill. React Query handles caching and retries. If the Worker API is unreachable, the mobile hook can query data.gov.cy directly. EAS build profiles are configured, but the app has not shipped to the App Store or Play Store yet.

What the app does

Live Mapbox map with GPS

Once GPS resolves, the Mapbox camera moves to the user’s coordinates and plots each on-duty pharmacy as a selectable marker. The list and detail views show the calculated distance and contact details. The detail sheet identifies the selected pharmacy as on duty, and records resolved only to a district centroid are clearly marked as approximate.

Nearest-first results with a clear fallback

The pharmacy.list procedure queries D1 for a district and date. When GPS coordinates are supplied, it orders the results with the shared Haversine calculation; pharmacies without precise coordinates receive their district centroid so they remain visible, but are marked as approximate. pharmacy.search is a separate text-search procedure for names, surnames, and municipalities.

One backend for web and mobile

The app and the web version use the same D1-backed tRPC API, so both get the same pharmacy data and daily refresh. In the pnpm workspace, the router lives in @dutypharmacies/api and exports its AppRouter type to the mobile client. Pharmacy and geospatial helpers live in @dutypharmacies/shared, while @dutypharmacies/db owns the Drizzle schema.

Government data ingestion

The daily Cloudflare Cron sync pulls all five district feeds from the government CKAN datastore in parallel. The upstream schema contains field-name typos such as surmame and muniuciplity__community, so the sync maps them to normalized surname and municipality fields. It checks the required schema before writing pharmacy and duty-schedule records to D1 in batches of 100. The geocoding pass tries Google for precise coordinates, then uses the district centroid when it cannot resolve an address.

Architecture

Frontend: Expo (~52) with React Native 0.76.9, Mapbox for the map layer, and tRPC with React Query for data fetching.

Backend: Hono and tRPC on Cloudflare Workers, with Cloudflare D1 as the database through Drizzle ORM. The web app uses this backend too.

Monetization: AdMob banner integration is built but switched off with ADS_ENABLED = false while the app is in pre-launch. The component uses Google’s test banner IDs in development or when production IDs are placeholders. No ads are currently earning revenue.

Status: Built with EAS, with build profiles configured. Store submission is still pending.

Have a project in mind?

Tell me what you're working on and where you need help.