LuxBrief - Auth & Weather Architecture

Safety Ground Rules

LuxBrief is safety-critical aviation software. Flight planning errors, missing weather data, or incorrect compliance documentation can endanger lives.

Core Principles

  1. Safety over speed — Never prioritize time-to-market over correctness
  2. Reliability over features — Ship less, ship solid
  3. Never delete without asking — Soft-delete by default; no destructive migrations
  4. Ask when in doubt — Stop and ask on ambiguous safety/compliance decisions
  5. Aviation data is sacred — Weather, compliance, flights must never be lost

Development Rules

  • No destructive migrations (no DROP TABLE/COLUMN)
  • No silent failures (all errors logged with context)
  • Fail open for reads, fail closed for writes
  • EASA compliance is non-negotiable (12+ month retention)
  • Test weather parsing thoroughly (CAVOK, NSC, VRB, SPECI, TEMPO/BECMG edge cases)
  • Auth must be defense-in-depth (frontend + backend)
  • Multi-tenancy isolation is mandatory (all queries scoped by operator ID)

Data Retention

Data Type Retention Mutability Deletion
Weather readings 36 months min Immutable Never
Compliance reports 36 months min Immutable Never
Flight records 36 months min Editable (audit log) Soft-delete only
Aircraft records Indefinite Editable Soft-delete only
Audit logs 36 months min Immutable Never

Full safety rules are version-controlled in ~/dev/luxbrief/AGENTS.md.


Authentication Architecture

Stack

  • Identity Provider: Clerk (Cloud OIDC)
  • Frontend: Clerk Next.js SDK (@clerk/nextjs v7)
  • Backend: JWKS-based JWT validation (Go, golang-jwt/jwt/v5)
  • Architecture: Auth is a separate bounded context (internal/auth/), not just middleware

Auth Flow

1. User signs in via Clerk (frontend)
2. Clerk issues JWT with claims: sub, org_id, org_role, org_slug
3. Frontend sends: Authorization: Bearer <jwt> on every API call
4. Backend middleware:
   a. Extracts Bearer token from header
   b. Validates JWT signature via JWKS (cached RSA keys from Clerk)
   c. Checks exp, nbf, iss claims
   d. Extracts org_id → resolves to internal operator UUID via DB lookup
   e. Sets OperatorContext in Echo request context
5. Handler reads OperatorContext for tenant-scoped queries

Package Structure

internal/auth/
  service.go      # AuthService — ValidateToken(), OperatorResolver interface
  claims.go       # OperatorContext struct, ClerkClaims struct
  jwks.go         # JWKSCache — thread-safe RSA key cache (1hr TTL)
  middleware.go    # Echo middleware using AuthService
  errors.go       # ErrInvalidToken, ErrExpiredToken, etc.

Key Types

  • OperatorContext: OperatorID (uuid), UserID (string), OrgRole (string)
  • AuthService: Validates tokens, resolves Clerk org_id to internal operator UUID
  • JWKSCache: sync.RWMutex-protected map of kid*rsa.PublicKey, 1-hour TTL, refresh on unknown kid
  • OperatorResolver: Interface: GetByClerkOrgID(ctx, clerkOrgID) (uuid.UUID, error)

Roles (Clerk Organization Roles)

Role Scope Description
org:admin Platform admin Manages organizations (you)
org:billing Billing Manage billing, invoices, payment methods
org:chiefpilot Operations Full operational access, manage aircraft/users
org:dispatcher Operations Create flights, generate briefings
org:pilot Flight crew View briefings, acknowledge alerts
org:readonly Audit View only (auditors, trainees)

Current phase: Auth + identity only. RBAC enforcement deferred.

Clerk Organization Setup (Testing)

1. Enable Organizations

Clerk Dashboard → Organizations → Enable → Set "Allow users to create organizations" to OFF

2. Create Custom Roles

Dashboard → Organizations → Roles → Add: org:chiefpilot, org:dispatcher, org:pilot, org:billing, org:readonly

3. Create Test Organization

Dashboard → Organizations → Create: Name "Gestair", slug gestair. Note the org_2abc... ID.

4. Add Yourself

Organization → Members → Invite your email as org:admin

5. Seed Database

UPDATE operators SET clerk_org_id = 'org_YOUR_CLERK_ORG_ID'
WHERE id = '00000000-0000-0000-0000-000000000001';

6. Backend Environment

CLERK_ISSUER=https://your-clerk-domain.clerk.accounts.dev
CLERK_JWKS_URL=https://your-clerk-domain.clerk.accounts.dev/.well-known/jwks.json

7. Verify JWT Claims

Clerk JWTs must include: sub, org_id, org_role, org_slug. Check in Dashboard → JWT Templates.

Troubleshooting

  • No active org error: User must select org. Use <OrganizationSwitcher /> or set default.
  • JWT missing org_id: User not in active org session.
  • 401 from backend: Check CLERK_ISSUER matches JWT iss claim. Decode at jwt.io.
  • Operator not found: operators.clerk_org_id must match JWT org_id exactly.

Weather Architecture

Data Sources

Integrated

Source Data Pricing Status
AviationWeather.gov METAR, TAF, SIGMET FREE (NOAA) Primary — integrated, parsing incomplete

Planned

Source Data Pricing Status
FlightAware AeroAPI Weather obs/forecast, airport search, flight tracking Enterprise (per-query, $0.002/wx) Planned — airport autocomplete + weather enrichment
CheckWX METAR, TAF, G-AIRMET, AIRSIGMET Free: 3K/day, Pro: $7/mo (50K/day) Planned fallback

Evaluated, Deferred

Source Data Pricing Reason Deferred
AVWX REST METAR, TAF, PIREP, NOTAM (parsed + translated) Free basic Good parser but AWG already provides parsed fields
Windy.com Point forecasts (wind, temp, precip) 990 EUR/yr Not aviation-specific; useful for route wx visualization later
Open-Meteo General weather forecasts Free / 150 EUR/mo Same — supplemental route weather, not METAR/TAF

Non-Weather Data (Available)

Source Purpose Access
FlightAware AeroAPI Flight tracking, ETAs, airport search, delays Enterprise
Flightradar24 Flight tracking Contributor level
OpenSky Network ADS-B state vectors Available

Weather Persistence

Current State

  • weather_readings table exists in DB (migration 001) — never written to
  • WeatherReadingRepository interface defined in ports/repository.go — no implementation
  • Weather fetched live on every request (4 HTTP calls per briefing), never cached
  • WeatherData struct defined but Parsed field always nil (fields received but not mapped)

Target Architecture

Request → Check cache (GetLatestByStation)
  → Fresh? Return cached reading
  → Stale? Fetch live from AWG → Parse → Persist → Return
  → AWG down? Return cached with staleness warning
  → No cache, no AWG? Return error (do NOT show empty as "no weather")

Cache TTL

Type TTL Reasoning
METAR 30 minutes Issued hourly, SPECI possible anytime
TAF 6 hours Issued every 6 hours
SIGMET 1 hour When implemented

METAR Parsing (to implement)

Map AWG JSON fields → WeatherData struct: - TempTemperature - DewpDewPoint - WspdWindSpeed - WdirWindDirection - WgstWindGust - VisibilityVisibility - AltimPressure - Clouds[]Cloud - WxStringWeather - FlightCategory → new field (VFR/MVFR/IFR/LIFR)

FlightAware AeroAPI Integration (Planned)

  • Airport autocomplete: GET /airports?query=... → typeahead in flight creation form
  • Weather enrichment: GET /airports/{id}/weather/observations ($0.002/query), GET /airports/{id}/weather/forecast ($0.002/query)
  • Airport data: Name, ICAO, IATA, city, country, coordinates, delays
  • Base client: internal/adapters/flightaware/client.go with x-apikey header and rate limiting

Implementation Phases

Phase 1: Sidebar Fix (completed planning)

Remove "N" avatar (Clerk floating widget). Replace sidebar bottom with minimal: brand icon + version + theme toggle.

Phase 2: Auth Service

  • 2a: Create internal/auth/ package (Go)
  • 2b: Activate Clerk middleware (rename proxy.tsmiddleware.ts), add Bearer token to API client
  • 2c: Wire middleware in main.go, replace hardcoded UUIDs, add env vars to docker-compose

Phase 3: METAR/TAF Parsing

Map AWG response fields into WeatherData struct. Expand response structs. Add FlightCategory. Fix silent time parse errors. Unit tests.

Phase 4: Weather Persistence

Implement postgres/weather_repository.go. Add cache-before-fetch logic. Add weather history endpoint. Wire into services.

Phase 5: FlightAware AeroAPI

Airport autocomplete (search endpoint + frontend typeahead). Weather enrichment adapter. Rate limiting.

results matching ""

    No results matching ""