eugine.me

full-stack platform for the grenada meteorological service

a multi-app monorepo supporting weather product publishing, operational monitoring, emergency planning, and internal workforce management

Summary

I designed and built a production-ready platform for the Grenada Meteorological Service — a multi-application monorepo consolidating weather product publishing, internal staff tooling, public-facing forecast display, and emergency preparedness documentation into a single coherent system. The work ran from architecture through to deployed applications, with me as the sole developer across the full stack. The platform is in operational use. The source is private.

Project Metadata

Background: Why This Problem Matters

A meteorological service isn't one product — it's several distinct operations running simultaneously. Forecasters publish official weather bulletins on fixed schedules. Aviation meteorologists produce METARs and TAFs. Marine teams issue bulletins with colour-coded alert levels. Emergency managers consult tropical cyclone procedures. Staff file leave requests, trade shifts, and submit timesheets.

Without shared infrastructure, each of these tends to accumulate as an isolated tool — inconsistent, hard to maintain, and expensive to evolve. Weather product formats carry regulatory weight: a METAR is not a TAF, and a CAP alert is not a marine bulletin. The data model has to be accurate because the outputs are official operational documents.

The goal was a platform where all of these operations could share authentication, UI conventions, and build tooling, while each application still owned its domain clearly.

My Role

I was the sole developer responsible for the full platform — architecture, implementation, and deployment. This included monorepo setup and tooling, the shared authentication system, all seven Next.js applications, the FastAPI backend, the PostgreSQL schema, API client generation, and Docker deployment configuration. Scope, design decisions, and implementation were all mine, with requirements and domain knowledge coming from the GMS team.

Requirements and Constraints

Development Environment

The platform was developed locally using Docker Compose to mirror the production environment. The monorepo used pnpm workspaces with Turbo v2 for build orchestration and Biome / Ultracite for linting and formatting — applied consistently across all applications from the start. The FastAPI backend was developed with hot reload and a local PostgreSQL instance. Kubb was used to generate the TypeScript API client from openapi.json directly, with types, Zod schemas, React Query hooks, and a fetch client all generated and committed to the repo.

Architecture and Approach

The platform is a pnpm workspace with Turbo v2 orchestrating builds. Applications are Next.js 16 / React 19 frontends; the backend is a FastAPI service with asyncpg and SQLModel.

apps/
  web-auth/        # centralised auth gateway
  wxproducts/      # forecast product publishing
  wxwatch/         # weather image monitoring
  spicewx/         # public weather portal
  hurricaneplan/   # emergency planning docs
  web-hr/          # HR form layouts
  admin-gms/       # internal staff dashboard
packages/
  ui/              # shared component library
  auth/            # shared auth client + server
  api-client/      # Kubb-generated TypeScript API client

Enforcing consistency from the start — linting, formatting, type checking, and environment variable handling working the same way in every application — made adding new apps much cheaper than if each had been bootstrapped independently.

The key architectural decision was centralised auth via a dedicated gateway rather than per-app authentication. The trade-off: routing complexity (safe return-to handling, shared cookie domains across ports) in exchange for consistent session management across all seven applications.

Engineering Process

Architecture decisions were made before any application existed. The monorepo structure, the dedicated auth gateway, the shared component library, and the OpenAPI codegen pipeline were all in place before the domain applications were built. This front-loaded complexity paid off: adding a new application meant wiring it to existing auth, existing UI primitives, and an existing API client — not rebuilding infrastructure.

Product schemas were designed modularly from the start, with one Zod schema per product type. A single generic schema would have been simpler to write but would have lost the structural distinctions that make official meteorological documents correct. The schema design was driven by domain requirements, not technical convenience.

The Kubb codegen pipeline kept the TypeScript client in sync with the Python backend without manual maintenance — changes to the FastAPI schema propagated to TypeScript types, Zod validators, and React Query hooks on the next generation run.

The Work

Centralised authentication (web-auth)

A dedicated gateway at port 3000 handles sign-in and sign-up. Unauthenticated requests from any app redirect there, with safe return-to routing back to the origin.

// packages/auth/src/server.ts
export async function getSession(req: Request): Promise<Session | null> {
  const token = req.cookies.get(SESSION_COOKIE)?.value;
  if (!token) return null;
  return exchangeToken(token);
}

The @grenmet/auth package exports both client and server utilities — session providers, hooks, cookie helpers, session creation, token refresh, and logout for single or all sessions. Sessions are shared across apps via a common cookie domain. The FastAPI backend issues session tokens, subsequently exchanged for short-lived JWTs on each API request.

Weather product publishing (wxproducts)

The operational publishing system handles structured ingestion and display of forecast products: morning, midday, and evening forecasts; marine bulletins with colour-coded alert levels; tropical weather outlooks; aviation reports (METAR, SPECI, TAF); BUFR; and CAP alerts.

[placeholder — wxproducts bulletin view]

Each product type has its own Zod schema:

export const marineSchema = z.object({
  issuedAt: z.string().datetime(),
  validFrom: z.string().datetime(),
  validTo: z.string().datetime(),
  alertLevel: z.enum(["none", "advisory", "watch", "warning"]),
  areas: z.array(marineAreaSchema),
});

Products are stored in PostgreSQL via Drizzle ORM. Print-optimised A4 page layouts support PDF export via Playwright.

Weather image monitoring (wxwatch)

wxwatch is an authentication-gated gallery of weather imagery sourced from external scrapers — satellite passes, radar composites, and model output graphics — browseable by date with lightbox display.

[placeholder — wxwatch gallery view]

Metadata per image includes spider name, file format, animation status, observation time, checksums, and raw JSON, stored in PostgreSQL. Forecasters use it to review recent satellite and radar data without leaving the platform.

Public weather portal (spicewx)

SpiceWX is the public-facing forecast hub: current conditions (temperature, humidity, wind, visibility, pressure, UV index), active weather alerts with severity-coded styling, a five-day forecast strip, and product tiles linking to marine, tropical, climate, and aviation pages.

[placeholder — spicewx public portal]

The interface is designed for public accessibility — responsive, clearly hierarchical, and styled to communicate alert severity through colour. It is ready for live data integration.

Hurricane emergency planning (hurricaneplan)

The hurricaneplan app is a searchable documentation portal covering tropical cyclone operational procedures for airport emergency management. Content is authored in MDX and covers pre-season, during-season, and post-event procedures, departmental responsibilities, personnel contacts, and vehicle coordination.

[placeholder — hurricaneplan search]

It uses FlexSearch for client-side full-text search, Algolia Autocomplete for enhanced search UX, Shiki for syntax highlighting, and Framer Motion for transitions. One consequence: the MDX toolchain required Webpack rather than Turbopack, which meant a build config divergence from the rest of the monorepo.

FastAPI backend

The Python backend covers two domains. The auth module manages users, roles, permissions, and session lifecycle — RBAC with JWT issuance. The HR module handles timesheets, duty rosters, shift scheduling, leave requests, shift exchanges, absentee tracking, daily status reports, and approval workflows.

@router.post("/sessions")
async def create_session(
    credentials: LoginRequest,
    db: AsyncSession = Depends(get_db),
):
    user = await authenticate_user(db, credentials.username, credentials.password)
    if not user:
        raise HTTPException(status_code=401)
    token = create_session_token(user)
    return SessionResponse(token=token)

The API uses asyncpg for async PostgreSQL access, Alembic for migrations, SlowAPI for rate limiting, and Sentry for error tracking.

HR and internal tools (web-hr, admin-gms)

The web-hr app provides print-optimised form layouts for leave applications, timesheets, shift exchange requisitions, and duty rosters — mirroring the physical documents used in operations while enabling digital submission and workflow routing.

The admin-gms dashboard provides metrics, FullCalendar scheduling, ApexCharts visualisations, TanStack Table for tabular data, TanStack Form for controlled inputs, file upload, and user management.

Outcome

What I Would Do Differently

The hurricaneplan MDX toolchain required a Webpack config divergence from the rest of the monorepo, which was a friction point throughout. I'd investigate whether that constraint still holds in current Next.js versions and, if so, isolate the MDX compilation earlier — either as a separate build step or in a dedicated package — rather than carrying the build config exception in the app itself.

I'd also formalise the API contract earlier. The Kubb codegen pipeline worked well, but it was introduced mid-project rather than from the start. Running the backend in OpenAPI-first mode from day one — schema defined before implementation — would have made the client generation pipeline cleaner and reduced the number of breaking changes that propagated through the TypeScript client during early development.