The Shop at the End of the Galaxy is a marketplace where you browse, collect, and “buy” real near-Earth asteroids. Every asteroid in the shop is a real object from NASA’s Near-Earth Object API, with a price computed from its actual physical and orbital properties, and a procedurally generated visual that is unique and stable per asteroid. The name — and the whimsy — is borrowed from The Hitchhiker’s Guide to the Galaxy.
Built by a five-person team over three months in the DH2643 Advanced Interaction Programming course at KTH: Next.js frontend, Express + GraphQL backend, MongoDB — self-hosted with Docker on my own home server, behind Caddy and Cloudflare.
What It Does
Users log in with Firebase auth, claim daily CosmoCoin rewards, browse and filter the asteroid catalogue, add asteroids to a cart and check out, and show off their collection on a profile page — including a Galaxy view where owned asteroids orbit an animated planet. A WebSocket layer even shows how many other users are currently eyeing the same asteroid.
What I Did
I set up the repository and was mainly responsible for the architectural layers, while teammates built major features on top (Antonio: asteroid SVG generation, WebSocket live viewers, home page; Sara: shop filters and follow system; Hyosun: profile pages; Maja: cart and checkout UI). My areas:
- Backend architecture — restructured Express into a layered
config / graphql / loaders / middlewares / models / servicesdesign, and set up GraphQL schema auto-merging from per-domain.graphqlfiles so four teammates could add resolvers in parallel without conflicts. - Authentication — Firebase login exchanged for a server-side session cookie (more below).
- NASA data layer — a caching and pricing service that turns the raw NeoWs API into a queryable product catalogue.
- Domain logic — user/social/ownership GraphQL models and the checkout transaction: the server re-validates ownership, recomputes the total price, and checks the balance rather than trusting anything from the client.
- Frontend architecture — the Zustand-based MVVM structure the whole app follows.
- Galaxy orbit visualization — the animated, draggable orbit view of your collection, driven by real orbital data.
- DevOps — Docker Compose environments, Caddy reverse proxy, and a GitHub Actions CI/CD pipeline on a self-hosted runner.
Some Technical Details
Authentication. The first version sent the Firebase ID token with every GraphQL request, so the backend verified a token signature on every call. I changed this to verify the token once at login and exchange it for an httpOnly express-session cookie, which the GraphQL context reads afterwards. One bug worth noting: on the deployed site, login didn’t stick because Cloudflare terminated HTTPS and the Caddy-to-Express hop was plain HTTP — Express saw an insecure connection and wouldn’t set a secure cookie. Fixed by forwarding X-Forwarded-Proto in Caddy and enabling trust proxy in Express.
Pricing. Each asteroid’s CosmoCoin price comes from a weighted model over its NASA data: diameter, brightness, orbit class, inclination, miss distance, and a factor that decays with time until its next close approach. The model includes a small randomness term, but since a cron job rebuilds the catalogue nightly, Math.random() would reprice asteroids every day — including ones in someone’s cart. So the randomness is generated by a mulberry32 PRNG seeded from a hash of the asteroid’s NASA reference ID, making it stable per asteroid across rebuilds.
Frontend architecture. With four people adding to one Zustand store, AppModel.ts grew to over 900 lines mixing type definitions, GraphQL documents, fetch logic, and utility functions. I refactored it (without behavior changes) into separate layers: a pure-types model, a ViewModel, a persistence layer, GraphQL documents, and utility functions. Later commits from teammates followed the same structure.
Galaxy view. The orbit visualization initially used evenly spaced circles with arbitrary speeds. I reworked it to derive orbit radius from each asteroid’s miss distance and angular speed from its relative velocity, normalized across the user’s collection. NASA data has missing fields and inconsistent types, so each mapping falls back to a hash of the asteroid’s ID when data is unavailable, which also keeps asteroids from overlapping. The view supports drag-to-spin on mouse and touch, using a small movement threshold to tell a drag apart from a tap that opens the detail modal.
Tech Stack
| Layer | Technology |
|---|---|
| Frontend | Next.js 15 (App Router), TypeScript, Tailwind CSS 4, Zustand (MVVM), Apollo Client |
| Backend | Node.js, Express, Apollo Server (GraphQL), Mongoose + MongoDB 7, ws (WebSocket), Firebase Admin SDK |
| Data | NASA NeoWs API, node-cron nightly cache refresh |
| Infrastructure | Docker Compose, Caddy, GitHub Actions (self-hosted runner), Cloudflare |
Reflection
This was the project where I got to work across a full stack: deciding how the layers fit together, and keeping the deployment running while four other people shipped features on top of it. The CI/CD setup brought its own share of practical friction — one cleanup step failed because files created inside a container were owned by root and the runner user couldn’t delete them; the workaround was a disposable Alpine container that removes them.
There are things I’d do differently with more time: the ownership lookup is an N+1 query that should be a batched $in aggregation, and pagination happens in memory because we sort on nested approach-data fields. Both were conscious trade-offs under course deadlines, and they’d be the first things to fix if the project went further.