Tarot Lune: the engineering inside a tarot app

Why my tarot and astrology app has deterministic readings you can share as URLs, 218 prerendered routes, and a clean-room astronomy engine. A tour of the technical decisions behind Tarot Lune.


I shipped a tarot app. Tarot Lune is a bilingual (English and Spanish) tarot and astrology site: card readings with the classic 1909 deck illustrated by Pamela Colman Smith, a daily card, a birth chart, a moon calendar, lessons for learning the cards, a journal, and readings you can do together with another person.

Not the obvious project for a TypeScript developer, and that's exactly why it was fun. The category is dominated by sites that haven't changed in fifteen years and by AI chat wrappers behind paywalls. Building something genuinely well-crafted there turned out to involve more interesting engineering than I expected. Three decisions stand out.

Readings are deterministic URLs

A tarot reading in Tarot Lune isn't a row in a database. It's a seed. The URL encodes the seed, the spread, and your card picks, and the shuffle is a deterministic function of that seed. Open the same URL in ten years and you get exactly the same cards in the same positions.

That one decision bought a lot. Sharing a reading is just sharing a link, with nothing to store. There's no way for the app to quietly reshuffle toward an upsell, which matters in a category where trust is the whole product. And testing becomes trivial, because every reading is reproducible by construction.

Astronomy without the licensing trap

Birth charts need real planetary positions, and the standard way to get them is Swiss Ephemeris, which is dual-licensed: GPL or a paid commercial license. I wanted neither constraint, so the astrology package is clean-room: positions come from astronomy-engine (MIT), and the house systems and aspect math are implemented from published formulas.

Swiss Ephemeris still plays one role: a black-box test oracle. Its output feeds a set of golden tests that the clean-room implementation has to match within tolerance. You get the confidence of the reference implementation without inheriting a line of its code or its license.

SEO as an architecture decision

Tarot is a search-driven category, so the site is built for crawlers as much as for people. Everything is server-rendered with TanStack Start, and the build prerenders 218 routes to static HTML: every card meaning, every lesson, every landing page, in both languages. English lives at the root and every page has a Spanish twin under /es, with reciprocal hreflang so Google understands the pairing.

The heavy stuff stays out of the way of all that: three.js powers the card-drawing ritual but only loads on that route, so the pages that need to rank stay light.

I liked this URL scheme enough that this blog now uses the same one. If you want to see the app itself, draw a card. Even if tarot isn't your thing, the reading experience is the part I'm proudest of.