All projects

Zakkir

An Arabic-first prayer times and Azkar app for Android, desktop and Firefox, built on one web renderer, with reminders that keep working offline.

Context
Open source, solo project, June to August 2026
Role
Design and build, end to end
Stack
JavaScript renderer, Electron, React Native (Expo), Firefox MV3, Playwright
Outcome
Ten tagged releases, 138 Azkar, a published Firefox add-on
Zakkir on Android: the next prayer, Fajr, with a countdown, the day's prayer times and an Azkar card in Arabic with Previous, Reset and Next buttons.

Prayer times and Azkar, in Arabic first.

Zakkir shows the five daily prayer times with a countdown to the next one, and a library of 138 Azkar you can read and count: morning, evening, after-prayer, sleep, tasbih, and Quranic and prophetic supplications. It runs as a Windows and Linux desktop app, an Android app and a Firefox extension.

The interface is Arabic-first: right-to-left layout, ten bundled Arabic fonts and Arabic notification texts. There is no account and no backend. Prayer times come from the Aladhan API, and settings stay on the device.

I built it alone, from the prayer-time fetching to the Android build.

One renderer, wrapped by each platform.

The screens are one web renderer: popup.html, popup.css, popup.js and the Azkar data. Each platform wraps it.

  • Desktop. Electron opens the renderer in a frameless, always-on-top window. A 15-line preload bridge handles settings storage and window controls.
  • Android. A build script inlines the CSS, JavaScript, Azkar JSON, ten fonts and the sounds into one generated TypeScript file. An Expo app loads it in a WebView and replaces the Electron calls with messages to React Native, which owns notifications, storage and haptics.
  • Firefox. The extension started from the same popup files. It lives on its own branch with a background script for alarms, and has since diverged from the desktop renderer. So "one renderer" is exact for desktop and Android, and only a shared starting point for Firefox.

The cost is that every platform feature is a message across a bridge, and the Android bundle is one generated file of about 4 MB. The gain is that a fix to the reading screen reaches desktop and Android with one change.

Zakkir: one renderer, three shells The shared renderer is made of popup.html and popup.css, popup.js, azkar.json and notification-scheduler.js. Electron loads it directly and owns settings.json and a 15 second timer. The Android app inlines it into one TypeScript file and runs it in a WebView, and owns AsyncStorage, notifications and haptics. The Firefox extension is a copy on its own branch that has since diverged, and owns chrome.storage and alarms. All three fetch prayer times from the Aladhan API. One renderer, three shells Desktop and Android run the same files. Firefox started from them. Shared renderer popup.html + popup.css layout and themes popup.js screens, settings, countdown azkar.json 138 entries, 8 categories notification-scheduler.js pure function, shared loads directly inlined copied once Electron Windows and Linux desktop app loads popup.html through preload.js owns: settings.json, 15 s timer Expo and React Native Android app, renderer runs in a WebView build script makes one TypeScript file owns: AsyncStorage, notifications, haptics Firefox extension (MV3) own branch, started from the popup files has since diverged from the renderer owns: chrome.storage, alarms Aladhan API prayer times, fetched a month at a time Solid gold: the shell uses the shared files. Dashed: a copy that has since diverged. The first launch needs one successful fetch; after that, times come from a local cache.
The renderer is shared. What each shell owns is the part that cannot be: storage, notifications and the app lifecycle.

Reminders that work without a connection.

Prayer times move every day, so a repeating daily alarm drifts. Zakkir fetches times from the Aladhan API a month at a time and keeps the next 60 days, and the last 2, in a local cache keyed by location and calculation method. After one successful fetch, times and reminders keep working offline. The very first launch still needs the network, because the app does not calculate prayer times itself.

A shared scheduler module turns each day's times into events: a reminder before the prayer, one at the adhan and one at the iqama, each with its own minute offset per prayer. It is a pure function, so I could test the awkward cases: settings from older versions, per-prayer overrides, late timers, and an iqama that falls after midnight.

Delivery differs by platform, because the platforms allow different things.

  • Desktop checks every 15 seconds for events that are due, and remembers the keys of sent events for seven days so a delayed tick never repeats a notification.
  • Android cancels all pending notifications and recreates one-shot ones for the next 28 days. Five prayers, three event types and 28 days make at most 420, under Android's limit of 500 pending alarms.
Zakkir: prayer times cached for 60 days, Android reminders scheduled for 28 A timeline from today to 60 days ahead. The cache of prayer times covers 2 days back and 60 days ahead. Android notifications are scheduled for the next 28 days: five prayers, three events each, at most 420 against a limit of 500. Each event has an offset of 1 to 60 minutes for the before and iqama events and a delivery window of 15, 15 and 30 minutes. On desktop a timer runs every 15 seconds, checks events due yesterday and today and skips event keys already sent in the last 7 days. Cached for 60 days, scheduled for 28 Prayer times are cached once; each platform turns them into reminders its own way. Today: Android cancels everything, then reschedules Cached prayer times: 2 days back to 60 days ahead, fetched a month at a time Android notifications: the next 28 days today +28 days +60 days Three events per prayer Event Offset Delivered if late by up to Before the prayer 1 to 60 min before 15 min Adhan on time 15 min Iqama 1 to 60 min after 30 min 5 prayers × 3 events × 28 days = 420 at most Android allows 500 pending notifications Desktop: a timer instead of a schedule 15 s Each tick looks at events due yesterday and today, so an iqama after midnight is not missed. Keys of events already sent are kept for 7 days, so a late tick never repeats a notification.
The cache and the Android schedule are two different windows: 60 days of times are stored, and 28 days of notifications are scheduled from them.

Arabic reading and themes.

Arabic text is the main content, so the reading screen has ten bundled fonts, a size setting, and separate rendering for the opening basmala and istiadhah. Switching to Arabic flips the layout to right-to-left, translates the interface, and uses Arabic plural forms for the countdown, such as "ساعتان" and "دقيقتان".

The app has a large set of visual themes and about 40 accent colors that recolor any theme. The two screens below are the same desktop home in a light and a dark theme.

Tests and releases.

The desktop app has a Playwright suite that launches the real Electron app with a clean profile. It covers navigation, settings, theme switching, notification toggles, the Azkar controls and a full right-to-left check. The scheduler has its own unit tests in node:test, but that file is not matched by npm test, so continuous integration does not run it.

A GitHub Actions workflow on the main branch runs the Playwright suite, then builds a Windows installer, a Debian package and an AppImage with electron-builder and publishes them. The repository has ten tagged releases, from v1.1.0 on 3 June 2026 to v1.5.0 on 4 August 2026.

The Android app and the Firefox extension have no automated UI tests. The Android build has a GitHub Actions workflow that I have not committed yet, so I do not call the APK CI-built.

Running on a phone and in Firefox.

Two screen recordings: the Android app (1 minute 33 seconds) and the Firefox extension popup (1 minute 1 second). The add-on is published on Mozilla's add-ons site.

Limits, and what I would change.

  • The first launch needs the network. Bundling an offline calculation would remove that.
  • Firefox forked from the desktop renderer. I would merge it back so one fix covers all three.
  • Only desktop is tested end to end. Android and Firefox have no UI tests.
  • Prayer times depend on the Aladhan API and the calculation method it offers.
  • Version numbers drift between package.json, the manifests and the tags.

Read the code.

The source is MIT licensed.