Case study — Local-first astronomy in the browser
AWQAT
أوقات — “times.” A retro-pixel prayer clock that computes the day’s five prayers from solar geometry, then draws the sky it just calculated.
Most prayer apps are thin clients for somebody else’s API — they phone home for a timetable, wrap it in ads, and go blank on airplane mode. But prayer times aren’t a database. They’re astronomy — a closed-form calculation that’s been solvable with pencil and paper for centuries.
AWQAT does the whole thing on-device, in vanilla JavaScript with zero runtime dependencies, fully offline. And then it goes one step further: the clock face is a living pixel scene, driven by the same solar engine that set the times.

The real problem
Prayer times are astronomy. Astronomy runs fine on a phone.
Fajr begins when the sun is a fixed angle below the horizon. Dhuhr is solar noon. Asr is a shadow-length ratio. Maghrib is sunset; Isha, deeper twilight. Given a date, a latitude, and a longitude, all five fall out of the solar declination and the equation of time — the same two quantities printed on the back of every almanac.
The existing options ignore that. The API apps need connectivity, show ads, and ship your location upstream to display numbers they can’t explain. The printed masjid timetable is beautifully reliable — for one city, one calculation method, one year. Travel, or follow a different school on Asr, and you’re back to searching.
When the computation is centuries old and fits in your pocket, the respectful move is to do it on the device — offline, private, and explained.
The principle
One engine: sun position → angles → your clock.
The core is a few dozen lines of celestial mechanics. From the Julian day it derives the sun’s declination and the equation of time; each prayer is then a root-finding question — at what hour does the sun reach this angle?— answered per method, per madhhab, entirely in UTC, and localized only at the moment it’s drawn.
lat / lng
- Julian day → mean anomaly & ecliptic longitude
- Solar declination
- Equation of time
- Fajr / Isha: twilight depression angles
- Dhuhr: solar noon · Maghrib: sunset
- Asr: shadow = length ×1 (or ×2, Hanafi)
- All roots solved in UTC
- Rendered through Intl time zones
- High-latitude fallback rules
on screen
The decisions that mattered
Four calls shaped everything else.
The dependency list is the attack surface, so the list is empty.
The production bundle is one sectioned module of vanilla ES — 18 KB gzipped — driving a static HTML shell. Vite and jsdom exist only at build and test time. There is nothing to phone home, nothing to expire, nothing to audit but the source itself. For an app people build a daily practice around, boring longevity is a feature.
Every time is solved in UTC; localization is a display concern.
The solar math never touches local time. Each prayer is a root-finding question — at what hour does the sun reach this angle? — answered in UTC, per method, per madhhab, and rendered through Intl time zones only at the last moment. Seven calculation methods (ISNA, MWL, Egyptian, Umm al-Qura, Karachi, Tehran, Jafari) are just different pairs of twilight angles; the Hanafi Asr is a one-line change to a shadow factor.
If the app computes the sun, the app should show the sun.
The same engine that sets the times drives a pixel scene above them. The sun's screen position comes from the actual fraction of daylight elapsed — not an animation loop. The palette blends continuously through dawn, day, dusk, and night; the mosque windows light at Maghrib; and the moon renders at its real synodic phase, waxing on the right, waning on the left, earthshine on the dark side.
A prayer app carries real responsibility, so the disclaimers ship on every view — not in the fine print.
Every card carries its astronomical definition — "when an object's shadow equals its own length" — and the footer names the method, madhhab, coordinates, and time zone that produced the numbers. Congregational times are explicitly deferred to the local masjid. Tabular Hijri dates are labeled estimates that can differ from moon sighting. The app can always answer why.
Seeing it run
Four frames, one screen, hours apart.
These are real captures of the same view across a single day — not mockups, not alternate themes. The palette blends continuously between phases; the countdown and period label track the actual prayer windows; the timeline segment for the current prayer stays lit.




Tonight's moon
computing…
Not a screenshot. This is the app’s actual moon renderer running on this page — it just drew the moon as it is right now, the moment you scrolled here. The same code paints AWQAT’s night sky.
Learn mode
Tap any prayer card, and the clock becomes a teacher.
Every prayer opens into a step-by-step walkthrough — Maghrib unfolds into 29 steps across its three rak’ah. A pixel figure demonstrates each posture while the recitation appears one line at a time: Arabic, transliteration, meaning. Narration plays per line — real committed Qur’an recordings first, generated dhikr clips second, the device’s Arabic voice as a last resort — highlighting the active card and turning pages by itself. A single incrementing token guards every audio callback, so pause, skip, and close cancel cleanly with no orphaned sound.


The calendar
Two calendars, one table — and a year you can see.
The Hijri calendar is arithmetic too: the tabular reckoning cycles 11 leap years through every 30, so Gregorian ↔ Hijri conversion is pure integer math — the same zero-dependency rule as the solar engine. AWQAT runs both side by side and charts the whole year’s daylight, so you can watch Fajr and Isha breathe with the seasons.


The evidence
Small enough to read. Tested like it matters.
The whole app is one sectioned module driving a static HTML shell. The seven test suites don’t mock it — they load the real module into jsdom and drive it end to end: full Learn-mode playback runs, pagination contracts, moon-phase math pinned to known new and full moons, sun-arc continuity across day boundaries. Any change to the DOM contract or the audio queue has to update the tests in the same commit.
The boundaries
What it deliberately defers to people.
Congregational times. Jama’ah and Jumu’ah are set by the local masjid, and the app says so on every view — it computes when prayer becomes due, not when the community gathers. The tabular Hijri calendar can differ a day or two from moon sighting, so every date wears an estimate label and a ±2-day adjustment lives in settings.
And the narration is a pronunciation aid, not a substitute for instruction — the Learn overlay itself tells you that proper recitation (tajwid) is learned from a teacher. The software knows the edge of its competence and points past it.
The point
Local-first isn’t a constraint. It’s a design position.
AWQAT went from a single-file prototype to a tested, shipping app in four feature commits — small enough that one person can hold the whole thing in their head, and boring enough, in the best sense, to still work untouched in ten years. The pattern generalizes: wherever the underlying data is deterministic — tides, seasons, sky events, timetables of any celestial kind — the computation belongs on the device, and the interface should be able to answer why.