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
- Organisation: Grenada Meteorological Service
- Role: Sole developer (full-stack)
- Domain: National meteorological services / operational weather publishing
- Stack: TypeScript (strict) · Next.js 16 / React 19 · Python · FastAPI · SQLModel · asyncpg · PostgreSQL · Drizzle ORM · Alembic · pnpm workspaces · Turbo v2 · Biome / Ultracite · Docker / Docker Compose · TanStack Query, Table, Form · FullCalendar · ApexCharts · MDX · Shiki · FlexSearch · Framer Motion · Kubb · Playwright · JWT / session-based auth
- Timeline: 2024–2025
- Project type: Commissioned production platform / sole contractor
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
- Auth needed to work consistently across seven separate applications without per-app implementation
- Each weather product type had distinct structure and regulatory format — a single generic schema would have been lossy
- Print-optimised PDF export was required for operational bulletins and HR forms
- The public portal needed to be accessible and clearly communicative of alert severity
- The emergency planning app needed full-text search over MDX content
- The TypeScript API client needed to stay in sync with the Python backend without manual maintenance
- The entire platform needed to run self-hosted via Docker Compose
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
- Seven production applications deployed and in operational use at the Grenada Meteorological Service
- Centralised auth system shared across all apps with zero per-app auth implementation
- Structured, schema-validated weather product publishing for all operational product types
- Public weather portal ready for live data integration
- Searchable emergency planning documentation portal for airport operations
- TypeScript API client generated from OpenAPI spec — types, validators, and React Query hooks in sync with the Python backend
- Print-optimised PDF export for operational bulletins and HR forms
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.