4.6 KiB
4.6 KiB
TimeToLeave Architecture
TimeToLeave is an npm workspaces monorepo. The web app is both the browser UI and the backend proxy for external services; the mobile app calls that backend through the shared API client; shared domain logic lives in packages/core.
Monorepo Structure
TimeToLeave/
├── apps/
│ ├── web/ # Next.js 16 App Router UI and backend proxy
│ └── mobile/ # Expo 54 / React Native 0.81 app
├── packages/
│ ├── api-client/ # Shared client for /api/* backend routes
│ └── core/ # Shared types, defaults, parsing, scoring, formatting
├── docs/ # Documentation
└── package.json # Workspace scripts
Components
Web App: apps/web
- Next.js 16 App Router with React 19 and Tailwind CSS 4.
- User-facing routes are
/and/calendar. - Route handlers under
apps/web/src/app/api/**/route.tsproxy HAFAS, Nominatim, OSRM, Google Calendar, remote ICS, and Wiener Linien calls. apps/web/src/proxy.tsapplies strict CORS and per-IP rate limiting to/api/*.- Client state is kept in React context and persisted to
localStoragethroughuseEventsStoreanduseReminderSettings.
Mobile App: apps/mobile
- Expo 54 / React Native 0.81 with React Navigation 7.
- Screens cover event list, add/edit event, event detail, settings, and calendar import.
- Uses native APIs through
expo-calendar,expo-location,expo-notifications, andAsyncStorage. - Calls the web backend through
@timetoleave/api-client; device builds should setEXPO_PUBLIC_API_BASE_URL.
Core Package: packages/core
- Owns the shared TypeScript model: events, calendar events, stations, journeys, routes, reminder settings, geocoding, and Wiener Linien types.
- Provides Vienna-aware HAFAS time conversion through
parseHafasTime()andhafasDateTime(). - Parses HAFAS journey responses, ranks journeys, formats dates/durations/distances, and computes countdown/status labels.
- Exports default origin constants for the Mödling fallback origin.
API Client Package: packages/api-client
- Wraps the web backend routes from browser and mobile code.
- Supports base URL failover by accepting either one base URL or an array of base URLs.
- Builds HAFAS
TripSearchandLocMatchrequests, parses journey responses with@timetoleave/core, and includes an arrive-by fallback search window.
Data Flow
- Events enter through manual entry, remote ICS URL import, local ICS file parsing, Google Calendar on web, or native device calendars on mobile.
- Events are normalized to
EventorCalendarEventobjects and stored locally in the client. - The app resolves an origin station from saved settings, geolocation, or the shared default origin.
- The event destination is geocoded with Nominatim.
- HAFAS
LocMatchresolves the nearest destination station. - HAFAS
TripSearchfetches live journeys, with HAFAS time conversion handled bypackages/core. - OSRM provides optional bike and final-walk routes.
- Wiener Linien endpoints provide nearby stops and live departures around the destination.
- Countdown and leave-by calculations combine event time, selected transport mode, journey arrival, final walk duration, bike route duration, and reminder buffers.
External Integrations
| Service | Used for | Access path |
|---|---|---|
| ÖBB HAFAS | Station lookup and journey search | /api/hafas |
| ÖBB GTFS ZIP | Optional train metadata enrichment | apps/web/src/lib/oebb-gtfs.ts |
| Nominatim | Destination geocoding | /api/geocode |
| OSRM | Bike and foot routes | /api/bike-route, /api/walk-route |
| Wiener Linien Darwin | Nearby stops and live monitor data | /api/wienerlinien/* |
| Google Calendar | Web OAuth calendar import | /api/auth/google/*, /api/calendar/google |
| Remote ICS providers | Calendar URL import | /api/calendar |
Security and Operational Constraints
- Calendar URL imports validate provider domains, block private/reserved hosts, reject redirects, validate content type, and cap response size.
- HAFAS POST bodies are size-limited and only
TripSearchandLocMatchare allowed. - Coordinate APIs validate ranges and reject unrealistically distant route requests.
- API routes are rate-limited per client IP.
- CORS is allow-list based through
CORS_ALLOWED_ORIGINS. - Google OAuth tokens are stored in HTTP-only cookies.
- HAFAS time values must be treated as
Europe/Viennalocal strings. - This project uses Next.js 16. Before changing framework behavior, routing, route handlers, or proxy/middleware code, read the relevant guide in
node_modules/next/dist/docs/.