Skip to content
AFM Studio
Personal ProjectWeb AppAstronomy

Falak

Hijri calendar, prayer times, and qibla direction computed from real moon/sun astronomy — every verdict shows its numbers

All projects4 min read

Role

Fullstack Developer

Timeframe

Jul 2026

On this page

The problem

In Indonesia and much of the Muslim world, the start of Ramadan, Syawal, and Dzulhijjah is a recurring point of public confusion: different organizations sometimes announce different start dates because they use different hisab (calculation) criteria for whether the new crescent moon is visible. The government's official process combines calculation with physical moon-sighting reports, but the public rarely sees the underlying math — they just get an announcement. Existing hijri-date converters are either simple lookup tables with no real astronomy behind them, or official tools that give a verdict with no way to inspect why.

The approach

Astronomy computed from first principles, not fetched

Instead of calling a third-party ephemeris API, Falak implements a truncated VSOP87 solar-position model and an ELP2000/Meeus lunar-position model directly — it imports only math and datetime. This keeps the tool self-contained (no API keys, no rate limits, works offline) and, for a religious-calendar product, auditable: every number the app shows can be traced back to a formula in the repository, not a black box. Each conclusion — is the hilal visible, what time is Maghrib, what's the qibla bearing — is paired with a way to inspect the raw numbers that produced it (altitude, elongation, moon age, lag time) through a shared CalculationPanel, a direct answer to the "black box" complaint that motivated the project.

Three competing methods, shown side by side

Rather than picking one "correct" criterion for crescent visibility, the engine implements three real, competing methods — Wujudul Hilal, MABIMS-2021 imkanur rukyat, and the Odeh criterion — as independent pure functions, with a dedicated Method Divergence view showing exactly where they disagree. The Indonesia-wide visibility map evaluates the criteria across a 3,255-point 0.5° grid drawn over the real coastline; a visibility boundary cutting across Sumatra and Java on a partial evening is the clearest single image of the whole product — the same date is a different Hijri day depending on where you stand.

Validated before being trusted

The project's build order enforces a rule that no calculation feature ships without validation. Before any UI work began, the engine was cross-checked against Skyfield using the JPL DE440 ephemeris — a research-grade, independent source — for 50 historical lunar conjunctions, matching within a 5-minute tolerance. Skyfield is a dev/test-only dependency, never imported by the production path, preserving the "no third-party astronomy API at runtime" rule while still proving accuracy against one.

The whole product moved into the browser

Three earlier decisions made this possible: every endpoint was a pure function of its query parameters, the Postgres tables were a cache of deterministic output rather than a source of truth, and the engine had zero third-party dependencies. An app with no state, no I/O, and no dependencies doesn't need a server — so the astronomy engine was ported to TypeScript and the backend deleted from the request path. The result is a static site on GitHub Pages that costs nothing to run and can't break because a backend is down. The 3,255-point grid, once a Celery task, was re-sharded across Web Workers (one per core, capped at eight) — about 1.6s versus 6.3s single-threaded. The Python backend is retained as the validated oracle, not deployed.

Two engines, one source of truth — enforced mechanically

Porting a religious-calendar engine to a second language invites silent drift, so the Python implementation stays the oracle and the TypeScript port is pinned to it: a generator freezes the oracle's output for ~2,000 hand-chosen inputs, and a conformance suite replays them through the port. CI regenerates the fixture, re-checks it numerically, then runs conformance — and blocks the deploy on any failure. That discipline caught a real bug: one grid point off Merauke disagreed with the oracle by 1.6×10⁻⁷ degrees of moon altitude while 362 others matched exactly. The cause was representing instants as floating-point milliseconds — bisection drifts a fraction of a microsecond off the grid, and since sidereal time multiplies by 360.98 it re-emerged amplified as visible altitude error, right at the MABIMS 3° threshold where it could have flipped a verdict. Fixed by using integer microseconds, exactly as Python's datetime does. The bug was found precisely because the tolerance was strict enough to notice.

Outcome

Live at andifathulms.github.io/falak-visualizer at $0 hosting cost, deployed and verified in a real browser — all nine pages compute with no console errors, and the visibility map spawns eight Web Workers to render the full grid. 86 commits, solo: roughly three days from empty repo to the original feature set, then a later pass that moved the whole product into the browser and shipped it. The engine exists twice — 1,544 lines of validated Python and 2,313 lines of TypeScript across 15 modules — held in agreement by ~2,083 golden vectors, with 100 backend tests (including the JPL DE440 cross-check) and 51 frontend tests gating every deploy. Scope ran well past the PRD's MVP: Method Divergence, a Visibility Calendar, Isbat Accuracy tracking against real historical Kemenag records, and Rashdul Qibla sun-calibration — calibrate your qibla from a shadow, no instruments — were all deferred to "Phase 2" and shipped anyway.

Outcomes

Hosting — runs in the browser
$0
Golden vectors pinning both engines
~2,083
Engine ports, one oracle
2
Commits, single author
86

Screenshots

Beranda
Calendar Conversion
Hilal Visibility
Visibility Map
Prayer Times
Visibility Calendar

Have a project like this?

If you need a system built with the same care — clear scope, solid execution — let's talk.

Start a project