@aulunarcana/ephemeris
v0.2.3
Published
AuLun Ephemeris: NASA-grade astronomical ephemeris and chart engine (WASM). Free for non-commercial use; commercial licenses available.
Maintainers
Readme
@aulunarcana/ephemeris
Build professional astrology software without relying on external APIs or GPL-licensed ephemerides.
AuLun Ephemeris is a production-grade astronomical engine for Node.js, with a Rust core and WebAssembly bindings. One call computes a complete natal chart — bodies, eleven house systems, tropical and sidereal zodiacs, aspects, and pattern detection — plus standalone transit, equatorial, and heliocentric position APIs. Validated against JPL Horizons to sub-arcsecond accuracy across 1900–2100, with a Moon median error of 0.06″. Free for non-commercial use; commercial licenses available (see License below).
Install
npm install @aulunarcana/ephemerisNode 18+. Self-contained — all ephemeris data (~13 MB) is embedded.
Quick start — tropical zodiac, Placidus houses
import { Ephemeris, degreesToRadians, radiansToDegrees } from "@aulunarcana/ephemeris";
const engine = new Ephemeris(); // parses embedded data once — construct once, reuse
const chart = engine.computeChart(
{ year: 1990, month: 6, day: 15, hour: 21, minute: 30 }, // local civil time
"America/Los_Angeles", // IANA timezone
{ latitudeRad: degreesToRadians(34.05), longitudeRad: degreesToRadians(-118.24) },
{ houseSystem: "placidus" }, // tropical is the default zodiac
);
radiansToDegrees(chart.ascendantRad); // Ascendant
chart.bodies.sun.longitudeRad; // apparent ecliptic longitude
chart.bodies.moon.longitudeRateRadPerDay; // negative = retrograde
chart.houses.cuspsRad; // the 12 Placidus cusps
chart.aspects; // aspects with applying/separating
chart.patterns; // grand trines, T-squares, …All angles are radians; convert with radiansToDegrees /
degreesToRadians. The full result shape is in index.d.ts
(Chart).
Deploying — Next.js, React, bundlers
The published build targets Node (wasm-pack --target nodejs) and
loads its WASM binary at import time through
createRequire("./pkg/ephemeris_wasm.js") — a dynamic require that
bundlers and file tracers cannot follow statically. Plain node
scripts and servers need nothing special. Anything that bundles or
traces your server code does:
Next.js / Vercel
Mark the package as a server external:
// next.config.ts
const config: NextConfig = {
serverExternalPackages: ["@aulunarcana/ephemeris"],
};Without it, next build succeeds but production functions crash at
module load with MODULE_NOT_FOUND: the bundler inlines index.mjs
while the pkg/ WASM files it requires are left behind, and output
file tracing cannot see the dynamic require either.
serverExternalPackages keeps the package un-bundled, so the require
resolves against the real node_modules directory and the tracer
copies the whole package into the deployed function.
Two notes from a real production incident with this package:
outputFileTracingIncludesalone is not a reliable substitute — it can verify clean locally and on a preview deployment and still fail in production functions. UseserverExternalPackages.- The failure is at module load, after a green build. When you first deploy, request one route that actually computes a chart before calling it done.
Import the package only in server code — route handlers, Server
Components, server actions, crons — on the Node runtime. It cannot run
under export const runtime = "edge".
React
The package does not run in the browser: browser bundlers (webpack, Vite, esbuild) cannot bundle its Node-target WASM loading. Compute on the server and hand React plain data — chart output is JSON-serializable and crosses the server→client boundary as-is:
// app/chart/page.tsx — Server Component
import { Ephemeris, degreesToRadians } from "@aulunarcana/ephemeris";
export default async function ChartPage() {
const eph = new Ephemeris();
const chart = eph.computeChart(
{ year: 1990, month: 6, day: 15, hour: 21, minute: 30 },
"America/Los_Angeles",
{ latitudeRad: degreesToRadians(34.05), longitudeRad: degreesToRadians(-118.24) },
{ houseSystem: "placidus" },
);
return <ChartView chart={chart} />; // client component receives plain data
}The same shape applies to any SPA: put the ephemeris behind an API
route and fetch the JSON. An in-browser build (--target web) is not
currently published — open an issue if your use case needs one.
Other Node-side bundlers
Same rule, same reason — keep the package external:
- Vite SSR:
ssr: { external: ["@aulunarcana/ephemeris"] } - esbuild:
external: ["@aulunarcana/ephemeris"] - webpack (server):
externals: { "@aulunarcana/ephemeris": "commonjs @aulunarcana/ephemeris" }
What a chart contains
- Bodies (
chart.bodies):sun,moon,mercury,venus,mars,jupiter,saturn,uranus,neptune,pluto,chiron— each with apparent ecliptic longitude, latitude, distance, and daily motion (negative = retrograde). - Angles and houses: Ascendant, Midheaven, 12 house cusps, plus
vertexRadandeastPointRad(the prime-vertical angles — see below). - Lunar points: mean and true North/South Node and Lilith.
- Hermetic lots: Fortune, Spirit, Eros, Necessity, Courage,
Victory, Nemesis (day/night sect handled via
isDayChart). - Aspects: major and minor aspects with orb, exact angle, and applying/separating; plus detected patterns (grand trine, T-square, …). Orbs are configurable — see Orb policy below.
Vertex and East Point
Beside the Ascendant and Midheaven, every timed chart carries the two prime-vertical angles:
chart.vertexRad— the Vertex (western prime-vertical/ecliptic intersection).nullat the equator, where the prime vertical degenerates.chart.eastPointRad— the East Point (Equatorial Ascendant). Singularity-free, always present.
Both are radians, tropical true-ecliptic-of-date, like ascendantRad.
House systems
Pass one of these as houseSystem (default: "placidus"):
| Value | System |
|---|---|
| "placidus" | Placidus |
| "koch" | Koch |
| "whole_sign" | Whole Sign |
| "equal" | Equal |
| "porphyry" | Porphyry |
| "regiomontanus" | Regiomontanus |
| "campanus" | Campanus |
| "alcabitius" | Alcabitius |
| "topocentric" | Topocentric (Polich–Page) |
| "morinus" | Morinus |
| "meridian" | Meridian (Axial) |
Zodiac: tropical and sidereal (Vedic)
Every chart is computed in the tropical zodiac. To also get
sidereal values, pass an ayanamsa:
| Value | Tradition |
|---|---|
| "lahiri" | Vedic / Jyotish standard (Chitrapaksha) |
| "fagan_bradley" | Western sidereal |
| "krishnamurti" | Krishnamurti Paddhati (KP) |
| "raman" | B. V. Raman |
| "true_chitrapaksha" | True Spica pinned at 0° Libra exactly |
true_chitrapaksha and lahiri are both true-Spica-anchored in this
engine and return identical values — this crate's lahiri is the
true/Chitrapaksha variant, not the mean-precession construction some
reference software uses under that name.
const vedic = engine.computeChart(civil, zone, observer, {
houseSystem: "whole_sign",
ayanamsa: "lahiri",
});
vedic.bodies.moon.siderealLongitudeRad; // sidereal position
vedic.siderealHouses.cuspsRad; // sidereal cusps
vedic.ayanamsaRad; // the ayanamsa appliedSidereal output is additive — the tropical fields are always present.
Custom ayanamsa
For a sidereal zero point not in the table, pass ayanamsaCustom
instead of ayanamsa — a linear model in degrees: valueAtT0Deg
at Julian date t0JdTt (TT), advancing at ratePerCenturyDeg per
Julian century. It takes precedence over ayanamsa and reports
ayanamsaName === "custom".
const custom = engine.computeChart(civil, zone, observer, {
ayanamsaCustom: {
t0JdTt: 2451545.0, // J2000.0 (TT)
valueAtT0Deg: 23.85, // ayanamsa at t0
ratePerCenturyDeg: 1.4, // ≈ precession rate
},
});Orb policy
By default, aspects use fixed per-aspect orbs (conjunction 8°, sextile
6°, square/trine 7°, opposition 8°, minors 3°). Pass an orbPolicy to
choose a different scheme. Every field is optional and in degrees;
omitting orbPolicy keeps the defaults exactly.
| Field | Meaning |
|---|---|
| resolution | "perAspect" (default, fixed per-aspect orbs), "moietySum" (moiety(a)+moiety(b), + luminary bonus, on majors; flat orb on minors), or "moietyMax" (wider of the two body moieties on majors; per-aspect orb on minors) |
| moietiesDeg | Per-body moiety (half-orb), e.g. { sun: 4.5, moon: 4.5 } |
| defaultMoietyDeg | Moiety for bodies not in moietiesDeg |
| luminaries | Bodies earning the luminary bonus (moietySum) |
| luminaryBonusDeg | Extra orb on a major leg touching a luminary |
| minorFlatOrbDeg | Flat orb for minor aspects under moietySum |
| majorAspects | Which aspect names count as "major" |
| basesDeg | Per-aspect base-orb overrides, e.g. { conjunction: 10 } |
const chart = engine.computeChart(civil, zone, observer, {
orbPolicy: {
resolution: "moietySum",
moietiesDeg: { sun: 4.5, moon: 4.5, mercury: 3.5 },
defaultMoietyDeg: 3.5,
luminaries: ["sun", "moon"],
luminaryBonusDeg: 1.0,
minorFlatOrbDeg: 1.5,
},
});The policy governs both chart.aspects and the patterns built from
them. AspectHit.orbRad reports the resolved allowance actually used.
Extended patterns
Pass extendedPatterns: true to also populate chart.patternsExtended
with the full seven-figure set — the four default figures plus kite,
stellium, and minor grand trine — computed over the bodies, the
angles, and the true node. This is additive: chart.patterns (the
default four figures) is unchanged.
const chart = engine.computeChart(civil, zone, observer, {
extendedPatterns: true,
});
for (const p of chart.patternsExtended) {
p.pattern; // "kite" | "stellium" | "minor_grand_trine" | …
p.bodies; // members, apex-first where a figure has an apex
p.apex; // focal point (t_square/yod/minor_grand_trine) or null
p.sign; // zodiac sign for a stellium, else null
p.maxOrbRad; // loosest leg / widest spread, radians
}Extended minor bodies (optional)
Sixteen additional bodies are supported but not embedded: ceres,
pallas, juno, vesta, eris, sedna, haumea, makemake,
quaoar, orcus, varuna, ixion, gonggong, pholus, nessus,
chariklo. Load a body's data table first, then request it:
engine.loadMinorBodyTable("ceres", ceresTableText);
const chart = engine.computeChart(civil, zone, observer, {
includeMinorBodies: ["ceres"],
});A requested body whose table isn't loaded (or whose date is outside
table coverage) is listed in chart.outOfRange instead of failing
the chart.
Fast path: positions only
For transit scans and anything else that samples a few bodies across
many instants, computeBodies returns just the requested bodies'
apparent positions and daily rates — identical numbers to the full
chart, at a fraction of the cost (a full chart computes houses,
nodes, lots, aspects, and patterns you may not need):
const { bodies } = engine.computeBodies(
{ year: 2026, month: 1, day: 1, hour: 12, minute: 0 }, // UTC fields
["jupiter", "saturn", "mars"],
);
bodies.jupiter.longitudeRad; // apparent ecliptic longitude
bodies.jupiter.longitudeRateRadPerDay; // analytic rate (negative = retrograde)Equatorial and heliocentric positions
Two more position surfaces re-express the same apparent pipeline in other frames. Both take resolved UTC fields plus a body list, and route uranus/neptune/pluto through the DE441 tables just like the chart.
Equatorial — apparent RA/Dec of date plus astrometric ICRF/J2000:
const eq = engine.computeEquatorial(
{ year: 2026, month: 1, day: 1, hour: 12, minute: 0 },
["mars", "uranus"],
);
eq.bodies.mars.ofDate.rightAscensionRad; // apparent RA, equinox of date
eq.bodies.mars.ofDate.declinationRad; // apparent Dec of date
eq.bodies.mars.j2000.rightAscensionRad; // astrometric ICRF/J2000 RAHeliocentric — geometric ecliptic-of-date longitude, latitude, and radius. Defined only for Sun-orbiting bodies: requesting the Sun, Moon, or a lunar node/Lilith throws.
const helio = engine.computeHeliocentric(
{ year: 2026, month: 1, day: 1, hour: 12, minute: 0 },
["jupiter", "uranus"],
);
helio.bodies.jupiter.longitudeRad; // heliocentric ecliptic longitude
helio.bodies.jupiter.radiusAu; // heliocentric distanceFor both, a body outside its table coverage (or an unloaded minor) is
listed in outOfRange rather than failing the call.
Time handling
computeChart takes naive civil time plus an IANA zone and resolves
DST correctly:
- Ambiguous local times (DST fall-back) throw
AmbiguousLocalTimeErrorunless you passfold(0= earlier instant,1= later). - Nonexistent local times (spring-forward gap) always throw
NonexistentLocalTimeError.
If you already have UTC, use
engine.computeChartUtc(utc, observer, options) and skip timezone
resolution entirely.
Accuracy
Apparent positions are computed from VSOP87E, ELP/MPP02, and JPL-derived state vectors with full IAU 2000A/2006 precession- nutation, light-time, and relativistic aberration.
vs JPL Horizons, 1900–2100 (median error, arcseconds):
| Body | Median error | vs. the previous Moshier-derived engine AuLun ran on | |---|---|---| | Moon | 0.06″ | 67× more accurate | | Sun | 0.13″ | 5× more accurate | | Mercury / Venus | 0.13″ | 5× more accurate | | Mars | 0.12″ | 3× more accurate | | Jupiter / Saturn | 0.24–0.28″ | at parity (both already sub-arcsecond) | | Uranus / Neptune / Pluto | 0.02–0.13″ | at parity to 1.2× |
Every body ships sub-arcsecond median accuracy. Independently cross-checked against a second, widely-used commercial ephemeris implementation on identical ground truth: this engine lands within 1.0–1.3× of it for the Moon, the outer planets, Chiron, and Vesta — effectively at parity for the bodies that matter most in natal and transit work, with the largest remaining gap (2–4×, still sub-arcsecond in absolute terms) on the inner planets and main-belt asteroids.
Numbers above are generated by a committed benchmark harness against cached JPL Horizons reference data (41 epochs, 1900–2100, no hand-typed values) — full per-body breakdown across all 25 supported bodies available on request.
License
Proprietary. Free for non-commercial use — personal, educational, and research use is free of charge under the LICENSE shipped with this package. Use in or for anything sold, monetized, or operated for commercial advantage requires a commercial license: contact the copyright holder via https://github.com/AuLunArcana.
Changelog
See CHANGELOG.md, included in this package.
© 2026 AuLun Arcana LLC. All rights reserved.
