Oshun Platform · Features

Nyx — Sky Events and Perspective

A focused page within the Oshun Platform Features documentation. The full map and every sibling page live in the Features hub.

9sections17 minread5tables

On this page

Nyx is V1's sky, awe, calendar, event, and cosmic-perspective domain — the place a member opens to find out what is happening overhead tonight, whether it is worth going outside, what it means, and how to keep a record of what they saw. It serves the curious and the contemplative alike: a casual stargazer checking the next meteor shower, an enthusiast logging an aurora through binoculars, or someone who wants a Tara perspective meditation tied to the night sky. Nyx sits among the six customer-facing domains (Tara, Arete, Veritas, Nyx, Nisaba, Metis) hubbed at ../features.md. It is unusual among them in carrying a genuine astronomical compute core rather than a thin CRUD layer — see the companion architecture deep-dive Customer Domains.

This page is candid about what is real versus what is seam-mocked. The astronomy engine and the contract model are unambiguously real, in-repo, and test-pinned to known-correct published values. The live external data-source ingestion (actual NASA/IMO/NOAA fetches), the OAuth two-way calendar-provider sync, and cross-device observation sync are honestly partial / boundary-mocked / fail-loud, not passed off as shipped. Where a thing is planned or provider-gated, this page says so.

What Nyx is, at a glance#

Nyx covers five surfaces of one experience:

  • A computed "tonight" core. A real Meeus-based ephemeris computes the Sun and Moon positions, the lunar illuminated fraction and phase, sidereal time, rise/transit/set, and observer-relative altitude/azimuth — entirely in-repo, with no external service dependency for the core figures.
  • A canonical event model. Eleven event families and twenty-seven event types (meteor showers, eclipses, conjunctions, occultations, transits, comets, aurorae, satellite passes, supermoons, seasonal markers, deep-sky peaks), each with a family-specific detail schema, observation windows, and prediction-source provenance.
  • An observe-loop. Forecasts say what should be visible; the LoggedObservation contract lets a member record what they actually saw — site, conditions, equipment, quality, attachments, sharing scope.
  • Calendar, save, follow, remind. Per-event calendar entries with location-aware peak times, reminder cadences, and provider sync state.
  • Cross-domain companions. Tara perspective practices, Nisaba cosmology overlays, Veritas grounded explainers, assistant evening narration, and Living Scenes "Sky Briefing" tonight-only briefings.

The canonical contracts live in libs/contracts/src/nyx/index.ts, re-exported from the contracts package barrel via export * from './nyx' (libs/contracts/src/index.ts, "Nyx V1 canonical contracts"). The compute and adapter logic lives in libs/oshun/domain-nyx/src, the BFF route prefix is /v1/nyx/* (apps/oshun/bff/src/routes/nyx.ts), and the web surfaces are under apps/oshun/web/src/app/nyx and .../app/domains/nyx.

Surfaces#

Consumer hub and first-class consumer routes#

The consumer hub is at /nyx (the Lilith-design-system NyxRoom). The real web tree, however, is richer than a single hub: alongside /nyx there are first-class consumer routes that the older feature copy did not enumerate. apps/oshun/web/src/app/nyx/ contains, in addition to page.tsx (the room):

Route Purpose
/nyx/events Upcoming sky-events list
/nyx/observation Compact observation companion
/nyx/sky-almanac The almanac surface driven by the depth modules
/nyx/tonight The computed "tonight" view (Sun/Moon, phase, rise/set)

These are consumer surfaces, not power-user tools, and they belong in any honest description of the Nyx surface area.

Power-user deep tools#

The deep tools live under apps/oshun/web/src/app/domains/nyx/. The full set: analysis, catalogs, coordinates, events, events/[eventId], learning, moon, neo, observation-log, observation-log-deep, renderer, satellites, sky-conditions, solar, solar-system, sonification, star-chart, telescope, time-travel, and widgets. The [eventId] dynamic route is the per-event detail page; observation-log / observation-log-deep back the observe-loop; renderer / star-chart / solar-system are visual surfaces; time-travel and sonification are perspective tools.

A note on mobile#

Earlier architecture copy listed "mobile event cards" as a dedicated Nyx surface. The code does not bear that out: apps/oshun/mobile/src has no nyx/ screen directory. Nyx appears on mobile only as cross-domain companion components — for example TaraNyxPerspectiveCompanionCard.tsx — rather than as a standalone Nyx mobile vertical. Treating "mobile event cards" as a shipped Nyx surface tree is stale; the honest statement is that Nyx's mobile presence is currently a companion card embedded in other domains.

The ephemeris engine — the real astronomy core#

The most impressive real asset in Nyx is libs/oshun/domain-nyx/src/ephemeris.ts (~19.7 KB), a self-contained astronomical computation module implementing the standard algorithms from Jean Meeus, Astronomical Algorithms (2nd ed.). Its own header is explicit about why it exists: an audit found that nyx/tonight had been served by a static fixture and the web layer fabricated sky data on backend failure. This module computes the real positions instead, so the "tonight" surface is grounded in arithmetic rather than a hand-built payload.

Exported functions#

Symbol What it computes Meeus reference
toJulianDay(date) Julian Day for a UTC instant ch. 7 (Gregorian)
julianCenturies(jd) Julian centuries from epoch J2000.0
sunPosition(jd) Apparent geocentric solar position (RA/dec, ecliptic λ, distance in AU) ch. 25, abridged
moonPosition(jd) Apparent geocentric lunar position (principal periodic terms) ch. 47
moonIllumination(jd) Phase angle, illuminated fraction, elongation, phaseName ch. 48
computeNightSky(date) Location-independent sky core (JD, Sun, Moon, and illumination)
greenwichMeanSiderealTime(jd) GMST in degrees eq. 12.4
localSiderealTime(jd, longitudeEast) LST for an east-positive longitude ch. 12
equatorialToHorizontal(pos, jd, lat, lonEast) RA/dec → altitude/azimuth (from north, eastward) ch. 13
riseTransitSet(pos, date, lat, lonEast, h0) Rise / transit / set plus circumpolar flags ch. 15
computeTonight(date, location) Full observer-relative "tonight" (alt/az and rise/set for Sun and Moon)

The module also exports the STANDARD_ALTITUDE constant — the apparent disk-center altitudes used for rise/set: sun: -0.8333 (−0°50′, refraction plus semidiameter), moon: 0.125 (a mean value incorporating horizontal parallax), and star: -0.5667 (−0°34′ refraction only).

Why it is built this way#

A few design decisions are worth naming because they are deliberate and correctness-bearing:

  • Longitude is east-positive throughout, the standard geographic/API convention. Meeus's formulae are west-positive, so the observer-relative conversions invert the sign — a single consistent convention that the tests pin against Meeus's worked examples.
  • The lunar series keeps the principal (largest-amplitude) terms — good to a few arc-minutes, "ample for a naked-eye 'tonight' surface." This is a documented approximation with its tradeoff written down, not a stub: the solar terms reproduce Meeus's worked example to ~0.001°.
  • riseTransitSet uses the sidereal rate, not 360°/day. The code comment explains the subtlety: the target degree difference must be reduced mod 360 before dividing by SIDEREAL_RATE = 360.98564736629, because wrapping the day fraction instead would shift GMST by the rate and desync from the full GMST formula. It returns alwaysUp / alwaysDown flags for circumpolar and never-rising bodies.

Known-answer tests (this is not a stub)#

libs/oshun/domain-nyx/src/ephemeris.test.ts pins every routine to Meeus's published worked examples, so any coefficient-transcription error fails loudly:

  • toJulianDay(2000-01-01T12:00Z) is exactly 2451545.0 (the J2000.0 epoch), and Meeus example 7.a (Sputnik 1, 1957-10-04.81 UT) is 2436116.31.
  • sunPosition for 1992-10-13 reproduces ecliptic longitude ≈199.909°, declination ≈−7.78507°, distance ≈0.9976 AU (Meeus example 25.b).
  • moonPosition for 1992-04-12 reproduces ecliptic longitude ≈133.1627° (within 0.02°) and distance ≈368409.7 km (within 120 km) (example 47.a).
  • moonIllumination for the same instant gives illuminated fraction ≈0.6786 (Meeus example 48.a), and reports phaseName === 'new'/'full' at known new and full moons.
  • greenwichMeanSiderealTime matches example 12.a (≈197.693195°), and equatorialToHorizontal reproduces example 13.b (Venus from Washington: altitude ≈15.12°, azimuth ≈248.03° from north).
  • riseTransitSet is self-consistency-tested: a stationary body must sit at h0 at its own computed rise and set, and circumpolar/never-rising cases are asserted.

These are real domain-correctness tests against published values; they would fail on random or hardcoded returns.

The Sky Almanac depth modules#

Under libs/oshun/domain-nyx/src/depth/ there are 41 files — twenty depth modules, their tests, and the depth-page builder — that power the /domains/nyx/* deep tools and the /nyx/sky-almanac surface. Each module is a focused astronomical computation that composes the ephemeris core. They were entirely unmentioned in the older feature copy.

Module Notable real exports
lunar-phase-calendar PRINCIPAL_PHASE_ELONGATION, findNextPhaseInstant, lunarPhaseCalendar
twilight-schedule TWILIGHT_ALTITUDE, TwilightWindow, twilightSchedule
twilight-phase-now current twilight band for an observer
astronomical-night astronomicalNight (the truly-dark window)
equation-of-time equationOfTimeMinutes, equationOfTime
solar-noon solarNoon
solar-terms SOLAR_TERMS, solarTerms (the 24 traditional terms)
solar-season-markers equinox/solstice instants
daylight-extremes daylightExtremes (longest/shortest day)
zodiac-position ZODIAC_SIGNS, zodiacPosition, sunZodiacPosition
planetary-hours CHALDEAN_ORDER, WEEKDAY_RULER, planetaryHours
lunar-nodes findNextLunarNode, lunarNodes
moon-distance MOON_MEAN_RADIUS_KM, apparentLunarDiameterArcmin, findNextLunarApsis
moon-phase-detail detailed phase readout
sun-distance Earth–Sun distance over time
chart-angles meanObliquityDeg, chartAngles
object-transit transit of an arbitrary catalog object
horizon-point / solar-horizon-points / lunar-horizon-points horizon azimuths for rise/set

The depth-page module (depth-page.ts) assembles these into a presentational structure — NyxDepthRow, NyxDepthSection, NyxDepthPage, and the buildNyxSkyAlmanacPage builder — which is what the almanac surface renders. The existence of a dedicated .test.ts next to every module is the diagnostic that these are real computations, not placeholders.

The Nyx event and observation model#

The event model is the richest contract in the repo. It is built from a family/type taxonomy, per-family detail schemas, observation windows, prediction sources, calendar sync, and the logged-observation loop — all with cross-field superRefine validation enforcing domain correctness.

Event taxonomy: 11 families, 27 types#

SkyEventFamilySchema is an 11-value enum:

text
meteor-shower · eclipse · conjunction · occultation · transit · comet
aurora · satellite-pass · supermoon · seasonal-marker · deep-sky-peak

SkyEventTypeSchema refines this into 27 concrete types, including meteor-shower-peak; the eclipse subtypes solar-eclipse-{partial,total,annular} and lunar-eclipse-{partial,total,penumbral}; conjunction-planet-{planet,moon,star}; lunar/planetary/asteroid-occultation; mercury/venus/exoplanet-transit; comet-apparition; aurora-forecast; iss-pass and bright-satellite-pass; supermoon; the seasonal markers march-equinox, september-equinox, june-solstice, december-solstice, cross-quarter; and deep-sky-peak. A getSkyEventFamily(type) helper maps every type to its family, and SkyEvent's superRefine enforces that the declared family matches the type's expected family.

Per-family detail schemas#

Each family carries its own detail block (SkyEventDetailsSchema is a record of nullable per-family details, and requiredDetailsKeyByFamily enforces that the matching block is present). These are far richer than a flat "type and peak time" bullet:

Family Detail schema Representative real fields
meteor-shower MeteorShowerEventDetailsSchema iauCode (regex /^[A-Z0-9]{3}$/), peakZenithalHourlyRate (int 1–1000), radiant (an AngularCoordinate), parentBody, activeStart/EndDate
eclipse EclipseEventDetailsSchema body (solar/lunar), subtype, sarosSeries, magnitude (≤2), gamma (−2..2), contactTimes (first/second/maximum/third/fourth)
conjunction ConjunctionEventDetailsSchema angularSeparationDegrees (0–10), elongationFromSunDegrees
occultation OccultationEventDetailsSchema occultingBody/occultedBody, disappearance/reappearanceTimeUtc, maxDurationSeconds
transit TransitEventDetailsSchema kind (mercury/venus/exoplanet), ingress/maximum/egress contactTimes
comet CometEventDetailsSchema designation, perihelionDate, predictedMagnitude, ephemerisObjectId
aurora AuroraEventDetailsSchema hemispheres (north/south), kpIndexMin/Max (0–9), geomagneticStormLevel (g1g5), validForecastHours (≤168)
satellite-pass SatellitePassEventDetailsSchema spacecraft (iss/bright-satellite), noradId, maxElevationDegrees, passStart/EndAt
supermoon SupermoonEventDetailsSchema perigeeDistanceKm (≤500000), illuminationPercent, fullMoonAt
seasonal-marker SeasonalMarkerEventDetailsSchema marker, hemisphereContext, traditionalName
deep-sky-peak DeepSkyPeakEventDetailsSchema objectType (galaxy/nebula/open-cluster/globular-cluster/asterism), constellation, catalogRefs, recommendedApertureMm

Beyond the family-match rule, validateSkyEventSubtypeDetails enforces finer-grained consistency: a solar-eclipse-* type requires eclipse.body === 'solar', an iss-pass requires satellitePass.spacecraft === 'iss', and a *-equinox/*-solstice type requires the seasonal-marker's marker to equal the event type.

The SkyEvent envelope#

SkyEventSchema ties it together: id, slug, type, family, title, summary, peakTimeUtc, windowStartUtc/windowEndUtc, locationDependent, visibilityScope (global/regional/local/not-visible), confidenceBand (high/medium/low/stale/experimental), an array of bodies (CelestialBodyRef), predictionSources, observationWindows, the per-family details, and related-content arrays linking the concept graph, Tara content, and Nisaba content. Its superRefine enforces timestamp ordering (windowStart ≤ peak ≤ windowEnd), uniqueness of body/source/window ids, window eventId consistency, and that each window's sourceRefIds actually reference the event's predictionSources.

Observation windows and the quality band#

ObservationWindowSchema is the per-region visibility forecast for an event. It carries the site (latitude, longitude, elevation, timezone, region, geohash), startAt/peakAt/endAt, a qualityBand (excellent/good/fair/poor/not-visible), a visibility block (visible flag, limiting reason, peak altitude/azimuth, apparent magnitude), recommendedEquipment, and a qualityInputs block: bortleClass (1–9), astronomicalTwilight band, moonIlluminationPercent, cloudCoverPercent, precipitationProbabilityPercent, windKph, seeing, transparency, and weatherCondition.

The quality band is not a free label — it is validated. The superRefine enforces that not-visible windows set visibility.visible === false and carry a limitingReason, that an invisible window must use the not-visible band, and critically that an excellent window requires dark enough skies: bortleClass <= 4, cloudCoverPercent <= 25, and an astronomicalTwilight of astronomical-twilight or night. This is a real domain rule — you cannot label a bright, cloudy, twilit sky "excellent."

jsonc
// excerpt of an ObservationWindow's qualityInputs that would PASS as "excellent"
{
  "qualityBand": "excellent",
  "qualityInputs": {
    "bortleClass": 3, // ≤ 4 required
    "cloudCoverPercent": 10, // ≤ 25 required
    "astronomicalTwilight": "night", // must be astronomical-twilight or night
  },
}

Prediction sources and the family-support matrix#

PredictionSourceKindSchema has 11 kinds — five more than older copy listed. Each PredictionSourceRef carries kind, displayName, uri, version, a license block (label/uri/attribution), a freshness block (issuedAt/validThrough/maxAgeHours/status of current/stale/superseded), a confidenceContribution, and a fetchedAt — i.e. version, license, attribution, and freshness, exactly what a provenance contract should carry. Its superRefine rejects a validThrough that is not after issuedAt.

The 11 kinds and the families each may back (predictionSourceFamilySupport):

Source kind Backs families
nasa-jpl-horizons eclipse, conjunction, occultation, transit, comet, supermoon, seasonal-marker, deep-sky-peak
nasa-jpl-small-body-db comet, occultation
imo-meteor-calendar meteor-shower
iers-bulletin-a eclipse, occultation, transit, seasonal-marker
noaa-swpc-geomagnetic-forecast aurora
weather-provider (none — augments windows, never backs an event)
bortle-light-pollution-atlas (none — augments windows)
tle-provider satellite-pass
minor-planet-center comet, occultation
usno-astronomical-applications eclipse, supermoon, seasonal-marker
gaia-catalog occultation, deep-sky-peak

This matrix is enforced: SkyEvent.superRefine requires that at least one prediction source supports the event's family (predictionSourceSupportsEventType), so an aurora event with only a weather provider attached is a validation error. The five sources older copy omitted are nasa-jpl-small-body-db, tle-provider, minor-planet-center, usno-astronomical-applications, and gaia-catalog.

The observe-loop: LoggedObservation#

Forecasts predict; LoggedObservationSchema records reality. Its module comment states the intent plainly: "SkyEvent is the predicted phenomenon. ObservationWindow is the visibility forecast for a region. LoggedObservation closes the loop: the user reports what they actually saw … so that future forecasts can incorporate ground-truth and so the user's lifetime observation journal stays auditable."

A LoggedObservation carries eventId (nullable — you can log an unscheduled sight), eventType, userId, observedAt, durationMinutes, the site, a skyCondition (LoggedObservationSkyConditionSchema: bortle, seeing, transparency, weather, cloud %, moon-phase %), equipment (LoggedObservationEquipmentSchema: instrument kind, aperture, magnification, exposure, notes), a qualityRating (the same quality band), a visibilityScope, free-text notes, attachments (photo/sketch/audio-note/video), a sharedScope of private/household/public, and a sourceClient (mobile-app/web-app/desktop-app/api). Its superRefine even catches an inconsistency: a not-visible rating with clear-sky cloud cover (< 20%) is rejected.

This contract is wired to the web observe-loop surfaces (/domains/nyx/observation-log and observation-log-deep, backed by NyxObservationLog.tsx). Per the run notes (v1-real-infra-run-2026-06-22.md §8), local persistence is done via useNyxStore over localStorage['oshun.nyx'], rendering rows tagged with data-nyx-observation-event-id. What remains is the honest gap below.

Calendar integration: providers, cadences, sync state#

CalendarProviderSchema is ['google', 'apple', 'outlook', 'ics-file'] — note the ics-file provider, which older copy omitted. It carries its own special case: the CalendarSyncEntry.superRefine requires externalCalendarId for every provider except ics-file (an ICS download has no external calendar id). ReminderCadenceSchema is ['peak-only', 'evening-of', 'week-before', 'none'].

A CalendarSyncEntry models the per-event calendar row: provider, external/provider event ids, a syncState (pending/synced/conflict/disabled/deleted), location-aware peak time (peakTimeUtc, peakTimeLocal, and an ObservationSite location), a syncDirection (oshun-to-provider/provider-to-oshun/two-way), reminders, and conflict reason. Its superRefine enforces that a synced entry has a providerEventId and lastSyncedAt, that a conflict entry has a conflictReason, and that reminderCadence === 'none' cannot carry reminders (and a non-none cadence must carry at least one). Each CalendarReminder has a cadence (excluding none), a channel (push/email/sms/external-calendar), an offsetMinutesBeforePeak, and an enabled flag.

How the data flows#

The BFF exposes the Nyx read/launch surface under /v1/nyx/* (apps/oshun/bff/src/routes/nyx.ts):

  • The domain-adapter family at /v1/nyx/adapter/*: capabilities, availability, home-cards, continue-items, search, launch, nightly-highlights, object-search, saved-objects, event-reminders, observation-logs, and bridge-moments.
  • /v1/nyx/event-actions for save/follow/remind state, and /v1/nyx/perspective/home for the home perspective section.

Supporting modules sit beside the route (note these live under apps/oshun/bff/src/ subdirectories, not all under routes/): the per-user store apps/oshun/bff/src/nyx/nyx-member-stores.ts, the reminder delivery bridge apps/oshun/bff/src/reminders/nyx-reminder-bridge.ts, the read adapters apps/oshun/bff/src/adapters/nyx-read-adapters.ts, and the 3D/scene route apps/oshun/bff/src/isis/nyx-3d-route.ts.

The reminder bridge is a good example of honest scoping. Its header makes clear it does not create reminders — Nyx's own subsystem does that (getEventReminders/setEventReminder/toggleEventReminder). The bridge delivers the ones a user already opted into, through the BFF's in-app reminder pipeline. Delivery is deduped by a stable per-(user, reminder) session id (nyx-event:${userId}:${reminder.reminderId}), so re-emitting every tick is a no-op. It is fail-soft per user: a Nyx read that throws skips that user and never aborts the sweep. In-app delivery is explicitly additive to Nyx's own push/email/sms/calendar channels.

A separate, sizeable libs/nyx library tree (~30 subdirectories incl. catalogs/, constellations/, coordinates/, orbital/, positional/, realtime/, renderer/, sky-clock/, time-travel/, and client-python/) holds deeper astronomy machinery. Its libs/nyx/ephemeris package (cache, generator, minor-bodies, visibility) is the @nyx ephemeris consumed elsewhere in the platform — e.g. V9's libs/v9/hephaestus/src/nyx-sky-explorable.ts and libs/v9/aletheia/src/kernel-evaluators.ts.

Cross-domain companions#

Nyx is a hub for cosmic perspective, so it leans on its neighbors:

  • Tara perspective recommendations — awe practices, perspective meditations, and night-sky-themed reflections (the mobile TaraNyxPerspectiveCompanionCard is the embedded surface). See Tara — Rituals and Contemplative Practice.
  • Nisaba cosmology overlays — cultural cosmology references and cross-tradition mappings (e.g. the Pleiades across cultures), via the nisaba-cosmology-overlay module. See Nisaba — Scholarly Study.
  • Veritas explainers — grounded explanations of phenomena with source-set citations and retracted-source handling. See Veritas — Grounded Stories and Claims.
  • Assistant explainers — voice-mode evening narration for tonight's sky with location-aware visibility framing and accessibility audio description. See Assistant Experience.
  • Concept-graph linking — phenomena ↔ named events ↔ teacher references ↔ practice prescriptions, threaded through the relatedConceptGraphNodeIds, relatedTaraContentIds, and relatedNisabaContentIds arrays on SkyEvent.
  • Living Scenes integration — Nyx ships Sky Briefing Living Scenes: tonight-only briefings tied to the celestial-event clock, with Northern/Southern hemisphere parity. The score's camera/visual focus advances across visible objects as the narrator names them, time-aligned to ±60 ms. Observer location is opt-in per session; the briefing materializes once per location-bucket per night. See Living Scenes — Concept and Customer Promise.

Real vs. planned — the honest state#

This matters, and the docs are candid about it elsewhere; this page keeps that candor.

Real, in-repo, test-pinned:

  • The ephemeris engine and its 41 depth modules — genuine Meeus-based computation with known-answer tests against published values. Not a stub by any diagnostic.
  • The contract model — the richest in the repo, with cross-field superRefine validation enforcing real astronomical and provenance rules.
  • Local observation persistence — useNyxStore over localStorage['oshun.nyx'].

Partial / boundary-mocked / fail-loud (per V1/AUDIT_2026-06-24.md):

  • nyx-event-calendar-sync-reminder is partial: two-way provider sync is mocked at the boundary, and reminder dispatch/delivery plus the observation-log back-link remain. Live calendar-provider OAuth sync is not fully wired.
  • nyx-tonight-observation is deep as of 2026-07-02: the browser journey now drives event-detail → observation-log handoff, local-first save, /v1/nyx/observations POST/GET read-back after local storage clear, Home Nyx support-card and current-week KPI increment, and the observation-log versus telescope-control equipment-profile boundary.
  • nyx-to-tara-bridge is URL-only telemetry at present.
  • Live external data-source ingestion — the actual NASA JPL / IMO / NOAA SWPC fetches — is seam-mocked / fail-loud rather than fully wired. The contracts model the provenance precisely; the live fetchers are planned work, not claimed as shipped.

In short: a real astronomy core and a real, validated contract model, with the external integrations honestly marked partial or planned. See Customer Domains for the architectural framing and ../features.md for the feature hub.