-
-
Fajr Icon - Dark
-
ONE SCREEN, THREE WAYS - Dark, light and Arabic RTL
-
العربية - Right-to-left, Arabic-Indic numerals
-
HOME - The next prayer, counting down
-
ALARM BUILDER - Link it to a prayer, then choose how it ends
-
THE ALARM - Ringing, adhan playing, no way past it
-
PREMIUM - Three plans, and a free tier that stays free
-
MISSION MID-ACTION - Seven of ten. Still ringing.
-
RAMADAN - Suhoor, Iftar and thirty days tracked
-
QIBLA - 256° from north. 3,530 km to Makkah.
-
STREAK - Thirty-four days of Fajr
-
App Home - Screenshot
Inspiration
I did not need another app to tell me when Fajr was. I needed something to get me out of bed once it did.
That is the actual gap. Every prayer app in the store solves the information problem — times, notifications, a timetable — and almost none of them treat the hard part as a problem at all. The alarm rings, your hand finds it before your brain wakes up, and the prayer is gone. Knowing the time was never the obstacle.
Before writing any code I went and read the one-star reviews of the best mission-based alarm app I could find — the closest thing to what I wanted to build. Two complaints came up over and over, and they were not about features.
The first was reliability. People wrote that the alarm simply did not fire some mornings. For a normal alarm app that is a bug. For an app whose entire promise is that you will be awake at a specific time, it is the product failing at the only thing it exists to do.
The second was price. Review after review said some version of good app, too expensive for me — and a disproportionate number of those were coming from exactly the countries with the largest Muslim populations. A single US dollar price, auto-converted, prices out most of the people who need a Fajr alarm most.
So Fajr is built around those two answers. The alarm is native code with a hardware test matrix behind it, because it has to fire. And it is priced by market rather than by exchange rate, because a prayer app only the wealthy can afford has misunderstood who it is for.
What it does
Fajr combines accurate prayer times with an adhan alarm engineered to be hard to dismiss while half asleep.
Prayer times for Fajr, Dhuhr, Asr, Maghrib and Isha are calculated on-device from your location and work offline after setup, with multiple calculation methods, Standard and Hanafi Asr, a high-latitude rule, Standard/Jafari midnight mode, per-prayer offsets to match your local masjid, and Hijri date adjustment.
The alarm is what makes it different. A normal alarm dies with one tap, which is precisely the failure mode for Fajr. Fajr requires a wake-up mission before the alarm will stop. Seven of them: renew your niyyah with a press-and-hold, take fifteen motion-detected steps, shake the phone until the meter fills, complete a set of push-ups, solve a math challenge, type adhkar, or pass a memory tap. Three are free, four come with Premium. You pick the one that matches how deeply you sleep.
Alarms are linked to prayer times, so they shift with them through the year instead of drifting out of sync.
Around that core: a Qibla compass with calibration, a monthly timetable, Sunrise, Imsak and last-third-of-the-night times for planning Tahajjud, and a Ramadan mode with Suhoor and Iftar times, a Suhoor alarm, a fasting tracker and daily countdowns. Streaks and prayer statistics show whether the habit is actually holding.
Design came first
Roughly 50 screens were designed as a working interactive prototype before a line of app code existed — real copy, a live countdown, a functioning hold-to-confirm — rendered in dark, light and Arabic RTL for both Android and iOS, and exported as a 15-page print spec.
That order mattered more than I expected. Because the prototype had real copy, the copy became the contract: when its intention timer ran two seconds while its caption said "hold for three seconds," the visible copy won and the timer changed. Every string in the shipped app is taken verbatim from the prototype and goes through ARB localisation files.
The design system, not just the screens. Colour tokens were lifted 1:1 from
the prototype's CSS variables into app/theme.dart as FajrColors, accessed
through a context extension. No screen hardcodes a hex value. Both palettes are
complete and the theme defaults to system.
An Islamic visual language, not a generic one. The mark is a mihrab arch holding a Rub el Hizb star — it replaced an earlier sunrise semicircle that could have belonged to any wellness app. Typography is Rubik for interface, Amiri for Hijri dates and duʿāʾ display, and NotoNaskhArabic for Arabic body text, because setting Arabic script in a Latin UI face is the tell that a prayer app was localised rather than designed.
Three locales, mirrored properly. EN, UR and AR, RTL-aware from the ground
up — EdgeInsetsDirectional rather than left/right padding, an LtrNumerals
wrapper so clock faces stay readable in RTL, Arabic-Indic numerals where the
locale expects them, Semantics on interactive widgets and live regions on
mission progress.
How I built it
Flutter and Dart 3, Android first. Riverpod for state, go_router for navigation, Drift over SQLite as the local source of truth. The architecture is deliberately client-heavy and thin-cloud: the device owns alarms and prayer times, and the cloud only persists data for restore and sync. The rule I held to throughout — no network call ever sits on the alarm-fire path.
Prayer engine. adhan_dart wrapped in a PrayerEngine service with a
method registry, a country-to-default-method map, offsets, high-latitude rule
and midnight mode. It computes a rolling 60-day cache into Drift so the
timetable and Ramadan views work with the radio off, and recomputes on app open,
midnight rollover, settings change, a >75 km location shift, and timezone or DST
change.
Correctness here is a trust issue, not a feature, so it has its own CI gate. For every fixture city \(c\), calculation method \(m\) and prayer \(p \in {\text{Fajr}, \text{Dhuhr}, \text{Asr}, \text{Maghrib}, \text{Isha}}\), the build fails unless
$$\max_{c,m,p} \bigl| t_{\text{engine}}(c,m,p) - t_{\text{ref}}(c,m,p) \bigr| \le 1\ \text{minute}$$
against an independent reference implementation. That gate runs as a separate job and had to pass before I trusted any alarm scheduling built on top of it.
Alarm subsystem. This is the product, so it is native. A Kotlin module
schedules with AlarmManager.setAlarmClock — Doze-exempt, and it surfaces the
status-bar alarm icon. On fire, a BroadcastReceiver starts a foreground
service that posts a full-screen-intent notification on an IMPORTANCE_HIGH /
CATEGORY_ALARM channel, launches a full-screen activity over the lockscreen
with setShowWhenLocked and setTurnScreenOn, plays the adhan through
AudioAttributes.USAGE_ALARM with a volume ramp, and holds a partial wakelock
until the mission completes. Alarms re-arm from Drift on BOOT_COMPLETED and
MY_PACKAGE_REPLACED.
Flutter talks to it through a single AlarmScheduler interface defined in
Pigeon. Screens never touch a platform channel directly.
Missions. Seven missions, all on-device — no cloud, no LLM, nothing recorded
or uploaded. A privacy promise and an offline requirement at once. ML Kit Pose
Detection counts push-up reps from landmark motion, and sensors_plus drives
Stand Up and Shake to Wake. Make Intention is a timed press-and-hold. Math,
Memory Tap and Typing Adhkar are pure logic.
The testable logic lives in domain/ rather than widget state, because the
product risk is in it: StepDetector, PushupCounter and
AchievementRepository.countersFrom are each pure functions tested at both
failure ends. Every camera or motion mission degrades to Make Intention through
one shared path if permission is denied or a sensor is unavailable.
Backend. Firebase Auth (Google + Apple), Cloud Firestore with offline persistence on, Firebase Analytics and Crashlytics, and minimal Cloud Functions in TypeScript. Local Drift is primary; Firestore is the backup and restore layer, hydrated on login and written through on change. Entitlement is written only by the RevenueCat webhook through a Cloud Function, never by the client, so a tampered client cannot grant itself Premium.
How RevenueCat powers Fajr Premium
One entitlement, four products, two offerings.
The entitlement is premium, and all four products map to it. The app never
asks which SKU a user holds — it asks whether the entitlement is active. Adding,
repricing or retiring a plan touches the RevenueCat dashboard, not app logic,
and never requires a release.
The default offering carries three packages on the standard identifiers
$rc_monthly, $rc_three_month and $rc_annual. A second downsell offering
carries a discounted annual product on its own. Keeping the discount out of
default is deliberate: a single offering holding both a full-price and a
discounted annual product is one substring match away from selling the cheap one
as the headline plan, and an audit of the paywall code caught exactly that risk
before launch.
Four paywall entry points, each at a moment of intent. Rather than one generic wall, the paywall surfaces where a user has just decided they want something specific — during onboarding once the quiz has suggested a mission; when creating an alarm and reaching for a custom tone upload; when switching on Ramadan mode in Settings; and directly from a Premium entry in Settings for anyone who has already made up their mind. The pitch differs by context, because someone who just tried to upload their own adhan wants a different sentence than someone browsing.
The funnel itself is deliberately short — the paywall, a trial reminder, and a second-chance offering shown once per install. Two paywall screens from the original design were cut because they added steps without adding a decision.
Three durations, because the commitment curve is strange. Someone who has just slept through Fajr three days running will pay today but will not commit to a year. Someone mid-Ramadan often will. Monthly is the low-friction entry, quarterly covers a realistic habit-forming window, and the free trial sits on annual only — because this product proves itself across a week of real mornings, not in a session.
Priced by market, not by exchange rate.
This is the decision I am most confident about, and it came directly out of reading competitor reviews.
Google's price editor will happily convert one USD anchor into 170 currencies. That keeps the nominal price constant and lets the real burden float. If \(P\) is the subscription price and \(W\) is local median monthly income, a single converted anchor means
$$\frac{P_{\text{converted}}}{W_{\text{local}}} \;\gg\; \frac{P_{\text{USD}}}{W_{\text{US}}}$$
by an order of magnitude or more across Pakistan, India, Indonesia, Bangladesh, Egypt and Nigeria — which is to say, across most of the countries with the largest Muslim populations on earth. It is technically correct pricing and commercially absurd.
So Fajr ships with three price tiers: a high tier for high-income markets, a middle default, and a substantially lower tier for price-sensitive markets — with locally sensible charm prices rather than converted decimals, because a number that reads as a real local price converts differently from a rounding artefact.
Two things made this practical rather than a 170-country data-entry project:
- Play Console pricing templates. The regional pattern is defined once and applied across all four products, so the strategy lives in one place and can be revised in one place.
- RevenueCat reads store prices live. Every price, in every currency, in every tier, renders in the paywall with no code change and no release. The per-month labels and the computed discount percentage are derived from whatever the store returns, so they stay truthful in every market automatically — provided the tier ordering holds, which is spot-checked per tier.
The discount product is set to a fixed proportion of the full annual price within each tier, so the advertised saving is accurate everywhere rather than accidentally becoming a different percentage in Indonesia than in Germany.
What Premium unlocks
| Free | Premium | |
|---|---|---|
| Missions | Make Intention, Stand Up, Shake to Wake | push-ups, math, typing adhkar, memory tap |
| Sounds | Makkah adhan | every other adhan and tone, plus upload your own |
| Stats | current streak | full history, on-time % |
| Ramadan | — | Suhoor alarm, timetable, fasting tracker |
| Sync | — | backup and restore |
Two of those lines are deliberate and were argued over.
Make Intention is the free mission on purpose. It needs no sensor, no permission and no ML model, so the free tier has no failure mode. It is also the fallback every other mission degrades to — which means the paid missions can afford to be ambitious, because there is always a floor.
The current streak stays free while history and on-time percentage are paid. The streak is the number the whole product is judged on. Gating it would gate the core loop rather than an extra, and a user who cannot see their streak has no reason to keep the app long enough to convert.
Where the gates actually sit. Entitlement is read through
purchases_flutter and checked at the point of use, never against a local
boolean, and it is written server-side by the RevenueCat webhook through a Cloud
Function — never by the client — so a tampered app cannot grant itself Premium.
One subtle case worth naming, because getting it wrong produces the worst bug this app could have: onboarding arms a real alarm before the paywall appears. The mission suggested by the 7-step quiz is resolved against the entitlement before the alarm is committed, so nobody finishes onboarding holding an alarm they will not be able to dismiss. An adhan playing at 4am behind a locked mission is not a paywall, it is a bug with a rights holder's recitation attached.
Restore is implemented on both the paywall and the Subscription screen.
Challenges I ran into
A contract with two ends, wired at one. The entire wake-up loop was dead for
two milestones. Alarms scheduled, the native module fired, the Dart side had a
fully implemented handler — and nothing happened, because
AlarmFlutterApi.setUp was never called in Dart. Every individual piece looked
correct in review. Pigeon generates both ends of an interface and it is entirely
possible to implement one and never register it. The rule I took from it: when a
contract has two ends, grep both.
A layout assertion that erased a whole screen. An onboarding screen rendered
as a blank page for two milestones. FajrGhostButton declared a
double.infinity minimum width — a reasonable thing to write and a fatal thing
to put inside a Row, because a Row lays its non-flexible children out with
unbounded width, so infinity there is not a wide button but a layout assertion,
and it blanks the entire subtree it lives in. The only evidence was in the log.
Design-system widgets now read their incoming constraints with LayoutBuilder
instead of assuming a parent bounds them, and every one has a Row case in its
test.
A mission I had to delete. Recite / Adhkar used on-device speech recognition and could not be made reliable — a device with no offline model downloaded listens indefinitely and returns nothing. Which means the failure mode is a user standing at a ringing adhan at 4am with no way to complete the mission and no way to stop the sound. There is no graceful version of that, so the mission is gone and nothing in Fajr uses the microphone any more.
That asymmetry drove how the push-up mission is tuned. PushupCounter is
deliberately permissive, because the two error directions are not equally bad:
a false reject is someone doing press-ups on a cold floor at 4am with the adhan
still going and no way to stop it; a false accept is someone who is already out
of bed. Only one of those makes people uninstall.
A paywall that could have sold the wrong product. The down-sell was introduced as a fourth product after the subscription layer was already written, and the plan resolver matched packages by product-ID substring — which meant the discounted annual product matched the full-price annual slot. Nothing crashed. The paywall would simply have rendered or charged the wrong number. It was found by auditing the whole subscription path against the store configuration rather than by testing the happy route, and the fix was structural: the discount lives in its own offering with an explicit lookup, so the two can never collide.
Three locales are longer than one. Everything was built RTL-aware from the
start, and the first widget test on the Ramadan screen still found the bug that
shape hides: FajrListRow rendered its value unconstrained, so a long string
pushed the row off-screen — and Urdu and Arabic run longer than English for the
same copy. Fixed at both ends, and every new screen test now carries a
three-locale render assertion.
What I learned
Shipping an alarm is maybe 20% product work and 80% negotiating with the platform. Doze, exact-alarm permissions, full-screen-intent qualification on API 34, boot re-arm, and OEM battery managers are the actual project.
Both multi-milestone bugs above were invisible to code review and caught only by writing a test. I stopped believing a screen worked because it looked wired.
And I learned that pricing is a product decision, not a spreadsheet one. The single most consequential thing I did for this app's reach was not a feature — it was refusing to let one USD number auto-convert into the currencies of the countries this app is most for.
Privacy, and a free tier that is actually complete
Prayer apps are a category where location data has a poor history. Fajr is built so there is nothing to misuse.
Prayer times and Qibla bearing are computed on the device. No network call sits on the alarm path. Mission sensor data — camera, motion — is processed locally and discarded; nothing is recorded and nothing is uploaded. There are no ads and no data sale, stated plainly in the app rather than buried in a policy. Firestore is locked so a user can read and write only their own documents, and entitlement is server-written so it cannot be tampered with. Account deletion is available in-app.
And the free tier is not a trial in disguise. Without paying anything, ever, you get accurate prayer times for all five prayers, the full monthly timetable, a real can't-snooze adhan alarm on any prayer, three working wake-up missions, the Qibla compass, and your current streak. The paywall has a second-chance offer and then a screen that tells you honestly what you keep. And for the people who do subscribe, the price is set by where they live rather than by what the dollar is worth that week.
What happened after launch
Fajr went live on Google Play on 11 September 2026 (10 September PT), 19 days before the submission deadline.
Roughly 100 installs. 4.73 stars. 94.6% of the install base retained — the app is being kept, not tried and deleted. About two thirds of users are in Pakistan, with the remainder in the UAE, India and Ethiopia.
Zero trial starts and zero revenue.
I would rather state that plainly than dress it up, because I think both halves are explained by decisions I would make again.
The retention number is the one I actually care about. 94.6% retained, on an app whose defining feature is an alarm that refuses to switch off until you have physically got up, says the alarm is firing and people are tolerating being made to obey it. That was the entire product risk and it is the thing 19 days could genuinely test.
The revenue number is a consequence of where the users are and what the free tier contains. Two thirds of them sit in my lowest price tier by design. And the free tier is complete enough that a user has to want a specific premium thing before a paywall means anything to them — a slower funnel than a hard wall, and a trade I made knowingly.
Accomplishments I'm proud of
The alarm fires. That sounds like a low bar until you have read Android's documentation on Doze and full-screen-intent qualification, and until you have watched an aggressive OEM battery manager quietly kill a scheduled task overnight.
The two test devices were chosen to cover opposite failure surfaces. The Redmi 9 Activ tops out at Android 11 with MIUI, which is where OEM battery management is at its most aggressive and where scheduled work silently disappears. The Samsung Galaxy A15 runs modern Android, where the hazard is the permission gauntlet instead — runtime notification permission, exact-alarm permission, and full-screen-intent qualification. An alarm app has to survive both, and they fail in completely different ways.
Fajr has been through the matrix on both: reboot, Doze and idle, battery optimisation on and off, silent and DND, screen off and locked, timezone and DST change, app force-stopped, and multiple alarms in one day.
All ~50 designed screens implemented with no stubs left. flutter analyze
clean, dart format clean, Cloud Functions typechecking clean, and 344 tests
passing with one skipped — the skip is an opt-in live network cross-check,
offline by design in CI.
How to test
The annual plan includes a 7-day free trial — no promo code needed.
Fastest path to the paywall: Settings → Ramadan mode. Or create an alarm and try to upload your own adhan: Alarms → + → Sound → Upload your own. There is also a Premium entry in Settings, and the paywall appears once during onboarding.
To see the core loop without waiting for dawn: Alarms → set an alarm one minute out → lock the phone. The alarm takes over the lockscreen, the adhan ramps up, and it will not stop until the mission is completed.
What's next
Ramadan begins around 8 February 2027, and it is the only moment of the year when this entire audience is already awake before dawn. Every premium feature that isn't a mission — Suhoor alarm, Ramadan timetable, fasting tracker — points at that month, and the annual plan with its trial is the instrument built to meet it.
Between now and then, v2: a Quran reader that opens to the ayah you left rather than a list of 114 surahs, morning and evening adhkar reachable in one tap from the moment the mission completes, a dhikr counter, and a second streak beside the Fajr one. The organising idea is that Fajr owns the morning — wake, pray, and the twenty minutes after — rather than becoming a smaller version of the general-purpose Islamic apps that already exist.
Then iOS. The whole app is Flutter, but the alarm is not, and iOS needs its own native module built on AlarmKit — the only way to hold the screen and gate dismissal properly on that platform. The fallback path, repeating critical-alert notifications, fires on time but cannot stop someone swiping it away without completing the mission, which defeats the point of the app.
After that: home-screen widgets, a Watch complication, masjid-specific timetables submitted by local communities, and shared streaks with a friend — because the one thing that reliably beats a hard alarm is someone else knowing whether you got up.
Built With
- adhan-dart
- cloud-firestore
- cloud-functions
- crashlytics
- dart
- drift
- firebase
- firebase-analytics
- firebase-auth
- flutter
- flutter-compass
- flutter-local-notifications
- flutter-secure-storage
- go-router
- google-ml-kit
- kotlin
- pigeon
- revenuecat
- riverpod
- sensors-plus
- shared-preferences
- sqlite
- typescript
Log in or sign up for Devpost to join the conversation.