CodingNic

Testing, Verification & Next Steps

Next Steps

Testing, Verification & Next Steps 20 min read

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 dev to 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 a notFound flag 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 useAirportMetar already does: an in-memory Map for 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/server and apps/web each need their own Dockerfile โ€” 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 whole node_modules dev tree.
  • Environment variables move from .env to whatever your host provides โ€” DATABASE_URL pointing at a managed Postgres instance instead of the local Compose container, WEB_ORIGIN set to the real deployed frontend URL (Module 2’s CORS setup already reads this correctly, it just needs the right value), NEXT_PUBLIC_SERVER_URL set to the real deployed backend URL.
  • Migrations become a deploy step, not a local command โ€” npm run db:migrate needs 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 userId through 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 a limit query 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.ts already 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.