Astrology apps are one of India's quietest huge markets — panchang widgets, kundli generators, matrimonial matching, muhurta pickers. Under every one of them sits the same unforgiving requirement: the astronomy has to be exactly right, because your user's grandmother has a printed panchang on her wall and she will check.
I spent the last few months building GrahaAPI — 237 REST endpoints across 23 modules of Vedic astrology, Hindi + English in every response. This post isn't a feature tour. It's the four engineering problems I didn't expect, because I think they're interesting even if you never touch astrology.
First, 60 seconds of domain: what the computer actually calculates
Strip away the mysticism and Vedic astrology is a coordinate system plus 1,500 years of lookup tables:
- Tithi (the "lunar date"): the Moon-Sun angular separation, divided into 12° slices. 30 per lunar month.
- Nakshatra: which of 27 equal 13°20′ segments of the ecliptic the Moon occupies.
- Dasha: a 120-year planetary period cycle, seeded entirely by the Moon's exact position at birth — a birth-time error of minutes shifts period boundaries by months.
- The whole thing runs on the sidereal zodiac, offset from the tropical zodiac by ~24° (the ayanamsa — we use Lahiri, the Indian government standard).
So: an ephemeris gives you planetary longitudes, and everything else is careful classical bookkeeping. Which brings me to the first bug.
Bug #1: the thread-local zodiac
Our ephemeris core is a C library with Python bindings, and it holds "which zodiac mode are you in" as global state — per thread.
FastAPI runs sync endpoints on a threadpool. First request warms up thread A: sidereal mode set, positions correct. Then a request lands on freshly-spawned thread B: mode silently defaults to tropical, every longitude comes back ~24° off, and — because 24° is almost exactly one nakshatra-and-a-bit — the Moon lands in a plausible but wrong nakshatra. Which seeds the dasha. Which means the API happily returned dasha periods shifted by years, no exception, HTTP 200.
That's the nastiest class of bug: wrong answers that look right. The fix is boring — re-assert the sidereal mode at the top of every function that touches the ephemeris — but the lesson generalizes:
If you bind to a C library, find out where it keeps its state before you put it behind a threadpool.
grepfor anything that looks likeset_mode, and assume it's thread-local until proven otherwise.
Test fixtures written 1,500 years ago
How do you write regression tests for astrology? You steal your fixtures from the classics. Our accuracy suite encodes invariants no code change is allowed to break:
- Vimshottari dasha periods must sum to exactly 120 years — the canonical cycle length.
- Two identical birth charts run through 36-point marriage matching must score 28/36, not 36/36 — identical charts share a nadi, which is a dosha. (Great interview question for astrology-app devs, by the way.)
- Friday's first choghadiya period must be Char — the sequences are fixed tables, and any drift means an off-by-one in the day-slicing.
- The Ashtakavarga total for our reference chart must equal the classical published value, point for point.
Standing rule in the repo: if an accuracy test fails, the code is wrong — never the test. It's surprisingly clarifying to work on a system where the spec was frozen centuries before software existed.
My favorite bug this suite's philosophy caught: sunrise at 07:59:34 was displayed as 07:00. Classic — we rounded seconds into minutes (59.57 → 60) without carrying into the hour. Every "time of day" formatter you've ever written has flirted with this one.
The 429 that cosplayed as CORS
One morning, every free tool on our consumer site died at once. The browser console said:
Access to fetch at 'https://api...' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present.
CORS config? Untouched for weeks. curl from a terminal: everything 200. Classic "works in curl, dies in browser."
The real story: the site's API key had blown through its monthly quota, so the quota middleware was returning 429. But that middleware was registered outside the CORS middleware — its short-circuited response never passed through the layer that attaches Access-Control-Allow-Origin. A response without that header is, to a browser, indistinguishable from a CORS violation. The actual error — a perfectly informative bilingual 429 with reset dates — was invisible.
Two fixes, both worth stealing:
- Every early-return path in outer middleware must attach CORS headers itself. Your error responses need CORS more than your success responses — that's when the client is trying to tell the user what went wrong.
- When you're debugging "CORS errors" that appear out of nowhere: it's very often not CORS. It's an error response from something (a proxy, a rate limiter, a WAF) that sits outside your CORS layer.
Honesty as an API feature
The design decision I'd defend hardest: most astrology software silently assumes noon when the birth time is unknown, then confidently prints house placements that are pure fiction.
We made tob (time of birth) optional — and when it's missing, the response is computed from a sunrise chart plus an explicit accuracy object stating which parts are reliable (panchang-level facts) and which aren't (house placements, dasha boundaries). The client app decides what to show. Astrology output is probabilistic interpretation stacked on deterministic astronomy; the API's job is to keep the boundary between the two visible.
Same philosophy in quota handling: the free tier gets a clear 429 with its reset date; paid plans are throttled (0.5–2s), never blocked — an astrology app going down during Diwali because of a quota edge case is not an acceptable failure mode.
The test mode I wish every API had
The feature I'm proudest of has nothing to do with astrology. sk-test- keys are free, unlimited, no card — and instead of hand-written mock JSON, a test request is replayed through the real endpoint code against a fixed sample chart, then cached:
curl -X POST https://api.grahaapi.com/v1/kundli/full \
-H "Authorization: Bearer sk-test-anything" \
-H "Content-Type: application/json" \
-d '{"dob":"1990-06-15","tob":"10:30","lat":28.6139,"lon":77.209,"tz":5.5}'
Hand-written mocks drift — every team eventually ships a mock with a field the real endpoint renamed two sprints ago. Replayed-through-production mocks can't drift, because they aren't mocks. Every response is stamped meta.mode: "test" so nobody accidentally ships sample data. New endpoints get test mode for free, automatically.
You can integrate all 237 endpoints before paying a rupee, and the structure is guaranteed identical to production.
If you want to poke at it
- Docs for every endpoint: grahaapi.com/docs
- Node SDK: github.com/Vaigo/grahaapi-node
import GrahaAPI from "grahaapi";
const graha = new GrahaAPI("sk-test-hello");
const rk = await graha.rahuKaal({ date: "2026-08-30", lat: 19.076, lon: 72.8777, tz: 5.5 });
// → today's Rahu Kaal for Mumbai, from the city's actual sunrise
Free tier is 1,000 calls/month — enough for a real side project. If you're building anything astrology-adjacent for the Indian market (or you've fought your own thread-local C library), I'd genuinely love to hear about it in the comments.
Top comments (0)