Next Steps
Objectives
By the end of this chapter, you should be able to:
- Recognize the pattern behind an entire class of enrichment features this course deliberately left out, and build one yourself without new architecture
- Explain the specific scaling limit Module 2 flagged and knew it wasn’t paying for, and what would actually trigger paying for it
- Take the finished app from
npm run devto something deployable
๐ก Why this matters: A course has to stop somewhere, and where it stops shouldn’t feel arbitrary. Every feature left out below was left out for a specific, stated reason โ not because it was hard, but because it wasn’t load-bearing for anything this course needed to teach. Each one reuses a pattern you’ve already built at least once.
Aircraft, Type, and Route Enrichment
The real SKYWATCH app enriches the detail panel with registration, manufacturer, and model (from hexdb.io), a type photo, and origin/destination for the current flight (from a route-lookup service) โ none of which this course built. That’s not an oversight; it’s the same “Postgres cache in front of a flaky third-party API” shape Module 5’s METAR weather badge already taught, applied to three more upstream sources instead of one:
- A route handler (
GET /api/acdb/:hex, in the real app) checks a Postgres cache table first, calls the upstream API on a miss, stores whatever comes back (including anotFoundflag for a confirmed-absent hex, so a bad lookup doesn’t get retried forever), and returns it. - A frontend hook does the same two-tier caching on the client side that
useAirportMetaralready does: an in-memoryMapfor the lifetime of the tab, falling back to the server route on a miss.
If you want to build this yourself, the two cache tables you’d add follow the exact shape airports and saved_locations already established in Module 1 โ a notFound: boolean column and a fetchedAt timestamp are the only genuinely new ideas, there to distinguish “never looked this up” from “looked it up, upstream has nothing,” which matters for the same reason home’s three-state undefined/null/object design mattered in Module 5: collapsing two different kinds of “nothing” into one falsy value causes real bugs.
The Scaling Limit Module 2 Already Named
Module 2’s realtime-layer lesson said this outright, and it’s worth returning to now that the whole app is built: positions travel over a plain in-process EventEmitter because it’s the simplest thing that works at this app’s actual scale โ one Node process running both the poller and the WebSocket server. That stops working the moment the poller and the API/WS server become two separate processes (for instance, scaling the WebSocket layer horizontally behind a load balancer, or running the poller as a separate worker so a WS server restart doesn’t interrupt polling) โ at that point, eventBus.emit("positions", ...) in the poller’s process is invisible to a WebSocket connection sitting in a different process.
The fix, if that need actually arrives, is a real pub/sub layer both processes can reach โ Redis is the obvious choice, and the shape barely changes: emitPositions() becomes a Redis PUBLISH, onPositions() becomes a SUBSCRIBE callback, and every call site in poller/loop.ts and ws/register.ts stays exactly where it is. Alerts already made this exact tradeoff โ they’re on real Postgres LISTEN/NOTIFY specifically because they needed to survive a process boundary an in-process emitter can’t cross, at a payload size positions can’t fit through. Don’t build the Redis version until something actually forces the process split; it’s a real cost paid for a scaling scenario, not a default to reach for early.
From npm run dev to Deployed
Nothing about this course’s Postgres setup (docker-compose.yml, one postgres service) is production-shaped โ it’s a local dev convenience. A few concrete next steps, in the order you’d actually hit them:
- Containerize both apps, not just Postgres.
apps/serverandapps/webeach need their ownDockerfileโ a multi-stage build (install,npm run build, copy only the production output into a slim final image) keeps the deployed image from carrying the wholenode_modulesdev tree. - Environment variables move from
.envto whatever your host provides โDATABASE_URLpointing at a managed Postgres instance instead of the local Compose container,WEB_ORIGINset to the real deployed frontend URL (Module 2’s CORS setup already reads this correctly, it just needs the right value),NEXT_PUBLIC_SERVER_URLset to the real deployed backend URL. - Migrations become a deploy step, not a local command โ
npm run db:migrateneeds to run against the production database as part of deploying a release, before the new server code that expects the new schema starts receiving traffic. - A process manager or platform-level restart policy for the poller โ it’s a long-running process with real consequences (missed poll cycles, a stale “last update” indicator) if it silently dies and nothing brings it back.
What This Course Didn’t Build, On Purpose
A few things were left out entirely, for reasons worth stating plainly rather than leaving implicit:
- No authentication, no multi-user support. SKYWATCH is a single-operator tool by design โ one set of watch regions, one home location, one set of saved locations, shared by anyone who can reach the server. Adding real multi-tenancy would mean threading a
userIdthrough nearly every table and repo function built across all six modules โ a genuinely different app, not an incremental feature. - No pagination on any list endpoint.
GET /api/alerts,/api/overflights, and friends all cap out at alimitquery param with a hard maximum, not real cursor-based pagination โ correct at the data volumes this app actually produces (a few hundred alerts a day, at most), and the first thing that would need to change if this app tracked airspace busy enough to produce thousands. - No rate limiting on the REST API. Everything here assumes a small number of trusted clients (yourself, maybe a few others on your network) โ a public-facing deployment would need it before going live.
Recap
- Every deliberately-omitted feature in this course reuses a pattern already taught at least once โ the aircraft-enrichment cache is the airport-weather cache with a different upstream; the Redis scaling path is the same emit/subscribe shape
events/bus.tsalready has, just crossing a process boundary alerts already learned to cross via Postgres. - The specific trigger for the positions pub/sub rewrite is the poller and the WebSocket server becoming two separate processes โ not “the app got popular,” a more precise condition than that.
- Multi-user support, pagination, and rate limiting were left out because they’d change what kind of app this is, not because they were hard to add โ know that boundary before deciding whether to cross it.
You’ve now built SKYWATCH from an empty repository to a full-featured, real-time flight tracker: a Postgres schema with database-level invariants, a poller and detection engine, a WebSocket realtime layer, a four-theme frontend, watch regions with history playback, and a full feature set on top โ airports, weather, alerts, the overflight log, multi-region compare, and a mobile layout, all verified end to end and backed by a real, if small, test suite. That’s the course.