Transform real-time and historical NOAA buoy observations into reliable, well-structured APIs and derived sonification events for exploratory auditory analytics.
This is a TypeScript monorepo using pnpm workspaces with a three-layer architecture:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Infrastructure Layer (Docker) β
β PostgreSQL (database) + Redis (message bus) β
β Started once: docker compose up -d β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β (used by)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Application Layer (Node.js + TypeScript) β
β β’ Server (Fastify API + SSE streaming) β
β β’ Worker (NDBC data fetcher + Redis publisher) β
β Started separately: pnpm dev (with hot reload) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β (serves)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Client Layer (Browsers/Apps) β
β EventSource API consuming real-time observations β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Why separate layers?
- Docker services (PostgreSQL, Redis) are infrastructure - start once, rarely restart
- Node.js apps (Server, Worker) are your code - restart frequently during development
- Fast iteration: Hot reload without rebuilding Docker images
apps/
server/ - Fastify REST API + SSE streaming endpoint
worker/ - BullMQ job processor + NDBC data fetcher
web-demo/ - (Future) Web audio client demo
packages/
shared/ - Shared types and schemas
docs/
PRD.md - Product requirements
AUDIO_CLIENTS.md - Audio integration guide
SSE_IMPLEMENTATION.md - Real-time streaming architecture
Tech Stack:
- Runtime: Node.js 20+ with TypeScript
- Server: Fastify 5 + Prisma ORM
- Queue: BullMQ + Redis
- Database: PostgreSQL
- Streaming: Server-Sent Events (SSE) + Redis Pub/Sub
- Testing: Vitest
- Linting: ESLint + Prettier
- Node.js 20 or later
- pnpm 10+ (
npm install -g pnpm) - Docker & Docker Compose (for local Postgres + Redis)
git clone https://github.com/tsargent/buoy-data-project.git
cd buoy-sonification
pnpm install# Start PostgreSQL + Redis (infrastructure layer only)
docker compose up -d
# Verify services are running
docker compose psNote:
docker compose uponly starts database and Redis - NOT your application. The server and worker are Node.js apps started separately (next steps) for fast development iteration.
# Server environment
cp apps/server/.env.example apps/server/.env
# Worker environment
cp apps/worker/.env.example apps/worker/.env
# Default configuration works out of the box with Docker services
# Both use: postgresql://postgres:postgres@localhost:5432/buoyspnpm -F @app/server prisma:generate
pnpm -F @app/server prisma:migratepnpm -F @app/server prisma:seedThis seeds 5 active NOAA buoy stations:
- 44009: Delaware Bay
- 44013: Boston
- 46022: Eel River, CA
- 46050: Stonewall Bank, OR
- 42001: Mid-Gulf
# Terminal 1: Start API server (application layer)
pnpm -F @app/server dev
# Terminal 2: Start worker (application layer)
pnpm -F worker devWhy separate processes?
- Server = Fastify API + SSE streaming endpoint
- Worker = Background job that fetches NDBC data and publishes to Redis
- Both are Node.js apps (not Docker) for hot reload during development
- Both connect to Docker infrastructure (PostgreSQL + Redis)
The API will be available at http://localhost:3000.
Worker will automatically:
- Fetch latest observations from all active stations
- Parse and validate NOAA data
- Store observations in Postgres (handling missing sensors gracefully)
- Publish new observations to Redis (for real-time streaming)
- Ingest ~6,000-7,000 observations per station per run
Verify it's working:
# Check stations
curl http://localhost:3000/v1/stations | jq '.data | length'
# Wait ~10 seconds for first ingestion, then check observations
curl "http://localhost:3000/v1/observations/by-station/44009?limit=5" | jq '.meta.total'
# You should see thousands of observations per station!# Run all tests
pnpm test
# Run tests in watch mode
pnpm test
# Run tests with UI
pnpm test:ui
# Run server tests only
pnpm -F @app/server testNote: Integration tests expect a test database or will gracefully handle DB unavailability. For full integration testing, ensure Docker services are running.
pnpm build- Build all packagespnpm test- Run tests in watch modepnpm test:run- Run tests oncepnpm lint- Lint all codepnpm format- Format all code with Prettier
pnpm dev- Start dev server with hot reloadpnpm build- Build for productionpnpm start- Start production serverpnpm test- Run server testspnpm prisma:generate- Generate Prisma clientpnpm prisma:migrate- Run database migrationspnpm prisma:studio- Open Prisma Studio GUIpnpm prisma:seed- Seed database with sample NOAA buoy stations
pnpm dev- Start worker in dev mode (fetches real NOAA data every 5 min)
Base URL: http://localhost:3000
API Version: All API endpoints are versioned under /v1
Try it now:
# List all stations
curl http://localhost:3000/v1/stations | jq .
# Get latest observations from Delaware Bay buoy
curl "http://localhost:3000/v1/observations/by-station/44009?limit=5" | jq .GET /health- Service health check (no versioning)GET /metrics- Prometheus metrics endpoint (no versioning)
-
GET /v1/stations- List all active stations-
Query params:
page(optional): Page number, default 1limit(optional): Results per page, default 100, max 500
-
Response:
{ "data": [ ...stations... ], "meta": { "page": 1, "limit": 100, "total": 250 } }
-
-
GET /v1/stations/:id- Get station by ID
GET /v1/observations/by-station/:stationId- Get observations for a station-
Query params:
page(optional): Page number, default 1limit(optional): Results per page, default 100, max 500since(optional): ISO 8601 date, filter observations after this time
-
Response:
{ "data": [ ...observations... ], "meta": { "page": 1, "limit": 100, "total": 1500 } }
-
All errors follow a consistent shape per the project constitution:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid query parameters",
"details": { ... }
}
}Error codes: NOT_FOUND, VALIDATION_ERROR, INTERNAL_ERROR, UNAUTHORIZED, FORBIDDEN, BAD_REQUEST
All API endpoints are rate-limited to prevent abuse:
- Default: 100 requests per minute
- Observations endpoint: 60 requests per minute (stricter due to larger payloads)
Rate limit information is included in response headers:
x-ratelimit-limit: Total requests allowed in time windowx-ratelimit-remaining: Requests remaining in current windowx-ratelimit-reset: Timestamp when limit resets
When rate limit is exceeded, the API returns 429 Too Many Requests.
| Field | Type | Description |
|---|---|---|
| id | String | Station ID (e.g. "44009") |
| name | String | Station name |
| lat | Float | Latitude |
| lon | Float | Longitude |
| source | String | Data source (default "NDBC") |
| isActive | Boolean | Active status |
| createdAt | DateTime | Record creation time |
| updatedAt | DateTime | Last update time |
| Field | Type | Description |
|---|---|---|
| id | String | Unique observation ID (cuid) |
| stationId | String | Foreign key to Station |
| observedAt | DateTime | Observation timestamp |
| waveHeightM | Float? | Wave height in meters |
| windSpeedMps | Float? | Wind speed in m/s |
| windDirDeg | Int? | Wind direction in degrees |
| waterTempC | Float? | Water temperature in Celsius |
| pressureHpa | Float? | Atmospheric pressure in hPa |
| createdAt | DateTime | Record creation time |
See docs/AUDIO_CLIENTS.md for detailed guidance on integrating with:
- Web Audio API / Tone.js
- Max/MSP, Pure Data
- SuperCollider, ChucK
- TouchDesigner, VCV Rack
- Unity, Unreal Engine
Transports supported: SSE (default), WebSocket (future), OSC, MIDI, MQTT
- PRD (Product Requirements) - Full project requirements
- Audio Clients Guide - Sonification integration patterns
- Architecture Decision Records (ADRs) - Key architectural decisions
- Constitution - Project principles and governance
- Engineering Principles - Code quality standards
- Analysis Report - Current state assessment
- Write failing tests first (TDD per constitution)
- Implement feature
- Ensure tests pass:
pnpm test:run - Lint and format:
pnpm lint && pnpm format - Verify types compile:
pnpm build
- Edit
apps/server/prisma/schema.prisma - Run migration:
pnpm -F @app/server prisma:migrate - Regenerate client:
pnpm -F @app/server prisma:generate
- Server logs: Structured JSON via pino (configured in Fastify)
- Database queries: Set
DEBUG=prisma:*or use Prisma Studio (pnpm -F @app/server prisma:studio) - Worker jobs: Check Redis with
docker exec -it buoy-sonification-redis-1 redis-cli - View ingested data:
curl "http://localhost:3000/v1/observations/by-station/44009?limit=10" | jq .
This project adheres to strict engineering principles defined in our Constitution:
- Test-First Delivery (non-negotiable): Red β Green β Refactor
- Type-Centric Contracts: Explicit TypeScript types, runtime validation
- Observability by Design: Structured logging, metrics, error semantics
- Performance Awareness: No N+1 queries, pagination mandatory
- Security Baselines: Input validation, secrets in env vars, PII redaction
See Engineering Principles for detailed guidelines.
Phase: Production-ready (v0.1.0) - Phase 3 Complete β
Completed:
- β Test framework (Vitest) - 30 passing tests
- β Input validation (Zod schemas)
- β Error handling (standardized shapes)
- β API routes with pagination (stations, observations)
- β Database schema (Prisma ORM)
- β Docker Compose setup (Postgres + Redis)
- β CI/CD pipeline (GitHub Actions)
- β Metrics endpoint (Prometheus format)
- β Worker architecture (BullMQ + Redis)
- β Real data ingestion - Worker fetches live NOAA buoy data every 5 minutes
- β NDBC parser - Handles missing sensors, NaN values, and data validation
- β API versioning (/v1 prefix)
- β Rate limiting (100 req/min global, 60 req/min observations)
- β Security hardening (Helmet, CORS, JWT validation, PII redaction)
- β ADR documentation (Prisma, BullMQ, SSE)
- β Database seeding (5 active NOAA buoy stations)
- β Constitution Compliance: 100%
Live Data Available:
- π 30,000+ real observations ingested from 5 NOAA buoys
- π‘ Stations: Delaware Bay, Boston, Eel River, Stonewall Bank, Mid-Gulf
- π Data includes: wave height, wind speed/direction, water temp, pressure
- β»οΈ Auto-refresh every 5 minutes with latest observations
Planned (Phase 4+):
- π Real-time SSE stream endpoint
- π E2E smoke tests
- π Web demo client
- π OSC bridge for audio tools
- π Production deployment
See Analysis Report for detailed technical assessment.
- Follow the test-first mandate: write failing tests before implementation
- Ensure
pnpm lintandpnpm formatpass - Keep PRs focused (< ~400 LOC net change)
- Reference constitution principles in PR descriptions
- Add ADR for architectural decisions (
docs/adr/)
ISC
- Repository: github.com/tsargent/buoy-data-project
- NOAA NDBC Data: ndbc.noaa.gov
- Fastify Docs: fastify.dev
- Prisma Docs: prisma.io/docs
Onboarding Time Target: < 60 minutes from clone to first API request (per constitution 2.9)
Questions? See docs or open an issue.