The Problem: Most Tarot Apps Are Random
When I started building Luna Tarot — a web app that gives you a daily tarot reading tied to the actual moon phase — I faced a design decision that would shape the entire project.
Most tarot apps work like this: shuffle, deal, look up a card meaning from a table, display. The card you pull is random. The meaning never changes. There's no connection to anything external — no rhythm, no context.
I wanted something different. I wanted each reading to be grounded in something real: the actual phase of the moon.
So I needed a way to calculate the current moon phase — accurately, reliably, and without relying on third-party APIs.
The Solution: Astronomy Engine
I discovered** Astronomy Engine** — an open-source library for calculating positions of the Sun, Moon, and planets.
It's available as an npm package and supports both browser and Node.js environments. Here's why it stood out:
Accurate: Built on VSOP87 and NOVAS, validated against JPL Horizons data — precise to within ±1 arcminute.
Lightweight: The minified bundle is only ~120KB.
Fast: Moon-phase calculations run in under 5ms — zero impact on page load or UI responsiveness.
Here's what it can do:
Calculate the Moon's phase (expressed as an ecliptic longitude)
Find quarter moon phases (new, first quarter, full, third quarter)
Calculate rise, set, and culmination times
Predict lunar and solar eclipses
And much more
Why Zero API Calls?
Most developers would reach for a weather or astronomy API. Here's why I didn't:
Privacy by default: For a spiritual practice app, users shouldn't have to wonder if their reading data is being sent to some third-party service. By calculating everything locally, the app never makes any external requests — no API keys, no network calls, no tracking.
No geolocation needed: Moon phase and illumination percentage are global constants (unlike rise/set times, which depend on location). This means I never need to request the user's GPS data — eliminating another potential privacy leak at the source.
No costs, no outages: No rate limits, no paywalls, no CORS proxy servers, and zero dependency on third-party uptime.
How I Implemented It
Here's the core TypeScript implementation I use in Luna Tarot:
import * as Astronomy from 'astronomy-engine';
// Static metadata for each lunar phase
const moonPhaseInfo = {
new_moon: { nameKey: 'new-moon-name', descKey: 'new-moon-desc-full', icon: '🌑' },
waxing_crescent: { nameKey: 'waxing-crescent-name', descKey: 'waxing-crescent-desc-full', icon: '🌒' },
first_quarter: { nameKey: 'first-quarter-name', descKey: 'first-quarter-desc-full', icon: '🌓' },
waxing_gibbous: { nameKey: 'waxing-gibbous-name', descKey: 'waxing-gibbous-desc-full', icon: '🌔' },
full_moon: { nameKey: 'full-moon-name', descKey: 'full-moon-desc-full', icon: '🌕' },
waning_gibbous: { nameKey: 'waning-gibbous-name', descKey: 'waning-gibbous-desc-full', icon: '🌖' },
last_quarter: { nameKey: 'last-quarter-name', descKey: 'last-quarter-desc-full', icon: '🌗' },
waning_crescent: { nameKey: 'waning-crescent-name', descKey: 'waning-crescent-desc-full', icon: '🌘' },
};
type MoonPhaseType = keyof typeof moonPhaseInfo;
/**
* Calculate precise lunar phases using astronomy-engine
* phaseValue: 0-360 degrees, 0 = new moon, 180 = full moon
* illumination: percentage of moon surface visible from Earth
*/
function calculateMoonPhase(date: Date): { phase: MoonPhaseType; illumination: number } {
const phaseValue = Astronomy.MoonPhase(date);
// Standard illumination formula: (1 - cos(phaseAngle)) / 2
// 0° → 0%, 90° → 50%, 180° → 100%, 270° → 50%
const illumination = (1 - Math.cos(phaseValue * Math.PI / 180)) / 2 * 100;
// Map 0-360° to 8 discrete phases (45° each)
const phases: MoonPhaseType[] = [
'new_moon', 'waxing_crescent', 'first_quarter', 'waxing_gibbous',
'full_moon', 'waning_gibbous', 'last_quarter', 'waning_crescent'
];
const index = Math.floor(((phaseValue + 22.5) % 360) / 45);
const phase = phases[index] || 'new_moon';
return { phase, illumination: Math.round(illumination * 10) / 10 };
}
With just a few lines of code, I can determine exactly where the moon is in its cycle — and use that to change the tarot reading dynamically.
💡 Alternative approach: If you prefer explicit range checks for readability, the classic
if/else ifchain works perfectly too. The array-mapping version above is just a compact, functional alternative I ended up using.
The Result
Luna Tarot now delivers daily tarot readings that shift with the actual lunar cycle. The same card on a New Moon (beginnings, planting seeds) tells a different story than on a Full Moon (culmination, release).

Caption: The main dashboard blends daily tarot guidance with the current lunar energy — all computed client-side.

Caption: The calendar shows real-time phase, illumination percentage, and traditional almanac data, updated instantly for any date.
The full tech stack:
Next.js 14.2 + React 18 + TypeScript — frontend framework
Tailwind CSS — styling
astronomy-engine — moon phase calculations
lunar-javascript — Chinese lunar calendar conversion
Python + NumPy DSP — procedurally generated meditation music (pre‑rendered as audio assets)
Everything runs in the browser. No API calls. No server dependencies for the core functionality.
🧑💻 Development Note: I used GLM-5.2 as an AI coding assistant (similar to Copilot) during development for translating content and generating boilerplate. However, the production app itself makes zero external API calls — no AI models, no third-party services, no tracking.
Key Takeaways
You don't always need an API. For many types of data (astronomy, math, calendars), local calculation is not only possible but often better — faster, cheaper, and more reliable.
Open-source libraries are powerful. Astronomy Engine is a hidden gem — accurate, well-tested, and available in multiple languages.
Privacy is a feature. When you eliminate external calls, you eliminate data sharing concerns by default.
Start with the user experience. I chose local calculation not because it was easier, but because it created a better, more private experience for users.
Try It Yourself
Luna Tarot is live and completely free: https://www.lunatarotapp.com
No account required. Just you, the cards, and the moon. 🌙
If you're interested in the astronomy-engine library, check out the GitHub repository or the npm package.
📌 Suggested Tags for DEV.to:
nextjs, typescript, webdev, javascript, astronomy, opensource, tailwindcss, privacy, frontend, indiedev
📌 Discussion Prompt (Pin this in comments):
Have you built calendar, weather, or astronomy tools that avoid third-party APIs? What math libraries or local-computation tricks did you use? I'd love to compare approaches in the comments!
Top comments (0)